Repository: saveourtool/diktat Branch: master Commit: cd77a0b839e1 Files: 967 Total size: 3.2 MB Directory structure: gitextract_zjq19om1/ ├── .editorconfig ├── .git-hooks/ │ ├── commit-msg.sh │ └── pre-commit.sh ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── config.yml │ ├── codecov.yml │ ├── pull_request_template.md │ └── workflows/ │ ├── build_and_test.yml │ ├── codeql-analysis.yml │ ├── dependencies.yml │ ├── detekt.yml │ ├── diktat.yml │ ├── diktat_snapshot.yml │ └── release.yml ├── .gitignore ├── CNAME ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── RELEASING.md ├── _config.yml ├── build.gradle.kts ├── detekt-config.yml ├── diktat-analysis.yml ├── diktat-api/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── kotlin/ │ └── com/ │ └── saveourtool/ │ └── diktat/ │ ├── Constants.kt │ ├── DiktatProcessor.kt │ ├── DiktatProcessorFactory.kt │ ├── DiktatRunner.kt │ ├── DiktatRunnerArguments.kt │ ├── DiktatRunnerFactory.kt │ ├── api/ │ │ ├── DiktatBaseline.kt │ │ ├── DiktatBaselineFactory.kt │ │ ├── DiktatCallback.kt │ │ ├── DiktatError.kt │ │ ├── DiktatErrorEmitter.kt │ │ ├── DiktatProcessorListener.kt │ │ ├── DiktatReporterCreationArguments.kt │ │ ├── DiktatReporterFactory.kt │ │ ├── DiktatReporterType.kt │ │ ├── DiktatRule.kt │ │ ├── DiktatRuleConfig.kt │ │ ├── DiktatRuleConfigReader.kt │ │ ├── DiktatRuleNameAware.kt │ │ ├── DiktatRuleSet.kt │ │ └── DiktatRuleSetFactory.kt │ ├── common/ │ │ └── config/ │ │ └── rules/ │ │ └── LegacyAliases.kt │ └── util/ │ ├── DiktatProcessorListenerWrapper.kt │ └── FileUtils.kt ├── diktat-cli/ │ ├── README.md │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ ├── kotlin/ │ │ │ └── com/ │ │ │ └── saveourtool/ │ │ │ └── diktat/ │ │ │ ├── DiktatMain.kt │ │ │ ├── cli/ │ │ │ │ ├── DiktatMode.kt │ │ │ │ └── DiktatProperties.kt │ │ │ └── util/ │ │ │ └── CliUtils.kt │ │ └── script/ │ │ ├── diktat.cmd │ │ └── header-diktat.sh │ └── test/ │ ├── kotlin/ │ │ └── com/ │ │ └── saveourtool/ │ │ └── diktat/ │ │ ├── smoke/ │ │ │ ├── DiktatCliTest.kt │ │ │ ├── DiktatSaveSmokeTest.kt │ │ │ ├── DiktatSmokeTest.kt │ │ │ ├── DiktatSmokeTestBase.kt │ │ │ └── DiktatSmokeTestUtils.kt │ │ └── util/ │ │ └── CliUtilsKtTest.kt │ └── resources/ │ └── test/ │ └── smoke/ │ ├── .editorconfig │ ├── .gitignore │ ├── build.gradle.kts_ │ ├── save.toml │ └── src/ │ ├── jsMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── saveourtool/ │ │ └── diktat/ │ │ └── scripts/ │ │ ├── ScriptExpected.kt │ │ └── ScriptTest.kt │ └── main/ │ └── kotlin/ │ ├── Bug1Expected.kt │ ├── Bug1Test.kt │ ├── DefaultPackageExpected.kt │ ├── DefaultPackageTest.kt │ ├── Example1-2Expected.kt │ ├── Example1Expected.kt │ ├── Example1Test.kt │ ├── Example2Expected.kt │ ├── Example2Test.kt │ ├── Example3Expected.kt │ ├── Example3Test.kt │ ├── Example4Expected.kt │ ├── Example4Test.kt │ ├── Example5Expected.kt │ ├── Example5Test.kt │ ├── Example6Expected.kt │ ├── Example6Test.kt │ ├── Example7Expected.kt │ ├── Example7Test.kt │ ├── Example8Expected.kt │ ├── Example8Test.kt │ ├── KdocFormattingMultilineTagsExpected.kt │ ├── KdocFormattingMultilineTagsTest.kt │ ├── LocalVariableWithOffsetExpected.kt │ ├── LocalVariableWithOffsetTest.kt │ ├── ManyLineTransformInLongLineExpected.kt │ ├── ManyLineTransformInLongLineTest.kt │ ├── NewlinesAfterInterfacesExpected.kt │ ├── NewlinesAfterInterfacesTest.kt │ ├── SemicolonsExpected.kt │ ├── SemicolonsTest.kt │ ├── kotlin-library-expected.gradle.kts │ ├── kotlin-library.gradle.kts │ ├── save.toml │ └── script/ │ ├── PackageInScriptExpected.kts │ ├── PackageInScriptTest.kts │ ├── SimpleRunInScriptExpected.kts │ └── SimpleRunInScriptTest.kts ├── diktat-common-test/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── kotlin/ │ └── com/ │ └── saveourtool/ │ └── diktat/ │ └── test/ │ └── framework/ │ ├── processing/ │ │ ├── ResourceReader.kt │ │ ├── TestComparatorUnit.kt │ │ └── TestFileContent.kt │ └── util/ │ └── TestUtils.kt ├── diktat-dev-ksp/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ ├── kotlin/ │ │ └── com/ │ │ └── saveourtool/ │ │ └── diktat/ │ │ └── ruleset/ │ │ └── generation/ │ │ ├── EnumNames.kt │ │ ├── EnumNamesSymbolProcessor.kt │ │ └── EnumNamesSymbolProcessorProvider.kt │ └── resources/ │ └── META-INF/ │ └── services/ │ └── com.google.devtools.ksp.processing.SymbolProcessorProvider ├── diktat-gradle-plugin/ │ ├── README.md │ ├── build.gradle.kts │ └── src/ │ ├── functionalTest/ │ │ └── kotlin/ │ │ └── com/ │ │ └── saveourtool/ │ │ └── diktat/ │ │ └── plugin/ │ │ └── gradle/ │ │ ├── DiktatGradlePluginFunctionalTest.kt │ │ ├── DiktatGradlePluginGroovyFunctionalTest.kt │ │ ├── DiktatGradlePluginMultiprojectFunctionalTest.kt │ │ └── Utils.kt │ ├── main/ │ │ └── kotlin/ │ │ └── com/ │ │ └── saveourtool/ │ │ └── diktat/ │ │ └── plugin/ │ │ └── gradle/ │ │ ├── DiktatExtension.kt │ │ ├── DiktatGradlePlugin.kt │ │ ├── Utils.kt │ │ ├── extension/ │ │ │ ├── DefaultReporter.kt │ │ │ ├── Reporter.kt │ │ │ └── Reporters.kt │ │ └── tasks/ │ │ ├── DiktatCheckTask.kt │ │ ├── DiktatFixTask.kt │ │ ├── DiktatTaskBase.kt │ │ └── SarifReportMergeTask.kt │ └── test/ │ └── kotlin/ │ └── com/ │ └── saveourtool/ │ └── diktat/ │ └── plugin/ │ └── gradle/ │ ├── DiktatGradlePluginTest.kt │ └── DiktatJavaExecTaskTest.kt ├── diktat-ktlint-engine/ │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ └── kotlin/ │ │ └── com/ │ │ ├── pinterest/ │ │ │ └── ktlint/ │ │ │ └── rule/ │ │ │ └── engine/ │ │ │ └── api/ │ │ │ └── Code.kt │ │ └── saveourtool/ │ │ └── diktat/ │ │ └── ktlint/ │ │ ├── DiktatBaselineFactoryImpl.kt │ │ ├── DiktatProcessorFactoryImpl.kt │ │ ├── DiktatReporterFactoryImpl.kt │ │ ├── DiktatReporterImpl.kt │ │ ├── KtLintRuleWrapper.kt │ │ ├── KtLintUtils.kt │ │ └── ReporterV2Wrapper.kt │ └── test/ │ └── kotlin/ │ └── com/ │ └── saveourtool/ │ └── diktat/ │ └── ktlint/ │ └── KtLintRuleWrapperTest.kt ├── diktat-maven-plugin/ │ ├── README.md │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ └── kotlin/ │ │ └── com/ │ │ └── saveourtool/ │ │ └── diktat/ │ │ └── plugin/ │ │ └── maven/ │ │ ├── DiktatBaseMojo.kt │ │ ├── DiktatMojo.kt │ │ ├── Utils.kt │ │ └── reporters/ │ │ ├── DefaultReporter.kt │ │ ├── GitHubActionsReporter.kt │ │ ├── Reporter.kt │ │ └── Reporters.kt │ └── test/ │ ├── kotlin/ │ │ └── com/ │ │ └── saveourtool/ │ │ └── diktat/ │ │ └── plugin/ │ │ └── maven/ │ │ ├── DiktatBaseMojoTest.kt │ │ └── DiktatMavenPluginIntegrationTest.kt │ └── resources/ │ └── .mvn/ │ ├── jvm.config │ └── maven.config ├── diktat-rules/ │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ ├── kotlin/ │ │ │ └── com/ │ │ │ └── saveourtool/ │ │ │ └── diktat/ │ │ │ ├── common/ │ │ │ │ └── config/ │ │ │ │ └── rules/ │ │ │ │ └── LegacyUtils.kt │ │ │ └── ruleset/ │ │ │ ├── config/ │ │ │ │ ├── AbstractDiktatRuleConfigReader.kt │ │ │ │ ├── CommonConfiguration.kt │ │ │ │ ├── DiktatRuleConfigYamlReader.kt │ │ │ │ └── RuleConfiguration.kt │ │ │ ├── constants/ │ │ │ │ ├── Chapters.kt │ │ │ │ └── Warnings.kt │ │ │ ├── rules/ │ │ │ │ ├── DiktatRule.kt │ │ │ │ ├── DiktatRuleSetFactoryImpl.kt │ │ │ │ ├── chapter1/ │ │ │ │ │ ├── FileNaming.kt │ │ │ │ │ ├── IdentifierNaming.kt │ │ │ │ │ └── PackageNaming.kt │ │ │ │ ├── chapter2/ │ │ │ │ │ ├── comments/ │ │ │ │ │ │ ├── CommentsRule.kt │ │ │ │ │ │ └── HeaderCommentRule.kt │ │ │ │ │ └── kdoc/ │ │ │ │ │ ├── CommentsFormatting.kt │ │ │ │ │ ├── KdocComments.kt │ │ │ │ │ ├── KdocFormatting.kt │ │ │ │ │ └── KdocMethods.kt │ │ │ │ ├── chapter3/ │ │ │ │ │ ├── AnnotationNewLineRule.kt │ │ │ │ │ ├── BlockStructureBraces.kt │ │ │ │ │ ├── BooleanExpressionsRule.kt │ │ │ │ │ ├── BracesInConditionalsAndLoopsRule.kt │ │ │ │ │ ├── ClassLikeStructuresOrderRule.kt │ │ │ │ │ ├── CollapseIfStatementsRule.kt │ │ │ │ │ ├── ConsecutiveSpacesRule.kt │ │ │ │ │ ├── DebugPrintRule.kt │ │ │ │ │ ├── EmptyBlock.kt │ │ │ │ │ ├── EnumsSeparated.kt │ │ │ │ │ ├── LineLength.kt │ │ │ │ │ ├── LongNumericalValuesSeparatedRule.kt │ │ │ │ │ ├── MagicNumberRule.kt │ │ │ │ │ ├── MultipleModifiersSequence.kt │ │ │ │ │ ├── NullableTypeRule.kt │ │ │ │ │ ├── PreviewAnnotationRule.kt │ │ │ │ │ ├── RangeConventionalRule.kt │ │ │ │ │ ├── SingleLineStatementsRule.kt │ │ │ │ │ ├── SortRule.kt │ │ │ │ │ ├── StringConcatenationRule.kt │ │ │ │ │ ├── StringTemplateFormatRule.kt │ │ │ │ │ ├── TrailingCommaRule.kt │ │ │ │ │ ├── WhenMustHaveElseRule.kt │ │ │ │ │ ├── files/ │ │ │ │ │ │ ├── BlankLinesRule.kt │ │ │ │ │ │ ├── FileSize.kt │ │ │ │ │ │ ├── FileStructureRule.kt │ │ │ │ │ │ ├── IndentationAmount.kt │ │ │ │ │ │ ├── IndentationAware.kt │ │ │ │ │ │ ├── IndentationConfigAware.kt │ │ │ │ │ │ ├── IndentationError.kt │ │ │ │ │ │ ├── IndentationRule.kt │ │ │ │ │ │ ├── IndentedElementType.kt │ │ │ │ │ │ ├── NewlinesRule.kt │ │ │ │ │ │ ├── SemicolonsRule.kt │ │ │ │ │ │ ├── TopLevelOrderRule.kt │ │ │ │ │ │ └── WhiteSpaceRule.kt │ │ │ │ │ └── identifiers/ │ │ │ │ │ └── LocalVariablesRule.kt │ │ │ │ ├── chapter4/ │ │ │ │ │ ├── ImmutableValNoVarRule.kt │ │ │ │ │ ├── NullChecksRule.kt │ │ │ │ │ ├── SmartCastRule.kt │ │ │ │ │ ├── TypeAliasRule.kt │ │ │ │ │ ├── VariableGenericTypeDeclarationRule.kt │ │ │ │ │ └── calculations/ │ │ │ │ │ └── AccurateCalculationsRule.kt │ │ │ │ ├── chapter5/ │ │ │ │ │ ├── AsyncAndSyncRule.kt │ │ │ │ │ ├── AvoidNestedFunctionsRule.kt │ │ │ │ │ ├── CheckInverseMethodRule.kt │ │ │ │ │ ├── CustomLabel.kt │ │ │ │ │ ├── FunctionArgumentsSize.kt │ │ │ │ │ ├── FunctionLength.kt │ │ │ │ │ ├── LambdaLengthRule.kt │ │ │ │ │ ├── LambdaParameterOrder.kt │ │ │ │ │ ├── NestedFunctionBlock.kt │ │ │ │ │ ├── OverloadingArgumentsFunction.kt │ │ │ │ │ └── ParameterNameInOuterLambdaRule.kt │ │ │ │ └── chapter6/ │ │ │ │ ├── AvoidEmptyPrimaryConstructor.kt │ │ │ │ ├── AvoidUtilityClass.kt │ │ │ │ ├── CustomGetterSetterRule.kt │ │ │ │ ├── ExtensionFunctionsInFileRule.kt │ │ │ │ ├── ExtensionFunctionsSameNameRule.kt │ │ │ │ ├── ImplicitBackingPropertyRule.kt │ │ │ │ ├── PropertyAccessorFields.kt │ │ │ │ ├── RunInScript.kt │ │ │ │ ├── TrivialPropertyAccessors.kt │ │ │ │ ├── UseLastIndex.kt │ │ │ │ ├── UselessSupertype.kt │ │ │ │ └── classes/ │ │ │ │ ├── AbstractClassesRule.kt │ │ │ │ ├── CompactInitialization.kt │ │ │ │ ├── DataClassesRule.kt │ │ │ │ ├── InlineClassesRule.kt │ │ │ │ ├── SingleConstructorRule.kt │ │ │ │ ├── SingleInitRule.kt │ │ │ │ └── StatelessClassesRule.kt │ │ │ └── utils/ │ │ │ ├── AstConstants.kt │ │ │ ├── AstNodeUtils.kt │ │ │ ├── AstNodeUtilsFromKtLint.kt │ │ │ ├── FileUtils.kt │ │ │ ├── FunctionAstNodeUtils.kt │ │ │ ├── KdocUtils.kt │ │ │ ├── KotlinParseException.kt │ │ │ ├── KotlinParser.kt │ │ │ ├── PositionInTextLocator.kt │ │ │ ├── PsiUtils.kt │ │ │ ├── StringCaseUtils.kt │ │ │ ├── StringUtils.kt │ │ │ ├── indentation/ │ │ │ │ ├── Checkers.kt │ │ │ │ ├── CustomIndentationChecker.kt │ │ │ │ └── IndentationConfig.kt │ │ │ └── search/ │ │ │ ├── VariablesSearch.kt │ │ │ ├── VariablesWithAssignmentSearch.kt │ │ │ └── VariablesWithUsagesSearch.kt │ │ └── resources/ │ │ ├── diktat-analysis-huawei.yml │ │ └── diktat-analysis.yml │ └── test/ │ ├── kotlin/ │ │ └── com/ │ │ └── saveourtool/ │ │ └── diktat/ │ │ ├── ruleset/ │ │ │ ├── chapter1/ │ │ │ │ ├── EnumValueCaseTest.kt │ │ │ │ ├── IdentifierNamingFixTest.kt │ │ │ │ ├── IdentifierNamingWarnTest.kt │ │ │ │ ├── MethodNamingWarnTest.kt │ │ │ │ ├── PackageNamingFixTest.kt │ │ │ │ ├── PackageNamingWarnTest.kt │ │ │ │ └── PackagePathFixTest.kt │ │ │ ├── chapter2/ │ │ │ │ ├── CommentsFormattingFixTest.kt │ │ │ │ ├── CommentsFormattingTest.kt │ │ │ │ ├── HeaderCommentRuleFixTest.kt │ │ │ │ ├── HeaderCommentRuleTest.kt │ │ │ │ ├── KdocCommentsFixTest.kt │ │ │ │ ├── KdocCommentsWarnTest.kt │ │ │ │ ├── KdocFormattingFixTest.kt │ │ │ │ ├── KdocFormattingTest.kt │ │ │ │ ├── KdocMethodsFixTest.kt │ │ │ │ ├── KdocMethodsTest.kt │ │ │ │ ├── KdocParamPresentWarnTest.kt │ │ │ │ └── comments/ │ │ │ │ └── CommentedCodeTest.kt │ │ │ ├── chapter3/ │ │ │ │ ├── AnnotationNewLineRuleFixTest.kt │ │ │ │ ├── AnnotationNewLineRuleWarnTest.kt │ │ │ │ ├── BlockStructureBracesFixTest.kt │ │ │ │ ├── BlockStructureBracesWarnTest.kt │ │ │ │ ├── BooleanExpressionsRuleFixTest.kt │ │ │ │ ├── BooleanExpressionsRuleWarnTest.kt │ │ │ │ ├── BracesRuleFixTest.kt │ │ │ │ ├── BracesRuleWarnTest.kt │ │ │ │ ├── ClassLikeStructuresOrderFixTest.kt │ │ │ │ ├── ClassLikeStructuresOrderRuleWarnTest.kt │ │ │ │ ├── CollapseIfStatementsRuleFixTest.kt │ │ │ │ ├── CollapseIfStatementsRuleWarnTest.kt │ │ │ │ ├── ConsecutiveSpacesRuleFixTest.kt │ │ │ │ ├── ConsecutiveSpacesRuleWarnTest.kt │ │ │ │ ├── DebugPrintRuleWarnTest.kt │ │ │ │ ├── EmptyBlockFixTest.kt │ │ │ │ ├── EmptyBlockWarnTest.kt │ │ │ │ ├── EnumsSeparatedFixTest.kt │ │ │ │ ├── EnumsSeparatedWarnTest.kt │ │ │ │ ├── FileSizeWarnTest.kt │ │ │ │ ├── FileStructureRuleFixTest.kt │ │ │ │ ├── FileStructureRuleTest.kt │ │ │ │ ├── LineLengthFixTest.kt │ │ │ │ ├── LineLengthWarnTest.kt │ │ │ │ ├── LocalVariablesWarnTest.kt │ │ │ │ ├── LongNumericalValuesSeparatedFixTest.kt │ │ │ │ ├── LongNumericalValuesSeparatedWarnTest.kt │ │ │ │ ├── MagicNumberRuleWarnTest.kt │ │ │ │ ├── MultipleModifiersSequenceFixTest.kt │ │ │ │ ├── MultipleModifiersSequenceWarnTest.kt │ │ │ │ ├── NullableTypeRuleFixTest.kt │ │ │ │ ├── NullableTypeRuleWarnTest.kt │ │ │ │ ├── PreviewAnnotationFixTest.kt │ │ │ │ ├── PreviewAnnotationWarnTest.kt │ │ │ │ ├── RangeConventionalRuleFixTest.kt │ │ │ │ ├── RangeConventionalRuleWarnTest.kt │ │ │ │ ├── SingleLineStatementsRuleFixTest.kt │ │ │ │ ├── SingleLineStatementsRuleWarnTest.kt │ │ │ │ ├── SortRuleFixTest.kt │ │ │ │ ├── SortRuleWarnTest.kt │ │ │ │ ├── StringConcatenationRuleFixTest.kt │ │ │ │ ├── StringConcatenationWarnTest.kt │ │ │ │ ├── StringTemplateRuleFixTest.kt │ │ │ │ ├── StringTemplateRuleWarnTest.kt │ │ │ │ ├── SuperClassListWarnTest.kt │ │ │ │ ├── TrailingCommaFixTest.kt │ │ │ │ ├── TrailingCommaWarnTest.kt │ │ │ │ ├── WhenMustHaveElseFixTest.kt │ │ │ │ ├── WhenMustHaveElseWarnTest.kt │ │ │ │ ├── files/ │ │ │ │ │ ├── BlankLinesFixTest.kt │ │ │ │ │ ├── BlankLinesWarnTest.kt │ │ │ │ │ ├── NewlinesRuleFixTest.kt │ │ │ │ │ ├── NewlinesRuleWarnTest.kt │ │ │ │ │ ├── SemicolonsRuleFixTest.kt │ │ │ │ │ ├── SemicolonsRuleWarnTest.kt │ │ │ │ │ ├── TopLevelOrderRuleFixTest.kt │ │ │ │ │ └── TopLevelOrderRuleWarnTest.kt │ │ │ │ └── spaces/ │ │ │ │ ├── ExpectedIndentationError.kt │ │ │ │ ├── IndentationConfigAwareTest.kt │ │ │ │ ├── IndentationConfigFactory.kt │ │ │ │ ├── IndentationRuleFixTest.kt │ │ │ │ ├── IndentationRuleTest.kt │ │ │ │ ├── IndentationRuleTestSuite.kt │ │ │ │ ├── IndentationRuleTestUtils.kt │ │ │ │ ├── IndentationRuleWarnTest.kt │ │ │ │ ├── WhiteSpaceRuleFixTest.kt │ │ │ │ ├── WhiteSpaceRuleWarnTest.kt │ │ │ │ └── junit/ │ │ │ │ ├── IndentationTest.kt │ │ │ │ ├── IndentationTestExtension.kt │ │ │ │ ├── IndentationTestFixExtension.kt │ │ │ │ ├── IndentationTestFixInvocationContext.kt │ │ │ │ ├── IndentationTestInput.kt │ │ │ │ ├── IndentationTestInvocationContext.kt │ │ │ │ ├── IndentationTestInvocationContextProvider.kt │ │ │ │ ├── IndentationTestWarnExtension.kt │ │ │ │ ├── IndentationTestWarnInvocationContext.kt │ │ │ │ └── IndentedSourceCode.kt │ │ │ ├── chapter4/ │ │ │ │ ├── AccurateCalculationsWarnTest.kt │ │ │ │ ├── NoVarRuleWarnTest.kt │ │ │ │ ├── NullChecksRuleFixTest.kt │ │ │ │ ├── NullChecksRuleWarnTest.kt │ │ │ │ ├── SmartCastRuleFixTest.kt │ │ │ │ ├── SmartCastRuleWarnTest.kt │ │ │ │ ├── TypeAliasRuleWarnTest.kt │ │ │ │ ├── VariableGenericTypeDeclarationRuleFixTest.kt │ │ │ │ └── VariableGenericTypeDeclarationRuleWarnTest.kt │ │ │ ├── chapter5/ │ │ │ │ ├── AsyncAndSyncRuleTest.kt │ │ │ │ ├── AvoidNestedFunctionsFixTest.kt │ │ │ │ ├── AvoidNestedFunctionsWarnTest.kt │ │ │ │ ├── CheckInverseMethodRuleFixTest.kt │ │ │ │ ├── CheckInverseMethodRuleWarnTest.kt │ │ │ │ ├── CustomLabelsTest.kt │ │ │ │ ├── FunctionArgumentsSizeWarnTest.kt │ │ │ │ ├── FunctionLengthWarnTest.kt │ │ │ │ ├── LambdaLengthWarnTest.kt │ │ │ │ ├── LambdaParameterOrderWarnTest.kt │ │ │ │ ├── NestedFunctionBlockWarnTest.kt │ │ │ │ ├── OverloadingArgumentsFunctionWarnTest.kt │ │ │ │ └── ParameterNameInOuterLambdaRuleWarnTest.kt │ │ │ ├── chapter6/ │ │ │ │ ├── AbstractClassesFixTest.kt │ │ │ │ ├── AbstractClassesWarnTest.kt │ │ │ │ ├── AvoidUtilityClassWarnTest.kt │ │ │ │ ├── CompactInitializationFixTest.kt │ │ │ │ ├── CompactInitializationWarnTest.kt │ │ │ │ ├── CustomGetterSetterWarnTest.kt │ │ │ │ ├── DataClassesRuleWarnTest.kt │ │ │ │ ├── EmptyPrimaryConstructorFixTest.kt │ │ │ │ ├── EmptyPrimaryConstructorWarnTest.kt │ │ │ │ ├── ExtensionFunctionsInFileWarnTest.kt │ │ │ │ ├── ExtensionFunctionsSameNameWarnTest.kt │ │ │ │ ├── ImplicitBackingPropertyWarnTest.kt │ │ │ │ ├── InlineClassesWarnTest.kt │ │ │ │ ├── PropertyAccessorFieldsWarnTest.kt │ │ │ │ ├── RunInScriptFixTest.kt │ │ │ │ ├── RunInScriptWarnTest.kt │ │ │ │ ├── SingleConstructorRuleFixTest.kt │ │ │ │ ├── SingleConstructorRuleWarnTest.kt │ │ │ │ ├── SingleInitRuleFixTest.kt │ │ │ │ ├── SingleInitRuleWarnTest.kt │ │ │ │ ├── StatelessClassesRuleFixTest.kt │ │ │ │ ├── StatelessClassesRuleWarnTest.kt │ │ │ │ ├── TrivialPropertyAccessorsFixTest.kt │ │ │ │ ├── TrivialPropertyAccessorsWarnTest.kt │ │ │ │ ├── UseLastIndexFixTest.kt │ │ │ │ ├── UseLastIndexWarnTest.kt │ │ │ │ ├── UselessSupertypeFixTest.kt │ │ │ │ └── UselessSupertypeWarnTest.kt │ │ │ ├── config/ │ │ │ │ └── DiktatRuleConfigYamlReaderTest.kt │ │ │ ├── junit/ │ │ │ │ ├── BooleanOrDefault.kt │ │ │ │ ├── CloseablePath.kt │ │ │ │ ├── ExpectedLintError.kt │ │ │ │ ├── ExpectedLintErrors.kt │ │ │ │ ├── NaturalDisplayName.kt │ │ │ │ └── RuleInvocationContextProvider.kt │ │ │ ├── smoke/ │ │ │ │ └── RulesConfigValidationTest.kt │ │ │ └── utils/ │ │ │ ├── AstNodeUtilsTest.kt │ │ │ ├── AvailableRulesDocTest.kt │ │ │ ├── FunctionAstNodeUtilsTest.kt │ │ │ ├── KotlinParserTest.kt │ │ │ ├── RulesConfigYamlTest.kt │ │ │ ├── StringCaseUtilsTest.kt │ │ │ ├── SuppressAnnotatedExpressionTest.kt │ │ │ ├── SuppressTest.kt │ │ │ ├── VariablesSearchTest.kt │ │ │ ├── VariablesWithAssignmentsSearchTest.kt │ │ │ ├── VariablesWithUsagesSearchTest.kt │ │ │ └── WarningsGenerationTest.kt │ │ └── util/ │ │ ├── DiktatRuleSetFactoryImplTest.kt │ │ ├── DiktatRuleTest.kt │ │ ├── FixTestBase.kt │ │ ├── LintTestBase.kt │ │ ├── SuppressingTest.kt │ │ └── TestUtils.kt │ └── resources/ │ ├── log4j2.properties │ ├── test/ │ │ ├── chapter6/ │ │ │ ├── abstract_classes/ │ │ │ │ ├── ShouldReplaceAbstractKeywordExpected.kt │ │ │ │ └── ShouldReplaceAbstractKeywordTest.kt │ │ │ ├── classes/ │ │ │ │ ├── AssignmentWithLocalPropertyExpected.kt │ │ │ │ ├── AssignmentWithLocalPropertyTest.kt │ │ │ │ ├── ConstructorShouldKeepExpressionsOrderExpected.kt │ │ │ │ ├── ConstructorShouldKeepExpressionsOrderTest.kt │ │ │ │ ├── ConstructorWithCommentsExpected.kt │ │ │ │ ├── ConstructorWithCommentsTest.kt │ │ │ │ ├── ConstructorWithComplexAssignmentsExpected.kt │ │ │ │ ├── ConstructorWithComplexAssignmentsTest.kt │ │ │ │ ├── ConstructorWithCustomAssignmentsExpected.kt │ │ │ │ ├── ConstructorWithCustomAssignmentsTest.kt │ │ │ │ ├── ConstructorWithInitExpected.kt │ │ │ │ ├── ConstructorWithInitTest.kt │ │ │ │ ├── ConstructorWithModifiersExpected.kt │ │ │ │ ├── ConstructorWithModifiersTest.kt │ │ │ │ ├── SimpleConstructorExpected.kt │ │ │ │ └── SimpleConstructorTest.kt │ │ │ ├── compact_initialization/ │ │ │ │ ├── ApplyOnStatementsWithThisKeywordExpected.kt │ │ │ │ ├── ApplyOnStatementsWithThisKeywordTest.kt │ │ │ │ ├── ApplyWithValueArgumentExpected.kt │ │ │ │ ├── ApplyWithValueArgumentTest.kt │ │ │ │ ├── ExampleWithCommentsExpected.kt │ │ │ │ ├── ExampleWithCommentsTest.kt │ │ │ │ ├── ParenthesizedReceiverExpected.kt │ │ │ │ ├── ParenthesizedReceiverTest.kt │ │ │ │ ├── SimpleExampleExpected.kt │ │ │ │ ├── SimpleExampleTest.kt │ │ │ │ ├── StatementUseFieldMultipleTimesExpected.kt │ │ │ │ └── StatementUseFieldMultipleTimesTest.kt │ │ │ ├── init_blocks/ │ │ │ │ ├── InitBlockWithAssignmentsExpected.kt │ │ │ │ ├── InitBlockWithAssignmentsTest.kt │ │ │ │ ├── InitBlocksExpected.kt │ │ │ │ ├── InitBlocksTest.kt │ │ │ │ ├── InitBlocksWithAssignmentsExpected.kt │ │ │ │ └── InitBlocksWithAssignmentsTest.kt │ │ │ ├── lastIndex_change/ │ │ │ │ ├── IncorrectUseLengthMinusOneExpected.kt │ │ │ │ ├── IncorrectUseLengthMinusOneTest.kt │ │ │ │ ├── UseAnyWhiteSpacesExpected.kt │ │ │ │ └── UseAnyWhiteSpacesTest.kt │ │ │ ├── primary_constructor/ │ │ │ │ ├── EmptyPCExpected.kt │ │ │ │ └── EmptyPCTest.kt │ │ │ ├── properties/ │ │ │ │ ├── TrivialPropertyAccessorsExpected.kt │ │ │ │ └── TrivialPropertyAccessorsTest.kt │ │ │ ├── script/ │ │ │ │ ├── SimpleRunInScriptExpected.kts │ │ │ │ └── SimpleRunInScriptTest.kts │ │ │ └── stateless_classes/ │ │ │ ├── StatelessClassExpected.kt │ │ │ └── StatelessClassTest.kt │ │ ├── paragraph1/ │ │ │ └── naming/ │ │ │ ├── class_/ │ │ │ │ ├── IncorrectClassNameExpected.kt │ │ │ │ └── IncorrectClassNameTest.kt │ │ │ ├── enum_/ │ │ │ │ ├── EnumValuePascalCaseExpected.kt │ │ │ │ ├── EnumValuePascalCaseTest.kt │ │ │ │ ├── EnumValueSnakeCaseExpected.kt │ │ │ │ └── EnumValueSnakeCaseTest.kt │ │ │ ├── file/ │ │ │ │ ├── fileNameTest.kt │ │ │ │ └── file_nameTest.kt │ │ │ ├── function/ │ │ │ │ ├── FunctionNameExpected.kt │ │ │ │ └── FunctionNameTest.kt │ │ │ ├── generic/ │ │ │ │ ├── GenericFunctionExpected.kt │ │ │ │ └── GenericFunctionTest.kt │ │ │ ├── identifiers/ │ │ │ │ ├── ConstantValNameExpected.kt │ │ │ │ ├── ConstantValNameTest.kt │ │ │ │ ├── IdentifierNameRegressionExpected.kt │ │ │ │ ├── IdentifierNameRegressionTest.kt │ │ │ │ ├── LambdaArgExpected.kt │ │ │ │ ├── LambdaArgTest.kt │ │ │ │ ├── PrefixInNameExpected.kt │ │ │ │ ├── PrefixInNameTest.kt │ │ │ │ ├── PropertyInKdocExpected.kt │ │ │ │ ├── PropertyInKdocTest.kt │ │ │ │ ├── TypeAliasNameExpected.kt │ │ │ │ ├── TypeAliasNameTest.kt │ │ │ │ ├── VariableNamingExpected.kt │ │ │ │ └── VariableNamingTest.kt │ │ │ ├── object_/ │ │ │ │ ├── IncorrectObjectNameExpected.kt │ │ │ │ └── IncorrectObjectNameTest.kt │ │ │ └── package/ │ │ │ ├── FixUnderscoreExpected.kt │ │ │ ├── FixUnderscoreTest.kt │ │ │ ├── FixUpperExpected.kt │ │ │ ├── FixUpperTest.kt │ │ │ ├── MissingDomainNameExpected.kt │ │ │ ├── MissingDomainNameTest.kt │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── kotlin/ │ │ │ ├── com/ │ │ │ │ └── saveourtool/ │ │ │ │ └── diktat/ │ │ │ │ └── some/ │ │ │ │ └── name/ │ │ │ │ ├── FixIncorrectExpected.kt │ │ │ │ ├── FixIncorrectTest.kt │ │ │ │ ├── FixMissingExpected.kt │ │ │ │ ├── FixMissingTest.kt │ │ │ │ ├── FixMissingWithAnnotationExpected.kt │ │ │ │ ├── FixMissingWithAnnotationExpected2.kt │ │ │ │ ├── FixMissingWithAnnotationExpected3.kt │ │ │ │ ├── FixMissingWithAnnotationTest.kt │ │ │ │ ├── FixMissingWithAnnotationTest2.kt │ │ │ │ ├── FixMissingWithAnnotationTest3.kt │ │ │ │ ├── FixMissingWithoutImportExpected.kt │ │ │ │ ├── FixMissingWithoutImportTest.kt │ │ │ │ ├── FixPackageRegressionExpected.kt │ │ │ │ └── FixPackageRegressionTest.kt │ │ │ └── some/ │ │ │ ├── FixIncorrectExpected.kt │ │ │ ├── FixIncorrectTest.kt │ │ │ ├── FixMissingExpected.kt │ │ │ └── FixMissingTest.kt │ │ ├── paragraph2/ │ │ │ ├── header/ │ │ │ │ ├── AutoCopyrightApplyPatternExpected.kt │ │ │ │ ├── AutoCopyrightApplyPatternTest.kt │ │ │ │ ├── AutoCopyrightExpected.kt │ │ │ │ ├── AutoCopyrightTest.kt │ │ │ │ ├── CopyrightAbsentInvalidPatternExpected.kt │ │ │ │ ├── CopyrightAbsentInvalidPatternTest.kt │ │ │ │ ├── CopyrightDifferentYearExpected.kt │ │ │ │ ├── CopyrightDifferentYearExpected2.kt │ │ │ │ ├── CopyrightDifferentYearTest.kt │ │ │ │ ├── CopyrightDifferentYearTest2.kt │ │ │ │ ├── CopyrightInvalidPatternValidCodeExpected.kt │ │ │ │ ├── CopyrightInvalidPatternValidCodeTest.kt │ │ │ │ ├── CopyrightShouldNotTriggerNPEExpected.kt │ │ │ │ ├── CopyrightShouldNotTriggerNPETest.kt │ │ │ │ ├── MisplacedHeaderKdocAppendedCopyrightExpected.kt │ │ │ │ ├── MisplacedHeaderKdocAppendedCopyrightTest.kt │ │ │ │ ├── MisplacedHeaderKdocExpected.kt │ │ │ │ ├── MisplacedHeaderKdocNoCopyrightExpected.kt │ │ │ │ ├── MisplacedHeaderKdocNoCopyrightTest.kt │ │ │ │ ├── MisplacedHeaderKdocTest.kt │ │ │ │ ├── MultilineCopyrightExample.kt │ │ │ │ ├── MultilineCopyrightNotTriggerExample.kt │ │ │ │ ├── MultilineCopyrightNotTriggerTest.kt │ │ │ │ ├── MultilineCopyrightTest.kt │ │ │ │ ├── NewlineAfterHeaderKdocExpected.kt │ │ │ │ └── NewlineAfterHeaderKdocTest.kt │ │ │ └── kdoc/ │ │ │ ├── BasicTagsEmptyLineBeforeExpected.kt │ │ │ ├── BasicTagsEmptyLineBeforeTest.kt │ │ │ ├── BasicTagsEmptyLinesExpected.kt │ │ │ ├── BasicTagsEmptyLinesTest.kt │ │ │ ├── ConstructorCommentExpected.kt │ │ │ ├── ConstructorCommentNewlineExpected.kt │ │ │ ├── ConstructorCommentNewlineTest.kt │ │ │ ├── ConstructorCommentNoKDocExpected.kt │ │ │ ├── ConstructorCommentNoKDocTest.kt │ │ │ ├── ConstructorCommentPropertiesExpected.kt │ │ │ ├── ConstructorCommentPropertiesTest.kt │ │ │ ├── ConstructorCommentTest.kt │ │ │ ├── DeprecatedTagExpected.kt │ │ │ ├── DeprecatedTagTest.kt │ │ │ ├── KdocBlockCommentExpected.kt │ │ │ ├── KdocBlockCommentTest.kt │ │ │ ├── KdocCodeBlockFormattingExampleExpected.kt │ │ │ ├── KdocCodeBlockFormattingExampleTest.kt │ │ │ ├── KdocCodeBlocksFormattingExpected.kt │ │ │ ├── KdocCodeBlocksFormattingTest.kt │ │ │ ├── KdocEmptyLineExpected.kt │ │ │ ├── KdocEmptyLineTest.kt │ │ │ ├── KdocFormattingFullExpected.kt │ │ │ ├── KdocFormattingFullTest.kt │ │ │ ├── KdocFormattingOrderExpected.kt │ │ │ ├── KdocFormattingOrderTest.kt │ │ │ ├── NoPackageNoImportExpected.kt │ │ │ ├── NoPackageNoImportTest.kt │ │ │ ├── OrderedTagsAssertionExpected.kt │ │ │ ├── OrderedTagsAssertionTest.kt │ │ │ ├── OrderedTagsExpected.kt │ │ │ ├── OrderedTagsTest.kt │ │ │ ├── SpacesAfterTagExpected.kt │ │ │ ├── SpacesAfterTagTest.kt │ │ │ ├── SpecialTagsInKdocExpected.kt │ │ │ ├── SpecialTagsInKdocTest.kt │ │ │ └── package/ │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── saveourtool/ │ │ │ └── diktat/ │ │ │ └── kdoc/ │ │ │ └── methods/ │ │ │ ├── EmptyKdocExpected.kt │ │ │ ├── EmptyKdocTested.kt │ │ │ ├── KdocMethodsFullExpected.kt │ │ │ ├── KdocMethodsFullTested.kt │ │ │ ├── KdocWithoutThrowsTagExpected.kt │ │ │ ├── KdocWithoutThrowsTagTested.kt │ │ │ ├── MissingKdocExpected.kt │ │ │ ├── MissingKdocOnFunctionExpected.kt │ │ │ ├── MissingKdocOnFunctionTest.kt │ │ │ ├── MissingKdocTested.kt │ │ │ ├── MissingKdocWithModifiersExpected.kt │ │ │ ├── MissingKdocWithModifiersTest.kt │ │ │ ├── ParamTagInsertionExpected.kt │ │ │ ├── ParamTagInsertionTested.kt │ │ │ ├── ReturnTagInsertionExpected.kt │ │ │ ├── ReturnTagInsertionTested.kt │ │ │ ├── ThrowsTagInsertionExpected.kt │ │ │ └── ThrowsTagInsertionTested.kt │ │ ├── paragraph3/ │ │ │ ├── annotations/ │ │ │ │ ├── AnnotationCommentExpected.kt │ │ │ │ ├── AnnotationCommentTest.kt │ │ │ │ ├── AnnotationConstructorSingleLineExpected.kt │ │ │ │ ├── AnnotationConstructorSingleLineTest.kt │ │ │ │ ├── AnnotationSingleLineExpected.kt │ │ │ │ └── AnnotationSingleLineTest.kt │ │ │ ├── blank_lines/ │ │ │ │ ├── CodeBlockWithBlankLinesExpected.kt │ │ │ │ ├── CodeBlockWithBlankLinesTest.kt │ │ │ │ ├── RedundantBlankLinesAtTheEndOfBlockExpected.kt │ │ │ │ ├── RedundantBlankLinesAtTheEndOfBlockTest.kt │ │ │ │ ├── RedundantBlankLinesExpected.kt │ │ │ │ └── RedundantBlankLinesTest.kt │ │ │ ├── block_brace/ │ │ │ │ ├── ClassBracesExpected.kt │ │ │ │ ├── ClassBracesTest.kt │ │ │ │ ├── DoWhileBracesExpected.kt │ │ │ │ ├── DoWhileBracesTest.kt │ │ │ │ ├── IfElseBracesExpected.kt │ │ │ │ ├── IfElseBracesTest.kt │ │ │ │ ├── LoopsBracesExpected.kt │ │ │ │ ├── LoopsBracesTest.kt │ │ │ │ ├── TryBraceExpected.kt │ │ │ │ ├── TryBraceTest.kt │ │ │ │ ├── WhenBranchesExpected.kt │ │ │ │ └── WhenBranchesTest.kt │ │ │ ├── boolean_expressions/ │ │ │ │ ├── BooleanExpressionsExpected.kt │ │ │ │ ├── BooleanExpressionsTest.kt │ │ │ │ ├── DistributiveLawExpected.kt │ │ │ │ ├── DistributiveLawTest.kt │ │ │ │ ├── ExpressionSimplificationExpected.kt │ │ │ │ ├── ExpressionSimplificationTest.kt │ │ │ │ ├── NegativeExpressionExpected.kt │ │ │ │ ├── NegativeExpressionTest.kt │ │ │ │ ├── OrderIssueExpected.kt │ │ │ │ ├── OrderIssueTest.kt │ │ │ │ ├── SameExpressionsInConditionExpected.kt │ │ │ │ ├── SameExpressionsInConditionTest.kt │ │ │ │ ├── SubstitutionIssueExpected.kt │ │ │ │ └── SubstitutionIssueTest.kt │ │ │ ├── braces/ │ │ │ │ ├── DoWhileBracesExpected.kt │ │ │ │ ├── DoWhileBracesTest.kt │ │ │ │ ├── IfElseBraces1Expected.kt │ │ │ │ ├── IfElseBraces1Test.kt │ │ │ │ ├── IfElseBracesInsideScopeFunctionsExpected.kt │ │ │ │ ├── IfElseBracesInsideScopeFunctionsTest.kt │ │ │ │ ├── LoopsBracesExpected.kt │ │ │ │ ├── LoopsBracesInsideScopeFunctionsExpected.kt │ │ │ │ ├── LoopsBracesInsideScopeFunctionsTest.kt │ │ │ │ ├── LoopsBracesTest.kt │ │ │ │ ├── WhenBranchesExpected.kt │ │ │ │ └── WhenBranchesTest.kt │ │ │ ├── collapse_if/ │ │ │ │ ├── CollapseIfStatementsExpected.kt │ │ │ │ └── CollapseIfStatementsTest.kt │ │ │ ├── else_expected/ │ │ │ │ ├── ElseInWhenExpected.kt │ │ │ │ └── ElseInWhenTest.kt │ │ │ ├── empty_block/ │ │ │ │ ├── EmptyBlockExpected.kt │ │ │ │ └── EmptyBlockTest.kt │ │ │ ├── enum_separated/ │ │ │ │ ├── EnumSeparatedExpected.kt │ │ │ │ ├── EnumSeparatedTest.kt │ │ │ │ ├── LastElementCommentExpected.kt │ │ │ │ └── LastElementCommentTest.kt │ │ │ ├── file_structure/ │ │ │ │ ├── BlankLinesBetweenBlocksExpected.kt │ │ │ │ ├── CompanionObjectWithCommentExpected.kt │ │ │ │ ├── CompanionObjectWithCommentTest.kt │ │ │ │ ├── CopyrightCommentPositionExpected.kt │ │ │ │ ├── CopyrightCommentPositionTest.kt │ │ │ │ ├── DeclarationsInClassOrderExpected.kt │ │ │ │ ├── DeclarationsInClassOrderTest.kt │ │ │ │ ├── DefaultPackageWithImportsExpected.kt │ │ │ │ ├── DefaultPackageWithImportsTest.kt │ │ │ │ ├── FileAnnotationExpected.kt │ │ │ │ ├── FileAnnotationTest.kt │ │ │ │ ├── HeaderKdocAfterPackageExpected.kt │ │ │ │ ├── HeaderKdocAfterPackageTest.kt │ │ │ │ ├── LoggerOrderExpected.kt │ │ │ │ ├── LoggerOrderTest.kt │ │ │ │ ├── MissingBlankLinesBetweenBlocksTest.kt │ │ │ │ ├── NoImportNoPackageExpected.kt │ │ │ │ ├── NoImportNoPackageTest.kt │ │ │ │ ├── OrderWithCommentExpected.kt │ │ │ │ ├── OrderWithCommentTest.kt │ │ │ │ ├── OrderWithEnumsExpected.kt │ │ │ │ ├── OrderWithEnumsTest.kt │ │ │ │ ├── OtherCommentsExpected.kt │ │ │ │ ├── OtherCommentsTest.kt │ │ │ │ ├── RedundantBlankLinesBetweenBlocksTest.kt │ │ │ │ ├── ReorderingImportsExpected.kt │ │ │ │ ├── ReorderingImportsRecommendedExpected.kt │ │ │ │ ├── ReorderingImportsRecommendedTest.kt │ │ │ │ ├── ReorderingImportsTest.kt │ │ │ │ ├── ScriptPackageDirectiveExpected.kts │ │ │ │ └── ScriptPackageDirectiveTest.kts │ │ │ ├── indentation/ │ │ │ │ ├── ConstructorExpected.kt │ │ │ │ ├── ConstructorTest.kt │ │ │ │ ├── IndentFullExpected.kt │ │ │ │ ├── IndentFullTest.kt │ │ │ │ ├── IndentationFull1Expected.kt │ │ │ │ ├── IndentationFull1Test.kt │ │ │ │ ├── IndentationParametersExpected.kt │ │ │ │ └── IndentationParametersTest.kt │ │ │ ├── long_line/ │ │ │ │ ├── LongBinaryExpressionExpected.kt │ │ │ │ ├── LongBinaryExpressionLastWordExpected.kt │ │ │ │ ├── LongBinaryExpressionLastWordTest.kt │ │ │ │ ├── LongBinaryExpressionTest.kt │ │ │ │ ├── LongComplexExpressionExpected.kt │ │ │ │ ├── LongComplexExpressionTest.kt │ │ │ │ ├── LongConditionInSmallFunctionExpected.kt │ │ │ │ ├── LongConditionInSmallFunctionTest.kt │ │ │ │ ├── LongDotQualifiedExpressionExpected.kt │ │ │ │ ├── LongDotQualifiedExpressionTest.kt │ │ │ │ ├── LongExpressionInConditionExpected.kt │ │ │ │ ├── LongExpressionInConditionTest.kt │ │ │ │ ├── LongExpressionNoFixExpected.kt │ │ │ │ ├── LongExpressionNoFixTest.kt │ │ │ │ ├── LongInlineCommentsExpected.kt │ │ │ │ ├── LongInlineCommentsTest.kt │ │ │ │ ├── LongLineAnnotationExpected.kt │ │ │ │ ├── LongLineAnnotationTest.kt │ │ │ │ ├── LongLineCommentExpected.kt │ │ │ │ ├── LongLineCommentExpected2.kt │ │ │ │ ├── LongLineCommentTest.kt │ │ │ │ ├── LongLineCommentTest2.kt │ │ │ │ ├── LongLineExpressionExpected.kt │ │ │ │ ├── LongLineExpressionTest.kt │ │ │ │ ├── LongLineFunExpected.kt │ │ │ │ ├── LongLineFunTest.kt │ │ │ │ ├── LongLineRValueExpected.kt │ │ │ │ ├── LongLineRValueTest.kt │ │ │ │ ├── LongShortRValueExpected.kt │ │ │ │ ├── LongShortRValueTest.kt │ │ │ │ ├── LongStringTemplateExpected.kt │ │ │ │ ├── LongStringTemplateTest.kt │ │ │ │ ├── LongValueArgumentsListExpected.kt │ │ │ │ └── LongValueArgumentsListTest.kt │ │ │ ├── long_numbers/ │ │ │ │ ├── LongNumericalValuesExpected.kt │ │ │ │ └── LongNumericalValuesTest.kt │ │ │ ├── multiple_modifiers/ │ │ │ │ ├── AnnotationExpected.kt │ │ │ │ ├── AnnotationTest.kt │ │ │ │ ├── ModifierExpected.kt │ │ │ │ └── ModifierTest.kt │ │ │ ├── newlines/ │ │ │ │ ├── ColonExpected.kt │ │ │ │ ├── ColonTest.kt │ │ │ │ ├── CommaExpected.kt │ │ │ │ ├── CommaTest.kt │ │ │ │ ├── ExpressionBodyExpected.kt │ │ │ │ ├── ExpressionBodyTest.kt │ │ │ │ ├── FunctionalStyleExpected.kt │ │ │ │ ├── FunctionalStyleTest.kt │ │ │ │ ├── LParExpected.kt │ │ │ │ ├── LParTest.kt │ │ │ │ ├── LambdaExpected.kt │ │ │ │ ├── LambdaTest.kt │ │ │ │ ├── ListArgumentLambdaExpected.kt │ │ │ │ ├── ListArgumentLambdaTest.kt │ │ │ │ ├── LongDotQualifiedExpressionExpected.kt │ │ │ │ ├── LongDotQualifiedExpressionTest.kt │ │ │ │ ├── OneLineFunctionExpected.kt │ │ │ │ ├── OneLineFunctionTest.kt │ │ │ │ ├── OperatorsExpected.kt │ │ │ │ ├── OperatorsTest.kt │ │ │ │ ├── ParameterListExpected.kt │ │ │ │ ├── ParameterListTest.kt │ │ │ │ ├── SizeParameterListExpected.kt │ │ │ │ ├── SizeParameterListTest.kt │ │ │ │ ├── SuperClassListOnTheSameLineExpected.kt │ │ │ │ └── SuperClassListOnTheSameLineTest.kt │ │ │ ├── nullable/ │ │ │ │ ├── CollectionExpected.kt │ │ │ │ ├── CollectionTest.kt │ │ │ │ ├── NullPrimitiveExpected.kt │ │ │ │ └── NullPrimitiveTest.kt │ │ │ ├── preview_annotation/ │ │ │ │ ├── PreviewAnnotationMethodNameExpected.kt │ │ │ │ ├── PreviewAnnotationMethodNameTest.kt │ │ │ │ ├── PreviewAnnotationPrivateModifierExpected.kt │ │ │ │ └── PreviewAnnotationPrivateModifierTest.kt │ │ │ ├── range/ │ │ │ │ ├── RangeToExpected.kt │ │ │ │ ├── RangeToTest.kt │ │ │ │ ├── RangeToUntilExpected.kt │ │ │ │ └── RangeToUntilTest.kt │ │ │ ├── semicolons/ │ │ │ │ ├── SemicolonsExpected.kt │ │ │ │ └── SemicolonsTest.kt │ │ │ ├── sort_error/ │ │ │ │ ├── ConstantsExpected.kt │ │ │ │ ├── ConstantsTest.kt │ │ │ │ ├── EnumSortExpected.kt │ │ │ │ └── EnumSortTest.kt │ │ │ ├── spaces/ │ │ │ │ ├── AnnotationExpected.kt │ │ │ │ ├── AnnotationTest.kt │ │ │ │ ├── BinaryOpExpected.kt │ │ │ │ ├── BinaryOpTest.kt │ │ │ │ ├── BracesLambdaSpacesExpected.kt │ │ │ │ ├── BracesLambdaSpacesTest.kt │ │ │ │ ├── EolSpacesExpected.kt │ │ │ │ ├── EolSpacesTest.kt │ │ │ │ ├── EqualsExpected.kt │ │ │ │ ├── EqualsTest.kt │ │ │ │ ├── LBraceAfterKeywordExpected.kt │ │ │ │ ├── LBraceAfterKeywordTest.kt │ │ │ │ ├── LambdaAsArgumentExpected.kt │ │ │ │ ├── LambdaAsArgumentTest.kt │ │ │ │ ├── LbraceExpected.kt │ │ │ │ ├── LbraceTest.kt │ │ │ │ ├── TooManySpacesEnumExpected.kt │ │ │ │ ├── TooManySpacesEnumTest.kt │ │ │ │ ├── TooManySpacesExpected.kt │ │ │ │ ├── TooManySpacesTest.kt │ │ │ │ ├── WhiteSpaceBeforeLBraceExpected.kt │ │ │ │ ├── WhiteSpaceBeforeLBraceTest.kt │ │ │ │ ├── WhiteSpaceBeforeLParExpected.kt │ │ │ │ └── WhiteSpaceBeforeLParTest.kt │ │ │ ├── src/ │ │ │ │ └── main/ │ │ │ │ ├── A/ │ │ │ │ │ ├── FileSize2000.kt │ │ │ │ │ └── FileSizeA.kt │ │ │ │ ├── B/ │ │ │ │ │ └── FileSizeB.kt │ │ │ │ ├── C/ │ │ │ │ │ └── FileSizeC.kt │ │ │ │ └── FileSizeLarger.kt │ │ │ ├── statement/ │ │ │ │ ├── StatementExpected.kt │ │ │ │ └── StatementTest.kt │ │ │ ├── string_concatenation/ │ │ │ │ ├── StringConcatenationExpected.kt │ │ │ │ └── StringConcatenationTest.kt │ │ │ ├── string_template/ │ │ │ │ ├── StringTemplateExpected.kt │ │ │ │ └── StringTemplateTest.kt │ │ │ ├── top_level/ │ │ │ │ ├── TopLevelSortExpected.kt │ │ │ │ ├── TopLevelSortTest.kt │ │ │ │ ├── TopLevelWithCommentExpected.kt │ │ │ │ └── TopLevelWithCommentTest.kt │ │ │ └── trailing_comma/ │ │ │ ├── TrailingCommaExpected.kt │ │ │ └── TrailingCommaTest.kt │ │ ├── paragraph4/ │ │ │ ├── generics/ │ │ │ │ ├── VariableGenericTypeDeclarationExpected.kt │ │ │ │ └── VariableGenericTypeDeclarationTest.kt │ │ │ ├── null_checks/ │ │ │ │ ├── IfConditionAssignCheckExpected.kt │ │ │ │ ├── IfConditionAssignCheckTest.kt │ │ │ │ ├── IfConditionBreakCheckExpected.kt │ │ │ │ ├── IfConditionBreakCheckTest.kt │ │ │ │ ├── IfConditionNullCheckExpected.kt │ │ │ │ ├── IfConditionNullCheckTest.kt │ │ │ │ ├── RequireFunctionExpected.kt │ │ │ │ └── RequireFunctionTest.kt │ │ │ └── smart_cast/ │ │ │ ├── SmartCastExpected.kt │ │ │ └── SmartCastTest.kt │ │ ├── paragraph5/ │ │ │ ├── method_call_names/ │ │ │ │ ├── ReplaceMethodCallNamesExpected.kt │ │ │ │ └── ReplaceMethodCallNamesTest.kt │ │ │ └── nested_functions/ │ │ │ ├── AvoidNestedFunctionsExample.kt │ │ │ ├── AvoidNestedFunctionsNoTriggerExample.kt │ │ │ ├── AvoidNestedFunctionsNoTriggerTest.kt │ │ │ ├── AvoidNestedFunctionsSeveralExample.kt │ │ │ ├── AvoidNestedFunctionsSeveralTest.kt │ │ │ └── AvoidNestedFunctionsTest.kt │ │ └── paragraph6/ │ │ └── useless-override/ │ │ ├── SeveralSuperTypesExpected.kt │ │ ├── SeveralSuperTypesTest.kt │ │ ├── UselessOverrideExpected.kt │ │ └── UselessOverrideTest.kt │ └── test-rules-config.yml ├── diktat-ruleset/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ ├── kotlin/ │ │ └── com/ │ │ └── saveourtool/ │ │ └── diktat/ │ │ └── ruleset/ │ │ └── rules/ │ │ └── DiktatRuleSetProviderV3Spi.kt │ └── resources/ │ └── META-INF/ │ └── services/ │ └── com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3 ├── diktat-runner/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── kotlin/ │ └── com/ │ └── saveourtool/ │ └── diktat/ │ └── DiktatFactories.kt ├── examples/ │ ├── README.md │ ├── gradle-groovy-dsl/ │ │ ├── build.gradle │ │ ├── diktat-analysis.yml │ │ └── src/ │ │ └── main/ │ │ └── kotlin/ │ │ ├── AnotherTest.kt │ │ └── Test.kt │ ├── gradle-kotlin-dsl/ │ │ ├── build.gradle.kts │ │ ├── diktat-analysis.yml │ │ ├── settings.gradle.kts │ │ └── src/ │ │ └── main/ │ │ └── kotlin/ │ │ ├── AnotherTest.kt │ │ └── Test.kt │ ├── gradle-kotlin-dsl-multiproject/ │ │ ├── backend/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── kotlin/ │ │ │ └── Test.kt │ │ ├── build.gradle.kts │ │ ├── diktat-analysis.yml │ │ ├── frontend/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── kotlin/ │ │ │ └── AnotherTest.kt │ │ └── settings.gradle.kts │ ├── maven/ │ │ ├── diktat-analysis.yml │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ └── kotlin/ │ │ ├── AnotherTest.kt │ │ └── Test.kt │ └── maven-multiproject/ │ ├── backend/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ └── kotlin/ │ │ └── Test.kt │ ├── diktat-analysis.yml │ ├── frontend/ │ │ ├── pom.xml │ │ └── src/ │ │ └── main/ │ │ └── kotlin/ │ │ └── AnotherTest.kt │ └── pom.xml ├── gradle/ │ ├── libs.versions.toml │ ├── plugins/ │ │ ├── build.gradle.kts │ │ ├── settings.gradle.kts │ │ └── src/ │ │ └── main/ │ │ └── kotlin/ │ │ ├── Versions.kt │ │ └── com/ │ │ └── saveourtool/ │ │ └── diktat/ │ │ └── buildutils/ │ │ ├── JacocoConfiguration.kt │ │ ├── PublishingConfiguration.kt │ │ ├── TaskNames.kt │ │ ├── VersioningConfiguration.kt │ │ ├── code-quality-convention.gradle.kts │ │ ├── detekt-convention-configuration.gradle.kts │ │ ├── diktat-convention-configuration.gradle.kts │ │ ├── git-hook-configuration.gradle.kts │ │ ├── kotlin-jvm-configuration.gradle.kts │ │ ├── publishing-configuration.gradle.kts │ │ ├── publishing-default-configuration.gradle.kts │ │ └── versioning-configuration.gradle.kts │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── info/ │ ├── README.md │ ├── available-rules.md │ ├── build.gradle.kts │ ├── buildSrc/ │ │ ├── build.gradle.kts │ │ ├── gradle.properties │ │ └── src/ │ │ └── main/ │ │ └── kotlin/ │ │ └── com/ │ │ └── saveourtool/ │ │ └── diktat/ │ │ └── generation/ │ │ └── docs/ │ │ ├── FullDocGenerator.kt │ │ ├── GenerationAvailableRules.kt │ │ ├── GenerationDocs.kt │ │ ├── LatexUtils.kt │ │ └── WarningsTableGenerator.kt │ ├── gradle/ │ │ └── wrapper/ │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ ├── guide/ │ │ ├── diktat-coding-convention.md │ │ ├── guide-TOC.md │ │ ├── guide-chapter-0.md │ │ ├── guide-chapter-1.md │ │ ├── guide-chapter-2.md │ │ ├── guide-chapter-3.md │ │ ├── guide-chapter-4.md │ │ ├── guide-chapter-5.md │ │ ├── guide-chapter-6.md │ │ └── table-of-content.md │ └── rules-mapping.md ├── renovate.json ├── settings.gradle.kts └── wp/ ├── README.md ├── makefile ├── references.bib ├── sections/ │ ├── appendix.tex │ ├── compare.tex │ ├── conclusion.tex │ ├── definition.tex │ ├── diKTat.tex │ ├── download.tex │ ├── feature.tex │ ├── introduction.tex │ ├── kotlin.tex │ └── work.tex └── wp.tex ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # https://editorconfig.org root = true [*] charset = utf-8 indent_size = 4 indent_style = space insert_final_newline = true max_line_length = 180 tab_width = 4 trim_trailing_whitespace = true ij_continuation_indent_size = 8 ij_formatter_off_tag = @formatter:off ij_formatter_on_tag = @formatter:on ij_formatter_tags_enabled = true ij_smart_tabs = false ij_visual_guides = 80,120,180 ij_wrap_on_typing = false [{*.yaml,*.yml}] indent_size = 2 [{*.kt,*.kts}] ij_kotlin_align_in_columns_case_branch = false ij_kotlin_align_multiline_binary_operation = false ij_kotlin_align_multiline_extends_list = false ij_kotlin_align_multiline_method_parentheses = false ij_kotlin_align_multiline_parameters = true ij_kotlin_align_multiline_parameters_in_calls = false ij_kotlin_allow_trailing_comma = false ij_kotlin_allow_trailing_comma_on_call_site = false ij_kotlin_assignment_wrap = normal ij_kotlin_blank_lines_after_class_header = 0 ij_kotlin_blank_lines_around_block_when_branches = 0 ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1 ij_kotlin_block_comment_add_space = false ij_kotlin_block_comment_at_first_column = true ij_kotlin_call_parameters_new_line_after_left_paren = true ij_kotlin_call_parameters_right_paren_on_new_line = true ij_kotlin_call_parameters_wrap = on_every_item ij_kotlin_catch_on_new_line = false ij_kotlin_class_annotation_wrap = split_into_lines ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL ij_kotlin_continuation_indent_for_chained_calls = false ij_kotlin_continuation_indent_for_expression_bodies = false ij_kotlin_continuation_indent_in_argument_lists = false ij_kotlin_continuation_indent_in_elvis = false ij_kotlin_continuation_indent_in_if_conditions = false ij_kotlin_continuation_indent_in_parameter_lists = false ij_kotlin_continuation_indent_in_supertype_lists = false ij_kotlin_else_on_new_line = false ij_kotlin_enum_constants_wrap = off ij_kotlin_extends_list_wrap = normal ij_kotlin_field_annotation_wrap = split_into_lines ij_kotlin_finally_on_new_line = false ij_kotlin_if_rparen_on_new_line = true ij_kotlin_import_nested_classes = false ij_kotlin_imports_layout = android.**,androidx.**,com.android.**,com.saveourtool.diktat.**,*,java.**,javax.**,kotlin.**,kotlinx.**,^ ij_kotlin_insert_whitespaces_in_simple_one_line_method = true ij_kotlin_keep_blank_lines_before_right_brace = 2 ij_kotlin_keep_blank_lines_in_code = 2 ij_kotlin_keep_blank_lines_in_declarations = 2 ij_kotlin_keep_first_column_comment = true ij_kotlin_keep_indents_on_empty_lines = false ij_kotlin_keep_line_breaks = true ij_kotlin_lbrace_on_next_line = false ij_kotlin_line_comment_add_space = false ij_kotlin_line_comment_add_space_on_reformat = false ij_kotlin_line_comment_at_first_column = true ij_kotlin_method_annotation_wrap = split_into_lines ij_kotlin_method_call_chain_wrap = normal ij_kotlin_method_parameters_new_line_after_left_paren = true ij_kotlin_method_parameters_right_paren_on_new_line = true ij_kotlin_method_parameters_wrap = on_every_item ij_kotlin_name_count_to_use_star_import = 2147483647 ij_kotlin_name_count_to_use_star_import_for_members = 2147483647 ij_kotlin_parameter_annotation_wrap = off ij_kotlin_space_after_comma = true ij_kotlin_space_after_extend_colon = true ij_kotlin_space_after_type_colon = true ij_kotlin_space_before_catch_parentheses = true ij_kotlin_space_before_comma = false ij_kotlin_space_before_extend_colon = true ij_kotlin_space_before_for_parentheses = true ij_kotlin_space_before_if_parentheses = true ij_kotlin_space_before_lambda_arrow = true ij_kotlin_space_before_type_colon = false ij_kotlin_space_before_when_parentheses = true ij_kotlin_space_before_while_parentheses = true ij_kotlin_spaces_around_additive_operators = true ij_kotlin_spaces_around_assignment_operators = true ij_kotlin_spaces_around_equality_operators = true ij_kotlin_spaces_around_function_type_arrow = true ij_kotlin_spaces_around_logical_operators = true ij_kotlin_spaces_around_multiplicative_operators = true ij_kotlin_spaces_around_range = false ij_kotlin_spaces_around_relational_operators = true ij_kotlin_spaces_around_unary_operator = false ij_kotlin_spaces_around_when_arrow = true ij_kotlin_variable_annotation_wrap = off ij_kotlin_while_on_new_line = false ij_kotlin_wrap_elvis_expressions = 1 ij_kotlin_wrap_expression_body_functions = 1 ij_kotlin_wrap_first_method_in_call_chain = false # disable ktlint rules ktlint_standard = disabled ktlint_experimental = disabled ktlint_test = disabled ktlint_custom = disabled ================================================ FILE: .git-hooks/commit-msg.sh ================================================ #!/usr/bin/env bash commit_pattern="(Merge (remote-tracking )?branch|### What's done:)" error_msg="Your commit message doesn't match the pattern $commit_pattern. Please fix it." commit_count="$(git rev-list --count master..HEAD 2> /dev/null)" if [[ $commit_count = "0" && ! $( cat "$1" ) =~ $commit_pattern ]] then echo "$error_msg" exit 1 fi exit 0 ================================================ FILE: .git-hooks/pre-commit.sh ================================================ #!/usr/bin/env bash branch_name="$(git rev-parse --abbrev-ref HEAD)" branch_pattern="^(feature|bugfix|hotfix|infra)/.*$" error_message="Your branch name doesn't match the pattern $branch_pattern. Please correct it before committing." if [[ ! $branch_name =~ $branch_pattern ]] then echo "$error_message" exit 1 fi exit 0 ================================================ FILE: .gitattributes ================================================ * text=auto *.bat eol=crlf ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: 'bug' --- ## Describe the bug ## Expected behavior ## Observed behavior ## Steps to Reproduce ## Environment information * diktat version: * build tool (maven/gradle): * how is diktat run (CLI, plugin, etc.): * kotlin version: * operating system: * link to a project (if your project is public): ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: true contact_links: - name: Discussions url: https://github.com/saveourtool/diktat/discussions about: Ask everything about diktat in discussions tab ================================================ FILE: .github/codecov.yml ================================================ codecov: max_report_age: off coverage: status: project: default: threshold: 2% ================================================ FILE: .github/pull_request_template.md ================================================ ## Which rule and warnings did you add? This pull request closes ## Actions checklist * [ ] Implemented Rule, added Warnings * [ ] Added tests on checks * [ ] Added tests on fixers * [ ] Updated diktat-analysis.yml * [ ] Updated available-rules.md ## Fixme ================================================ FILE: .github/workflows/build_and_test.yml ================================================ # vim:ai et ts=2 sts=2 sw=2: name: Build and test on: pull_request: push: branches: - 'master' concurrency: # https://docs.github.com/en/actions/using-jobs/using-concurrency # The latest queued workflow is preferred; the ones already in progress get cancelled # Workflows on master branch shouldn't be cancelled, that's why they are identified by commit SHA group: ${{ github.ref == 'refs/heads/master' && format('{0}-{1}', github.workflow, github.sha) || format('{0}-{1}', github.workflow, github.ref) }} cancel-in-progress: true env: GRADLE_OPTS: -Dorg.gradle.daemon=true -Dorg.gradle.parallel=true -Dorg.gradle.welcome=never GPG_SEC: ${{ secrets.PGP_SEC }} GPG_PASSWORD: ${{ secrets.PGP_PASSWORD }} OSSRH_USERNAME: ${{ secrets.SONATYPE_USER }} OSSRH_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} jobs: build_and_test_with_code_coverage: name: Build, test and upload code coverage runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: # required for correct codecov upload fetch-depth: 0 - name: Set up JDK 11 uses: actions/setup-java@v4 with: java-version: 11 distribution: temurin - name: Retrieve Kotlin version run: | kv=$(cat gradle/libs.versions.toml | grep '^kotlin =' | awk -F'[=]' '{print $2}' | tr -d '" ') echo KOTLIN_VERSION=$kv >> $GITHUB_ENV shell: bash - name: Cache konan uses: actions/cache@v4 with: path: ~/.konan key: ${{ runner.os }}-gradle-konan-${{ env.KOTLIN_VERSION }} - name: Build all uses: gradle/gradle-build-action@v3 with: gradle-version: wrapper gradle-home-cache-cleanup: true arguments: | build -x detekt --scan --build-cache - name: Upload gradle reports if: ${{ always() }} uses: actions/upload-artifact@v4 with: name: gradle-reports path: '**/build/reports/' retention-days: 1 - name: Code coverage report uses: codecov/codecov-action@v5 # Do not run coverage for forks since they cannot upload the results to codecov. # For reference, see: # https://docs.github.com/en/actions/using-jobs/using-conditions-to-control-job-execution#example-only-run-job-for-specific-repository if: github.repository == 'saveourtool/diktat' with: fail_ci_if_error: false token: ${{ secrets.CODECOV_TOKEN }} build_and_test: name: Build and test runs-on: ${{ matrix.os }} strategy: # We need multiple builds to run even if the 1st one is failing, because # test failures may be OS-specific (or the tests themselves flaky). fail-fast: false matrix: os: [ ubuntu-latest, windows-latest, macos-latest ] # A possible workaround for . permissions: checks: write contents: write pull-requests: write statuses: write packages: write steps: - uses: actions/checkout@v4 - name: Set up JDK 11 uses: actions/setup-java@v4 with: java-version: 11 distribution: temurin - name: Retrieve Kotlin version run: | kv=$(cat gradle/libs.versions.toml | grep '^kotlin =' | awk -F'[=]' '{print $2}' | tr -d '" ') echo KOTLIN_VERSION=$kv >> $GITHUB_ENV shell: bash - name: Cache konan uses: actions/cache@v4 with: path: ~/.konan key: ${{ runner.os }}-gradle-konan-${{ env.KOTLIN_VERSION }} - name: Build all uses: gradle/gradle-build-action@v3 with: gradle-version: wrapper gradle-home-cache-cleanup: true arguments: | build -x detekt --scan --build-cache # This step needs a Git repository, so it's impossible to extract it # into a separate job (or, otherwise, we'd need to upload the content # of the whole `.git` folder as an artifact). - name: JUnit Tests (dorny/test-reporter@v1) uses: dorny/test-reporter@v1 if: ${{ always() }} with: name: JUnit Tests (${{ runner.os }}, dorny/test-reporter@v1) # Comma-separated values. path: "**/build/test-results/*/TEST-*.xml" reporter: java-junit # Ignore the "Resource not accessible by integration" error when a PR # originates from a non-collaborator. This is # which may be # potentially fixed with . continue-on-error: true - name: Upload test results uses: actions/upload-artifact@v4 if: ${{ always() }} with: name: xml-test-reports-${{ runner.os }} path: | **/build/test-results/*/TEST-*.xml retention-days: 1 - name: Upload gradle reports if: ${{ always() }} uses: actions/upload-artifact@v4 with: name: gradle-test-report-${{ matrix.os }} path: '**/build/reports/' retention-days: 1 - name: 'Publish a snapshot to GitHub' id: publish-github if: ${{ github.event_name == 'push' && github.ref_type == 'branch' && github.ref == 'refs/heads/master' && github.repository == 'saveourtool/diktat' }} uses: gradle/gradle-build-action@v3 with: gradle-version: wrapper arguments: | -Preckon.stage=snapshot publishAllPublicationsToGitHubRepository - name: 'Publish a snapshot to Maven Central' id: publish-sonatype if: ${{ github.event_name == 'push' && github.ref_type == 'branch' && github.ref == 'refs/heads/master' && github.repository == 'saveourtool/diktat' }} uses: gradle/gradle-build-action@v3 with: gradle-version: wrapper arguments: | -Preckon.stage=snapshot publishToSonatype closeAndReleaseSonatypeStagingRepository report: name: Publish JUnit test results if: ${{ always() }} needs: build_and_test runs-on: ${{ matrix.os }} strategy: matrix: os: [ windows-latest, macos-latest ] permissions: checks: write pull-requests: write steps: - uses: actions/download-artifact@v4 if: ${{ always() }} with: name: xml-test-reports-${{ runner.os }} # Uses Docker, that's why Linux-only. - name: JUnit Tests (EnricoMi/publish-unit-test-result-action@v2, Linux) uses: EnricoMi/publish-unit-test-result-action@v2 if: ${{ runner.os == 'Linux' }} with: check_name: JUnit Tests (${{ runner.os }}, EnricoMi/publish-unit-test-result-action@v2) junit_files: | **/build/test-results/*/TEST-*.xml - name: JUnit Tests (EnricoMi/publish-unit-test-result-action@v2, Windows or Mac OS X) uses: EnricoMi/publish-unit-test-result-action/composite@v2 if: ${{ runner.os == 'Windows' || runner.os == 'macOS' }} with: check_name: JUnit Tests (${{ runner.os }}, EnricoMi/publish-unit-test-result-action@v2) junit_files: | **/build/test-results/*/TEST-*.xml ================================================ FILE: .github/workflows/codeql-analysis.yml ================================================ # For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. name: "CodeQL" on: push: branches: [master] pull_request: # The branches below must be a subset of the branches above branches: [master] schedule: - cron: '0 20 * * 5' jobs: analyze: name: Analyze runs-on: ubuntu-latest strategy: fail-fast: false matrix: # Override automatic language detection by changing the below list # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] language: ['kotlin'] # Learn more... # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection steps: - name: Checkout repository uses: actions/checkout@v4 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. fetch-depth: 2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) # - name: Autobuild # uses: github/codeql-action/autobuild@v2 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines # and modify them (or add more) to build your code if your project # uses a compiled language - uses: gradle/gradle-build-action@v3 with: gradle-version: wrapper # The `--continue` flag is necessary so that Gradle keeps going after the 1st test failure. # By default, when test for all MPP targets are executed, Kotlin Gradle Plugin generates a single aggregated HTML report. # Property `kotlin.tests.individualTaskReports` enables individual Junit-style XML reports. # See org.jetbrains.kotlin.gradle.testing.internal.KotlinTestReport. arguments: | build --continue -x detekt -Pkotlin.tests.individualTaskReports=true -Porg.gradle.caching=true -Pdetekt.multiplatform.disabled=true -PdisableRedundantTargets=true -PenabledExecutables=debug -PgprUser=${{ github.actor }} -PgprKey=${{ secrets.GITHUB_TOKEN }} - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 ================================================ FILE: .github/workflows/dependencies.yml ================================================ name: 'Dependencies' on: push: branches: - 'master' env: GRADLE_OPTS: -Dorg.gradle.daemon=true -Dorg.gradle.parallel=true -Dorg.gradle.welcome=never jobs: dependencies: name: 'Dependencies' runs-on: ubuntu-latest # The Dependency Submission API requires write permission. permissions: contents: write steps: - uses: actions/checkout@v4 with: # Fetch Git tags, so that semantic version can be calculated. # Alternatively, run `git fetch --prune --unshallow --tags` as the # next step, see # https://github.com/actions/checkout/issues/206#issuecomment-607496604. fetch-depth: 0 - name: 'Set up Java 11' uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: 11 - name: 'Run snapshot action' uses: mikepenz/gradle-dependency-submission@v1.0.0 with: use-gradlew: true gradle-build-module: |- :diktat-api :diktat-common-test :diktat-ktlint-engine :diktat-gradle-plugin :diktat-maven-plugin :diktat-rules :diktat-ruleset :diktat-runner :diktat-dev-ksp :diktat-cli gradle-build-configuration: |- compileClasspath sub-module-mode: 'INDIVIDUAL' ================================================ FILE: .github/workflows/detekt.yml ================================================ name: Run deteKT on: push: branches: [ master ] pull_request: jobs: detekt_check: runs-on: ubuntu-latest permissions: # required for all workflows security-events: write steps: - uses: actions/checkout@v4 - name: Set up JDK 11 uses: actions/setup-java@v4 with: java-version: 11 distribution: temurin - uses: gradle/gradle-build-action@v3 with: gradle-version: wrapper arguments: | detektAll --build-cache --continue -PgprUser=${{ github.actor }} -PgprKey=${{ secrets.GITHUB_TOKEN }} - name: Upload SARIF report to Github uses: github/codeql-action/upload-sarif@v3 if: ${{ always() }} with: sarif_file: build/detekt-sarif-reports/detekt-merged.sarif - name: Upload SARIF artifacts uses: actions/upload-artifact@v4 if: ${{ failure() }} with: name: sarif-reports path: "**/build/detekt-sarif-reports/" retention-days: 1 ================================================ FILE: .github/workflows/diktat.yml ================================================ name: Run diKTat (release) on: push: branches: [ master ] pull_request: branches: [ master ] jobs: diktat_check: runs-on: ubuntu-latest permissions: # required for all workflows security-events: write steps: - uses: actions/checkout@v4 - name: Set up JDK 11 uses: actions/setup-java@v4 with: java-version: 11 distribution: temurin - uses: gradle/gradle-build-action@v3 with: gradle-version: wrapper arguments: | diktatCheck mergeDiktatReports -Pdiktat.githubActions=true -Pdetekt.multiplatform.disabled=true --continue --build-cache -PgprUser=${{ github.actor }} -PgprKey=${{ secrets.GITHUB_TOKEN }} --stacktrace - name: Upload SARIF report to Github uses: github/codeql-action/upload-sarif@v3 if: ${{ always() }} with: sarif_file: build/reports/diktat/diktat-merged.sarif - name: Upload SARIF artifacts uses: actions/upload-artifact@v4 if: ${{ failure() }} with: name: sarif-reports path: "**/build/reports/diktat" retention-days: 1 ================================================ FILE: .github/workflows/diktat_snapshot.yml ================================================ name: Run diKTat (snapshot) on: push: branches: [ master ] pull_request: branches: [ master ] jobs: diktat_snapshot_check: name: 'Check the project using diktat snapshot plugin' runs-on: ubuntu-latest permissions: # required for all workflows security-events: write steps: - uses: actions/checkout@v4 with: # Fetch Git tags, so that semantic version can be calculated. # Alternatively, run `git fetch --prune --unshallow --tags` as the # next step, see # https://github.com/actions/checkout/issues/206#issuecomment-607496604. fetch-depth: 0 - name: Set up JDK 11 uses: actions/setup-java@v4 with: java-version: 11 distribution: temurin - name: 'Cache ~/.konan' id: cache-konan uses: actions/cache@v4 with: path: | ~/.konan key: ${{ runner.os }}-konan-${{ hashFiles('**/*.gradle.kts', '**/gradle-wrapper.properties') }}-build-java${{ matrix.java-version }} restore-keys: | ${{ runner.os }}-konan-${{ hashFiles('**/*.gradle.kts', '**/gradle-wrapper.properties') }}- ${{ runner.os }}-konan- - name: 'Publish a snapshot version to local repo' id: generateLibsForDiktatSnapshot uses: gradle/gradle-build-action@v3 with: gradle-version: wrapper arguments: | :generateLibsForDiktatSnapshot -x detekt -x test -x diktatCheck # copied from .github/workflows/diktat.yml - uses: gradle/gradle-build-action@v3 with: gradle-version: wrapper arguments: | diktatCheck mergeDiktatReports -Pdiktat.githubActions=true -Pdetekt.multiplatform.disabled=true --continue --build-cache -PgprUser=${{ github.actor }} -PgprKey=${{ secrets.GITHUB_TOKEN }} - name: Upload SARIF report to Github uses: github/codeql-action/upload-sarif@v3 if: ${{ always() }} with: sarif_file: build/reports/diktat/diktat-merged.sarif # override category to have a different with release version category: diktat (snapshot) - name: Upload SARIF artifacts uses: actions/upload-artifact@v4 if: ${{ failure() }} with: name: sarif-reports path: "**/build/reports/diktat" retention-days: 1 ================================================ FILE: .github/workflows/release.yml ================================================ name: Create diKTat release on: push: tags: - 'v*' env: GRADLE_OPTS: -Dorg.gradle.daemon=true -Dorg.gradle.parallel=true -Dorg.gradle.welcome=never GPG_SEC: ${{ secrets.PGP_SEC }} GPG_PASSWORD: ${{ secrets.PGP_PASSWORD }} OSSRH_USERNAME: ${{ secrets.SONATYPE_USER }} OSSRH_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} jobs: release: name: 'Release' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: # Fetch Git tags, so that semantic version can be calculated. # Alternatively, run `git fetch --prune --unshallow --tags` as the # next step, see # https://github.com/actions/checkout/issues/206#issuecomment-607496604. fetch-depth: 0 - name: 'Set up Java' uses: actions/setup-java@v4 with: java-version: 11 distribution: temurin - name: 'Calculate the release version' run: | echo "RELEASE_VERSION=${GITHUB_REF#'refs/tags/v'}" >> $GITHUB_ENV - name: 'Publish a release to Maven Central' id: publish-sonatype uses: gradle/gradle-build-action@v3 with: gradle-version: wrapper arguments: | publishToSonatype closeAndReleaseSonatypeStagingRepository - name: 'Publish a release to Gradle Plugins' id: publish-gradle uses: gradle/gradle-build-action@v3 with: gradle-version: wrapper arguments: | :diktat-gradle-plugin:publishPlugins -Pgradle.publish.key=${{ secrets.GRADLE_KEY }} -Pgradle.publish.secret=${{ secrets.GRADLE_SECRET }} - name: 'Publish a release to GitHub' id: publish-github uses: gradle/gradle-build-action@v3 with: gradle-version: wrapper arguments: | shadowExecutableJar publishAllPublicationsToGitHubRepository - name: 'GitHub Release' id: create_release uses: actions/create-release@v1 with: tag_name: ${{ github.ref }} release_name: Release ${{ env.RELEASE_VERSION }} draft: false prerelease: false - name: Upload Diktat CLI to GitHub release id: upload-release-asset-cli uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} asset_path: ./diktat-cli/build/diktat-cli-${{ env.RELEASE_VERSION }} asset_name: diktat asset_content_type: application/zip - name: Upload Diktat CLI for Windows to GitHub release id: upload-release-asset-cli-win uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} asset_path: ./diktat-cli/src/main/script/diktat.cmd asset_name: diktat.cmd asset_content_type: application/octet-stream - name: Upload Diktat ruleset for KtLint to GitHub release id: upload-release-asset-ruleset uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} asset_path: ./diktat-ruleset/build/libs/diktat-${{ env.RELEASE_VERSION }}.jar asset_name: diktat-${{ env.RELEASE_VERSION }}.jar asset_content_type: application/zip ================================================ FILE: .gitignore ================================================ target/ .gradle/ build/ !ktlint/src/main/resources/config/.idea .idea/ *.iml /examples/*/gradlew /examples/*/gradlew.bat /examples/*/gradle/wrapper/ out/ .DS_Store # Vim swap files *.swp *.sarif # a generated file for github action /gradle/libs.versions.toml_backup ================================================ FILE: CNAME ================================================ diktat.saveourtool.com ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Code of Conduct This is a small code of conduct to have a good and friendly development in diKTat ## Good behavior We expect everyone to love each other, be kind and polite. Do not hate anyone for comments on the review or for bugs that he (or she) made in his code. ## Unacceptable behavior But if you would like to bully some of our contributors - you are always welcome. We love critics and do not like to be in comfort zone. We can even organize some underground boxing match for you somewhere in Russia. ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing If you are reading this - then you have decided to contribute to our project. Oh, poor you... Rules are very simple: 1. Fork this repository to your own account 2. Make your changes and verify that tests pass (or wait that our CI/CD will do everything for you) 3. Commit your work and push to a new branch on your fork 4. Submit a pull request 5. Participate in the code review process by responding to feedback # Technical part Main components are: 1) diktat-rules — number of rules that are supported by diKTat; 2) diktat-common-test — util methods for functional/unit tests that can be used for running your code fixer on the initial code and compare it with the expected result; 3) also see our demo: diktat-demo in a separate repository. Mainly we wanted to create a common configurable mechanism that will give us a chance to enable/disable and customize all rules. That's why we added logic for: 1) Parsing `.yml` file with configurations of rules and passing it to visitors; 2) Passing information about properties to visitors. This information is very useful, when you are trying to get, for example, a filename of file where the code is stored; 3) We added a bunch of visitors, checkers and fixers that will extended KTlint functionaliity with code style rules; 4) We have proposed a code style for Kotlin language. Before you make a pull request, make sure the build is clean as we have lot of tests and other prechecks: ```bash $ mvn clean install ``` # Hooks We have some hooks to a commit messages: 1) your commit message should have the following format: ``` Brief Description ### What's done: 1) Long description 2) Long description ``` 2) Please also do not forget to update documentation on Wiki after the merge approval and before merge. ================================================ FILE: LICENSE ================================================ The MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ ![Build and test](https://github.com/saveourtool/diKTat/workflows/Build%20and%20test/badge.svg?branch=master) ![deteKT static analysis](https://github.com/saveourtool/diKTat/workflows/Run%20deteKT/badge.svg) ![diKTat code style](https://github.com/saveourtool/diKTat/workflows/Run%20diKTat%20%28release%29/badge.svg?branch=master) [![codecov](https://codecov.io/gh/saveourtool/diKTat/branch/master/graph/badge.svg)](https://codecov.io/gh/saveourtool/diKTat) [![Releases](https://img.shields.io/github/v/release/saveourtool/diKTat)](https://github.com/saveourtool/diKTat/releases) [![Maven Central](https://img.shields.io/maven-central/v/com.saveourtool.diktat/diktat-rules)](https://mvnrepository.com/artifact/com.saveourtool.diktat) [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fsaveourtool%2Fdiktat.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fsaveourtool%2Fdiktat?ref=badge_shield) [![Chat on Telegram](https://img.shields.io/badge/Chat%20on-Telegram-brightgreen.svg)](https://t.me/diktat_support) [![Hits-of-Code](https://hitsofcode.com/github/saveourtool/diktat)](https://hitsofcode.com/view/github/saveourtool/diktat) ![Lines of code](https://img.shields.io/tokei/lines/github/saveourtool/diktat) ![GitHub repo size](https://img.shields.io/github/repo-size/saveourtool/diktat) [![Awesome Kotlin Badge](https://kotlin.link/awesome-kotlin.svg)](https://github.com/KotlinBy/awesome-kotlin) **DiKTat** is a strict [coding standard](info/guide/diktat-coding-convention.md) for Kotlin, consisting of a collection of [Kotlin](https://kotlinlang.org/) code style rules implemented as Abstract Syntax Tree (AST) visitors built on top of [KTlint](https://ktlint.github.io/). It serves the purpose of detecting and automatically fixing code smells in the Continuous Integration/Continuous Deployment (CI/CD) process. You can find the comprehensive list of supported rules and inspections [here](info/available-rules.md). DiKTat has gained recognition and has been added to the lists of [static analysis tools](https://github.com/analysis-tools-dev/static-analysis), [kotlin-awesome](https://github.com/KotlinBy/awesome-kotlin), and [kompar](https://catalog.kompar.tools/Analyzer/diKTat/1.2.5). We extend our gratitude to the community for this support! ## See first | | | | | | | | --- | --- | --- | --- | --- | --- | |[Codestyle](info/guide/diktat-coding-convention.md)|[Inspections](info/available-rules.md) | [Examples](examples) | [Demo](https://saveourtool.com/#/demo/diktat) | [White Paper](wp/wp.pdf) | [Groups of Inspections](info/rules-mapping.md) | ## Why Choose DiKTat for CI/CD? While there are other tools like `detekt` and `ktlint` performing static analysis, you might wonder why DiKTat is necessary. Here are the key reasons: 1. **More Inspections:** DiKTat boasts over 100 inspections tightly coupled with its [Codestyle](info/guide/diktat-coding-convention.md). 2. **Unique Inspections:** DiKTat introduces unique inspections not found in other linters. 3. **Highly Configurable:** Every inspection is highly configurable, allowing customization and suppression. Check [configuration options](#config) and [suppression](#suppress). 4. **Strict Codestyle:** DiKTat enforces a detailed [Codestyle](info/guide/diktat-coding-convention.md) that can be adopted and applied in your project. ## Run as [CLI-application](diktat-cli/README.md) ### Download binary 1. Download diKTat manually: [here](https://github.com/saveourtool/diktat/releases) **OR** use `curl`: ```shell curl -sSLO https://github.com/saveourtool/diktat/releases/download/v2.0.0/diktat && chmod a+x diktat ``` _**For Windows only**_. Download diKTat.cmd manually: [here](https://github.com/saveourtool/diktat/releases) ### Run diKTat Finally, run KTlint (with diKTat injected) to check your '*.kt' files in 'dir/your/dir': ```console $ ./diktat "dir/your/dir/**/*.kt" ``` > _**On Windows**_ > ```console > diktat.bat "dir/your/dir/**/*.kt" > ``` To **autofix** all code style violations, use `--mode fix` option. ## Run with Maven using diktat-maven-plugin You can see how it is configured in our examples: - [Single project](examples/maven/pom.xml) - [Multi-module project](examples/maven-multiproject/pom.xml)
Add this plugin to your pom.xml: ```xml com.saveourtool.diktat diktat-maven-plugin ${diktat.version} diktat none check fix ${project.basedir}/src/main/kotlin ${project.basedir}/src/test/kotlin diktat-analysis.yml ${project.basedir}/src/test/kotlin/excluded ```
To run diktat in **only-check** mode use command `$ mvn diktat:check@diktat`. To run diktat in **autocorrect** mode use command `$ mvn diktat:fix@diktat`. Requesting a specific _Maven_ `executionId` on the command line (the trailing `diktat` in the above example) may be essential in these cases: * In your `pom.xml`, you have multiple executions with different configurations (e. g.: multiple rule sets):
```xml diktat-basic diktat-analysis.yml diktat-advanced diktat-analysis-advanced.yml ```
* Your YAML file with DiKTat rules has a non-default name and/or resides in a non-default location:
```xml diktat /non/default/rule-set-file.yml ```
* You can omit the `diktatConfigFile` or if it points to non-existed file then DiKTat runs with default configuration. If you omit the `executionId`: ```console $ mvn diktat:check ``` — the plug-in will use the default configuration and search for `diktat-analysis.yml` file in the project directory (you can still customize the rule sets by editing the YAML file). ## Run with Gradle using diktat-gradle-plugin Requires a gradle version no lower than 7.0 You can see how the plugin is configured in our examples: - [Kotlin DSL](examples/gradle-kotlin-dsl/build.gradle.kts) - [Kotlin DSL for multi-module project](examples/gradle-kotlin-dsl-multiproject/build.gradle.kts) - [Groovy DSL](examples/gradle-groovy-dsl/build.gradle)
Add this plugin to your `build.gradle.kts`: ```kotlin plugins { id("com.saveourtool.diktat") version "2.0.0" } ``` > _**Note**_ If you want to apply the plugin to multi-module projects" > ```kotlin > import com.saveourtool.diktat.plugin.gradle.DiktatGradlePlugin > > plugins { > id("com.saveourtool.diktat") version "2.0.0" apply false > } > > allprojects { > apply() > } > ``` You can then configure diktat using `diktat` extension: ```kotlin diktat { inputs { include("src/**/*.kt") // path matching this pattern (per PatternFilterable) that will be checked by diktat exclude("src/test/kotlin/excluded/**") // path matching this pattern will not be checked by diktat } debug = true // turn on debug logging } ``` Also in `diktat` extension you can configure different reporters and their output. You can specify `json`, `html`, `sarif`, `plain` (default). If `output` is set, it should be a file path. If not set, results will be printed to stdout. You can specify multiple reporters. If no reporter is specified, `plain` will be used with `stdout` as output. ```kotlin diktat { reporters { plain() json() html { output = file("someFile.html") } // checkstyle() // sarif() // gitHubActions() } } ```
You can run diktat checks using task `./gradlew diktatCheck` and automatically fix errors with task `./gradlew diktatFix`. ## Run with Spotless [Spotless](https://github.com/diffplug/spotless) is a linter aggregator. ### Gradle Diktat can be run via spotless-gradle-plugin since version 5.10.0
Add this plugin to your build.gradle.kts ```kotlin plugins { id("com.diffplug.spotless") version "5.10.0" } spotless { kotlin { diktat() } kotlinGradle { diktat() } } ```
You can provide a version and configuration path manually as configFile. ```kotlin spotless { kotlin { diktat("2.0.0").configFile("full/path/to/diktat-analysis.yml") } } ```
### Maven Diktat can be run via spotless-maven-plugin since version 2.8.0
Add this plugin to your pom.xml ```xml com.diffplug.spotless spotless-maven-plugin ${spotless.version} ```
You can provide a version and configuration path manually as configFile ```xml 2.0.0 full/path/to/diktat-analysis.yml ```
## GitHub Integration We suggest everyone to use common ["sarif"](https://docs.oasis-open.org/sarif/sarif/v2.0/sarif-v2.0.html) format as a `reporter` in CI/CD. GitHub has an [integration](https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning) with SARIF format and provides you a native reporting of diktat issues in Pull Requests. ![img.png](example.png)
Github Integration 1) Add the following configuration to your project's setup for GitHub Actions: Gradle Plugin: ```text githubActions = true ``` Maven Plugin (pom.xml): ```xml true ``` Maven Plugin (cli options): ```text mvn -B diktat:check@diktat -Ddiktat.githubActions=true ``` 2) Add the following code to your GitHub Action to upload diktat SARIF report (after it was generated): ```yml - name: Upload SARIF to Github using the upload-sarif action uses: github/codeql-action/upload-sarif@v1 if: ${{ always() }} with: sarif_file: ${{ github.workspace }} ``` *Note*: `codeql-action/upload-sarif` limits the number of uploaded files at 15. If your project has more than 15 subprojects, the limit will be exceeded and the step will fail. To solve this issue one can merge SARIF reports. `diktat-gradle-plugin` provides this capability with `mergeDiktatReports` task. This task aggregates reports of all diktat tasks of all Gradle project, which produce SARIF reports, and outputs the merged report into root project's build directory. Then this single file can be used as an input for GitHub action: ```yaml with: sarif_file: build/reports/diktat/diktat-merged.sarif ```
## Customizations via `diktat-analysis.yml` In Diktat we have supported `diktat-analysis.yml` that can be easily changed and help in customization of your own rule set. It has simple fields: `name` — name of the rule, `enabled` (true/false) — to enable or disable that rule (all rules are enabled by the default), `configuration` — a simple map of some extra unique configurations for this particular rule. For example: ```yaml - name: HEADER_MISSING_OR_WRONG_COPYRIGHT # all rules are enabled by the default. To disable add 'enabled: false' to the config. enabled: true configuration: isCopyrightMandatory: true copyrightText: Copyright (c) Jeff Lebowski, 2012-2020. All rights reserved. ``` Note, that you can specify and put `diktat-analysis.yml` that contains configuration of diktat in the parent directory of your project on the same level where `build.gradle/pom.xml` is stored. \ See default configuration in [diktat-analysis.yml](diktat-rules/src/main/resources/diktat-analysis.yml) \ Also see [the list of all rules supported by diKTat](info/available-rules.md). ## Suppress warnings/inspections
Suppress warnings on individual code blocks In addition to enabling/disabling warning globally via config file (`enable = false`), you can suppress warnings by adding `@Suppress` annotation on individual code blocks or `@file:Suppress()` annotation on a file-level. For example: ``` kotlin @Suppress("FUNCTION_NAME_INCORRECT_CASE") class SomeClass { fun methODTREE(): String { } } ```
Disable all inspections on selected code blocks Also you can suppress **all** warnings by adding `@Suppress("diktat")` annotation on individual code blocks. For example: ``` kotlin @Suppress("diktat") class SomeClass { fun methODTREE(): String { } } ```
ignoreAnnotated: disable inspections on blocks with predefined annotation In the `diktat-analysis.yml` file for each inspection it is possible to define a list of annotations that will cause disabling of the inspection on that particular code block: ```yaml - name: HEADER_NOT_BEFORE_PACKAGE enabled: true ignoreAnnotated: [MyAnnotation, Compose, Controller] ```
Suppress groups of inspections by chapters It is easy to suppress even groups of inspections in diKTat. These groups are linked to chapters of [Codestyle](info/guide/diktat-coding-convention.md). To disable chapters, you will need to add the following configuration to common configuration (`- name: DIKTAT_COMMON`): ```yaml disabledChapters: "1, 2, 3" ``` Mapping of inspections to chapters can be found in [Groups of Inspections](info/rules-mapping.md).
## Running against the baseline When setting up code style analysis on a large existing project, one often doesn't have an ability to fix all findings at once. To allow gradual adoption, diktat and ktlint support baseline mode. When running ktlint for the first time with active baseline, the baseline file will be generated. It is a xml file with a complete list of findings by the tool. On later invocations, only the findings that are not in the baseline file will be reported. Baseline can be activated with CLI flag: ```bash ./diktat --baseline=diktat-baseline.xml **/*.kt ``` or with corresponding configuration options in maven or gradle plugins. Baseline report is intended to be added into the VCS, but it can be removed and re-generated later, if needed. ## Contribution See our [Contributing Policy](CONTRIBUTING.md) and [Code of Conduct](CODE_OF_CONDUCT.md) ## Kotlin Coding Style Guide (Diktat Code Style), v.1.0.0 I [Preface](#c0) * [I.I Purpose of this document](#c0.1) * [I.II General principles](#c0.2) * [I.III Terminology](#c0.3) * [I.IV Exceptions](#c0.4) [1. Naming](#c1) * [1.1 Identifiers](#c1.1) * [1.2 Packages](#c1.2) * [1.3 Classes, enumerations, interfaces](#c1.3) * [1.4 Functions](#c1.4) * [1.5 Constants](#c1.5) * [1.6 Non-constant fields (variables)](#c1.6) * [1.6.1 Non-constant field name](#r1.6.1) * [1.6.2 Boolean variable names with negative meaning](#r1.6.2) [2. Comments](#c2) * [2.1 General form of Kdoc](#c2.1) * [2.1.1 Using KDoc for the public, protected, or internal code elements](#r2.1.1) * [2.1.2 Describing methods that have arguments, a return value, or can throw an exception in the KDoc block](#r2.1.2) * [2.1.3 Only one space between the Kdoc tag and content. Tags are arranged in the order.](#r2.1.3) * [2.2 Adding comments on the file header](#c2.2) * [2.3 Comments on the function header](#c2.3) * [2.4 Code comments](#c2.4) * [2.4.1 Add a blank line between the body of the comment and Kdoc tag-blocks](#r2.4.1) * [2.4.2 Do not comment on unused code blocks](#r2.4.2) * [2.4.3 Code delivered to the client should not contain TODO/FIXME comments](#r2.4.3) [3. General formatting (typesetting)](#c3) * [3.1 File-related rules](#c3.1) * [3.1.1 Avoid files that are too long](#r3.1.1) * [3.1.2 Code blocks in the source file should be separated by one blank line](#r3.1.2) * [3.1.3 Import statements order](#r3.1.3) * [3.1.4 Order of declaration parts of class-like code structures](#r3.1.4) * [3.1.5 Order of declaration of top-level code structures](#r3.1.5) * [3.2 Braces](#c3.2) * [3.2.1 Using braces in conditional statements and loop blocks](#r3.2.1) * [3.2.2 Opening braces are placed at the end of the line in *non-empty* blocks and block structures](#r3.2.2) * [3.3 Indentation](#c3.3) * [3.4 Empty blocks](#c3.4) * [3.5 Line length](#c3.5) * [3.6 Line breaks (newlines)](#c3.6) * [3.6.1 Each line can have a maximum of one statement](#r3.6.1) * [3.6.2 Rules for line-breaking](#r3.6.2) * [3.7 Using blank lines](#c3.7) * [3.8 Horizontal space](#c3.8) * [3.8.1 Usage of whitespace for code separation](#r3.8.1) * [3.8.2 No spaces for horizontal alignment](#r3.8.2) * [3.9 Enumerations](#c3.9) * [3.10 Variable declaration](#c3.10) * [3.10.1 Declare one variable per line](#r3.10.1) * [3.10.2 Variables should be declared near the line where they are first used](#r3.10.2) * [3.11 'When' expression](#c3.11) * [3.12 Annotations](#c3.12) * [3.13 Block comments](#c3.13) * [3.14 Modifiers and constant values](#c3.14) * [3.14.1 Declaration with multiple modifiers](#r3.14.1) * [3.14.2 Separate long numerical values with an underscore](#r3.14.2) * [3.15 Strings](#c3.15) * [3.15.1 Concatenation of Strings](#r3.15.1) * [3.15.2 String template format](#r3.15.2) * [3.16 Conditional statements](#c3.16) * [3.16.1 Collapsing redundant nested if-statements](#r3.16.1) * [3.16.2 Too complex conditions](#r3.16.2) [4. Variables and types](#c4) * [4.1 Variables](#c4.1) * [4.1.1 Do not use Float and Double types when accurate calculations are needed](#r4.1.1) * [4.1.2 Comparing numeric float type values](#r4.1.2) * [4.1.3 Try to use 'val' instead of 'var' for variable declaration [SAY_NO_TO_VAR]](#r4.1.3) * [4.2 Types](#c4.2) * [4.2.1 Use Contracts and smart cast as much as possible](#r4.2.1) * [4.2.2 Try to use type alias to represent types making code more readable](#r4.2.2) * [4.3 Null safety and variable declarations](#c4.3) * [4.3.1 Avoid declaring variables with nullable types, especially from Kotlin stdlib](#r4.3.1) * [4.3.2 Variables of generic types should have an explicit type declaration](#r4.3.2) * [4.3.3 Null-safety](#r4.3.3) [5. Functions](#c5) * [5.1 Function design](#c5.1) * [5.1.1 Avoid functions that are too long ](#r5.1.1) * [5.1.2 Avoid deep nesting of function code blocks, limiting to four levels](#r5.1.2) * [5.1.3 Avoid using nested functions](#r5.1.3) * [5.1.4 Negated function calls](#r5.1.4) * [5.2 Function arguments](#c5.2) * [5.2.1 The lambda parameter of the function should be placed at the end of the argument list](#r5.2.1) * [5.2.2 Number of function parameters should be limited to five](#r5.2.2) * [5.2.3 Use default values for function arguments instead of overloading them](#r5.2.3) * [5.2.4 Synchronizing code inside asynchronous code](#r5.2.4) * [5.2.5 Long lambdas should have explicit parameters](#r5.2.5) * [5.2.6 Avoid using unnecessary, custom label](#r5.2.6) [6. Classes, interfaces, and extension functions](#c6) * [6.1 Classes](#c6.1) * [6.1.1 Denoting a class with a single constructor](#r6.1.1) * [6.1.2 Prefer data classes instead of classes without any functional logic](#r6.1.2) * [6.1.3 Do not use the primary constructor if it is empty or useless](#r6.1.3) * [6.1.4 Do not use redundant init blocks in your class](#r6.1.4) * [6.1.5 Explicit supertype qualification](#r6.1.5) * [6.1.6 Abstract class should have at least one abstract method](#r6.1.6) * [6.1.7 When using the "implicit backing property" scheme, the name of real and back property should be the same](#r6.1.7) * [6.1.8 Avoid using custom getters and setters](#r6.1.8) * [6.1.9 Never use the name of a variable in the custom getter or setter (possible_bug)](#r6.1.9) * [6.1.10 No trivial getters and setters are allowed in the code](#r6.1.10) * [6.1.11 Use 'apply' for grouping object initialization](#r6.1.11) * [6.1.12 Prefer Inline classes when a class has a single property](#r6.1.12) * [6.2 Extension functions](#c6.2) * [6.2.1 Use extension functions for making logic of classes less coupled](#r6.2.1) * [6.2.2 No extension functions with the same name and signature if they extend base and inheritor classes (possible_bug)](#r6.2.2) * [6.2.3 Don't use extension functions for the class in the same file](#r6.2.3) * [6.3 Interfaces](#c6.3) * [6.4 Objects](#c6.4) * [6.4.1 Instead of using utility classes/objects, use extensions](#r6.4.1) * [6.4.2 Objects should be used for Stateless Interfaces](#r6.4.2) * [6.5 Kts Files](#c6.5) * [6.5.1 kts files should wrap logic into top-level scope](#r6.5.1) ## Preface ### Purpose of this document The purpose of this document is to provide a specification that software developers could reference to enhance their ability to write consistent, easy-to-read, and high-quality code. Such a specification will ultimately improve software development efficiency and product competitiveness. For the code to be considered high-quality, it must entail the following characteristics: 1. Simplicity 2. Maintainability 3. Reliability 4. Testability 5. Efficiency 6. Portability 7. Reusability ### General principles Like other modern programming languages, Kotlin is an advanced programming language that complies with the following general principles: 1. Clarity — a necessary feature of programs that are easy to maintain and refactor. 2. Simplicity — a code is easy to understand and implement. 3. Consistency — enables a code to be easily modified, reviewed, and understood by the team members. Unification is particularly important when the same team works on the same project, utilizing similar styles enabling a code to be easily modified, reviewed, and understood by the team members. Also, we need to consider the following factors when programming on Kotlin: 1. Writing clean and simple Kotlin code Kotlin combines two of the main programming paradigms: functional and object-oriented. Both of these paradigms are trusted and well-known software engineering practices. As a young programming language, Kotlin is built on top of well-established languages such as Java, C++, C#, and Scala. This enables Kotlin to introduce many features that help a developer write cleaner, more readable code while also reducing the number of complex code structures. For example, type and null safety, extension functions, infix syntax, immutability, val/var differentiation, expression-oriented features, "when" statements, much easier work with collections, type auto conversion, and other syntactic sugar. 2. Following Kotlin idioms The author of Kotlin, Andrey Breslav, mentioned that Kotlin is both pragmatic and practical, but not academic. Its pragmatic features enable ideas to be transformed into real working software easily. Kotlin is closer to natural languages than its predecessors, and it implements the following design principles: readability, reusability, interoperability, security, and tool-friendliness (https://blog.jetbrains.com/kotlin/2018/10/kotlinconf-2018-announcements/). 3. Using Kotlin efficiently Some Kotlin features can help you to write higher-performance code: including rich coroutine library, sequences, inline functions/classes, arrays of basic types, tailRec, and CallsInPlace of contract. ### Terminology **Rules** — conventions that should be followed when programming. **Recommendations** — conventions that should be considered when programming. **Explanation** — necessary explanations of rules and recommendations. **Valid Example** — recommended examples of rules and recommendations. **Invalid Example** — not recommended examples of rules and recommendations. Unless otherwise stated, this specification applies to versions 1.3 and later of Kotlin. ### Exceptions Even though exceptions may exist, it is essential to understand why rules and recommendations are needed. Depending on a project situation or personal habits, you can break some of the rules. However, remember that one exception may lead to many and eventually can destroy code consistency. As such, there should be very few exceptions. When modifying open-source code or third-party code, you can choose to use the code style from this open-source project (instead of using the existing specifications) to maintain consistency. Software that is directly based on the Android native operating system interface, such as the Android Framework, remains consistent with the Android style. # 1. Naming In programming, it is not always easy to meaningfully and appropriately name variables, functions, classes, etc. Using meaningful names helps to clearly express your code's main ideas and functionality and avoid misinterpretation, unnecessary coding and decoding, "magic" numbers, and inappropriate abbreviations. Note: The source file encoding format (including comments) must be UTF-8 only. The ASCII horizontal space character (0x20, that is, space) is the only permitted whitespace character. Tabs should not be used for indentation. ### 1.1 Identifiers This section describes the general rules for naming identifiers. #### 1.1.1 Identifiers naming conventions For identifiers, use the following naming conventions: 1. All identifiers should use only ASCII letters or digits, and the names should match regular expressions `\w{2,64}`. Explanation: Each valid identifier name should match the regular expression `\w{2,64}`. `{2,64}` means that the name length is 2 to 64 characters, and the length of the variable name should be proportional to its life range, functionality, and responsibility. Name lengths of less than 31 characters are generally recommended. However, this depends on the project. Otherwise, a class declaration with generics or inheritance from a superclass can cause line breaking. No special prefix or suffix should be used in names. The following examples are inappropriate names: name_, mName, s_name, and kName. 2. Choose file names that would describe the content. Use camel case (PascalCase) and `.kt` extension. 3. Typical examples of naming: | Meaning | Correct |Incorrect| | ---- | ---- | ---- | | "XML Http Request" | XmlHttpRequest | XMLHTTPRequest | | "new customer ID" | newCustomerId | newCustomerID | | "inner stopwatch" | innerStopwatch | innerStopWatch | | "supports IPv6 on iOS" | supportsIpv6OnIos | supportsIPv6OnIOS | | "YouTube importer" | YouTubeImporter | YoutubeImporter | 4. The usage of (``) and free naming for functions and identifiers are prohibited. For example, the following code is not recommended: ```kotlin val `my dummy name-with-minus` = "value" ``` The only exception is function names in `Unit tests.` 5. Backticks (``) should not be used for identifiers, except the names of test methods (marked with @Test annotation): ```kotlin @Test fun `my test`() { /*...*/ } ``` 6. The following table contains some characters that may cause confusion. Be careful when using them as identifiers. To avoid issues, use other names instead. | Expected | Confusing name | Suggested name | | ------------- | ------------------------ | ---------------- | | 0 (zero) | O, D | obj, dgt | | 1 (one) | I, l | it, ln, line | | 2 (two) | Z | n1, n2 | | 5 (five) | S | xs, str | | 6 (six) | e | ex, elm | | 8 (eight) | B | bt, nxt | | n,h | h,n | nr, head, height | | rn, m | m,rn | mbr, item | **Exceptions:** - The i,j,k variables used in loops are part of the industry standard. One symbol can be used for such variables. - The `e` variable can be used to catch exceptions in catch block: `catch (e: Exception) {}` - The Java community generally does not recommend the use of prefixes. However, when developing Android code, you can use the s and m prefixes for static and non-public non-static fields, respectively. Note that prefixing can also negatively affect the style and the auto-generation of getters and setters. | Type | Naming style | | ---- | ---- | | Interfaces, classes, annotations, enumerated types, and object type names | Camel case, starting with a capital letter. Test classes have a Test suffix. The filename is 'TopClassName'.kt. | | Class fields, local variables, methods, and method parameters | Camel case starting with a low case letter. Test methods can be underlined with '_'; the only exception is [backing properties](#r6.1.7). | Static constants and enumerated values | Only uppercase underlined with '_' | | Generic type variable | Single capital letter, which can be followed by a number, for example: `E, T, U, X, T2` | | Exceptions | Same as class names, but with a suffix Exception, for example: `AccessException` and `NullPointerException`| ### 1.2 Packages #### Rule 1.2.1 Package names dots Package names are in lower case and separated by dots. Code developed within your company should start with `your.company.domain.` Numbers are permitted in package names. Each file should have a `package` directive. Package names are all written in lowercase, and consecutive words are concatenated together (no underscores). Package names should contain both the product or module names and the department (or team) name to prevent conflicts with other teams. Numbers are not permitted. For example: `org.apache.commons.lang3`, `xxx.yyy.v2`. **Exceptions:** - In certain cases, such as open-source projects or commercial cooperation, package names should not start with `your.company.domain.` - If the package name starts with a number or other character that cannot be used at the beginning of the Java/Kotlin package name, then underscores are allowed. For example: `com.example._123name`. - Underscores are sometimes permitted if the package name contains reserved Java/Kotlin keywords, such as `org.example.hyphenated_name`, `int_.example`. **Valid example**: ```kotlin package your.company.domain.mobilecontrol.views ``` ### 1.3 Classes, enumerations, typealias, interfaces This section describes the general rules for naming classes, enumerations, and interfaces. ### 1.3.1 Classes, enumerations, typealias, interface names use Camel case Classes, enumerations, and interface names use `UpperCamelCase` nomenclature. Follow the naming rules described below: 1. A class name is usually a noun (or a noun phrase) denoted using the camel case nomenclature, such as UpperCamelCase. For example: `Character` or `ImmutableList`. An interface name can also be a noun or noun phrase (such as `List`) or an adjective or adjective phrase (such as `Readable`). Note that verbs are not used to name classes. However, nouns (such as `Customer`, `WikiPage`, and `Account`) can be used. Try to avoid using vague words such as `Manager` and `Process`. 2. Test classes start with the name of the class they are testing and end with 'Test'. For example, `HashTest` or `HashIntegrationTest`. **Invalid example**: ```kotlin class marcoPolo {} class XMLService {} interface TAPromotion {} class info {} ``` **Valid example**: ```kotlin class MarcoPolo {} class XmlService {} interface TaPromotion {} class Order {} ``` ### 1.4 Functions This section describes the general rules for naming functions. ### 1.4.1 Function names should be in camel case Function names should use `lowerCamelCase` nomenclature. Follow the naming rules described below: 1. Function names are usually verbs or verb phrases denoted with the camel case nomenclature (`lowerCamelCase`). For example: `sendMessage`, `stopProcess`, or `calculateValue`. To name functions, use the following formatting rules: a) To get, modify, or calculate a certain value: get + non-boolean field(). Note that the Kotlin compiler automatically generates getters for some classes, applying the special syntax preferred for the 'get' fields: kotlin private val field: String get() { }. kotlin private val field: String get() { }. ```kotlin private val field: String get() { } ``` Note: The calling property access syntax is preferred to call getter directly. In this case, the Kotlin compiler automatically calls the corresponding getter. b) `is` + boolean variable name() c) `set` + field/attribute name(). However, note that the syntax and code generation for Kotlin are completely the same as those described for the getters in point a. d) `has` + Noun / adjective () e) verb() Note: Note: Verb are primarily used for the action objects, such as `document.print ()` f) verb + noun() g) The Callback function allows the names that use the preposition + verb format, such as: `onCreate()`, `onDestroy()`, `toString()`. **Invalid example**: ```kotlin fun type(): String fun Finished(): Boolean fun visible(boolean) fun DRAW() fun KeyListener(Listener) ``` **Valid example**: ```kotlin fun getType(): String fun isFinished(): Boolean fun setVisible(boolean) fun draw() fun addKeyListener(Listener) ``` 2. An underscore (`_`) can be included in the JUnit test function name and should be used as a separator. Each logical part is denoted in `lowerCamelCase`, for example, a typical pattern of using underscore: `pop_emptyStack`. ### 1.5 Constants This section describes the general rules for naming constraints. ### 1.5.1 Using UPPER case and underscore characters in a constraint name Constant names should be in UPPER case, words separated by underscore. The general constant naming conventions are listed below: 1. Constants are attributes created with the `const` keyword or top-level/`val` local variables of an object that holds immutable data. In most cases, constants can be identified as a `const val` property from the `object`/`companion object`/file top level. These variables contain fixed constant values that typically should never be changed by programmers. This includes basic types, strings, immutable types, and immutable collections of immutable types. The value is not constant for the object, which state can be changed. 2. Constant names should contain only uppercase letters separated by an underscores. They should have a val or const val modifier to make them final explicitly. In most cases, if you need to specify a constant value, then you need to create it with the "const val" modifier. Note that not all `val` variables are constants. 3. Objects with immutable content, such as `Logger` and `Lock`, can be in uppercase as constants or have camel case as regular variables. 4. Use meaningful constants instead of `magic numbers`. SQL or logging strings should not be treated as magic numbers, nor should they be defined as string constants. Magic constants, such as `NUM_FIVE = 5` or `NUM_5 = 5` should not be treated as constants. This is because mistakes will easily be made if they are changed to `NUM_5 = 50` or 55. These constants typically represent business logic values, such as measures, capacity, scope, location, tax rate, promotional discounts, and power base multiples in algorithms. You can avoid using magic numbers with the following method: - Using library functions and APIs. For example, instead of checking that `size == 0`, use `isEmpty()` function. To work with `time`, use built-ins from `java.time API`. - Enumerations can be used to name patterns. Refer to [Recommended usage scenario for enumeration in 3.9](#c3.9). **Invalid example**: ```kotlin var int MAXUSERNUM = 200; val String sL = "Launcher"; ``` **Valid example**: ```kotlin const val int MAX_USER_NUM = 200; const val String APPLICATION_NAME = "Launcher"; ``` ### 1.6 Non-constant fields (variables) This section describes the general rules for naming variables. ### 1.6.1 Non-constant field name Non-constant field names should use camel case and start with a lowercase letter. A local variable cannot be treated as constant even if it is final and immutable. Therefore, it should not use the preceding rules. Names of collection type variables (sets, lists, etc.) should contain plural nouns. For example: `var namesList: List` Names of non-constant variables should use `lowerCamelCase`. The name of the final immutable field used to store the singleton object can use the same camel case notation. **Invalid example**: ```kotlin customername: String user: List = listof() ``` **Valid example**: ```kotlin var customerName: String val users: List = listOf(); val mutableCollection: MutableSet = HashSet() ``` ### 1.6.2 Boolean variable names with negative meaning Avoid using Boolean variable names with a negative meaning. When using a logical operator and name with a negative meaning, the code may be difficult to understand, which is referred to as the "double negative". For instance, it is not easy to understand the meaning of !isNotError. The JavaBeans specification automatically generates isXxx() getters for attributes of Boolean classes. However, not all methods returning Boolean type have this notation. For Boolean local variables or methods, it is highly recommended that you add non-meaningful prefixes, including is (commonly used by JavaBeans), has, can, should, and must. Modern integrated development environments (IDEs) such as Intellij are already capable of doing this for you when you generate getters in Java. For Kotlin, this process is even more straightforward as everything is on the byte-code level under the hood. **Invalid example**: ```kotlin val isNoError: Boolean val isNotFound: Boolean fun empty() fun next(); ``` **Valid example**: ```kotlin val isError: Boolean val isFound: Boolean val hasLicense: Boolean val canEvaluate: Boolean val shouldAbort: Boolean fun isEmpty() fun hasNext() ``` # 2. Comments The best practice is to begin your code with a summary, which can be one sentence. Try to balance between writing no comments at all and obvious commentary statements for each line of code. Comments should be accurately and clearly expressed, without repeating the name of the class, interface, or method. Comments are not a solution to the wrong code. Instead, you should fix the code as soon as you notice an issue or plan to fix it (by entering a TODO comment, including a Jira number). Comments should accurately reflect the code's design ideas and logic and further describe its business logic. As a result, other programmers will be able to save time when trying to understand the code. Imagine that you are writing the comments to help yourself to understand the original ideas behind the code in the future. ### 2.1 General form of Kdoc KDoc is a combination of JavaDoc's block tags syntax (extended to support specific constructions of Kotlin) and Markdown's inline markup. The basic format of KDoc is shown in the following example: ```kotlin /** * There are multiple lines of KDoc text, * Other ... */ fun method(arg: String) { // ... } ``` It is also shown in the following single-line form: ```kotlin /** Short form of KDoc. */ ``` Use a single-line form when you store the entire KDoc block in one line (and there is no KDoc mark @XXX). For detailed instructions on how to use KDoc, refer to [Official Document](https://docs.oracle.com/en/Kotlin/Kotlinse/11/tools/KDoc.html). #### 2.1.1 Using KDoc for the public, protected, or internal code elements At a minimum, KDoc should be used for every public, protected, or internal decorated class, interface, enumeration, method, and member field (property). Other code blocks can also have KDocs if needed. Instead of using comments or KDocs before properties in the primary constructor of a class - use `@property` tag in a KDoc of a class. All properties of the primary constructor should also be documented in a KDoc with a `@property` tag. **Incorrect example:** ```kotlin /** * Class description */ class Example( /** * property description */ val foo: Foo, // another property description val bar: Bar ) ``` **Correct example:** ```kotlin /** * Class description * @property foo property description * @property bar another property description */ class Example( val foo: Foo, val bar: Bar ) ``` - Don't use Kdoc comments inside code blocks as block comments **Incorrect Example:** ```kotlin class Example { fun doGood() { /** * wrong place for kdoc */ 1 + 2 } } ``` **Correct Example:** ```kotlin class Example { fun doGood() { /* * right place for block comment */ 1 + 2 } } ``` **Exceptions:** * For setters/getters of properties, obvious comments (like `this getter returns field`) are optional. Note that Kotlin generates simple `get/set` methods under the hood. * It is optional to add comments for simple one-line methods, such as shown in the example below: ```kotlin val isEmpty: Boolean get() = this.size == 0 ``` or ```kotlin fun isEmptyList(list: List) = list.size == 0 ``` **Note:** You can skip KDocs for a method's override if it is almost the same as the superclass method. #### 2.1.2 Describing methods that have arguments, a return value, or can throw an exception in the KDoc block When the method has such details as arguments, return value, or can throw exceptions, it must be described in the KDoc block (with @param, @return, @throws, etc.). **Valid examples:** ```kotlin /** * This is the short overview comment for the example interface. * / * Add a blank line between the comment text and each KDoc tag underneath * / * @since 1.6 */ protected abstract class Sample { /** * This is a long comment with whitespace that should be split in * comments on multiple lines if the line comment formatting is enabled. * / * Add a blank line between the comment text and each KDoc tag underneath * / * @param fox A quick brown fox jumps over the lazy dog * @return battle between fox and dog */ protected abstract fun foo(Fox fox) /** * These possibilities include: Formatting of header comments * / * Add a blank line between the comment text and each KDoc tag underneath * / * @return battle between fox and dog * @throws ProblemException if lazy dog wins */ protected fun bar() throws ProblemException { // Some comments / * No need to add a blank line here * / var aVar = ... // Some comments / * Add a blank line before the comment * / fun doSome() } } ``` #### 2.1.3 Only one space between the Kdoc tag and content. Tags are arranged in the order. There should be only one space between the Kdoc tag and content. Tags are arranged in the following order: @param, @return, and @throws. Therefore, Kdoc should contain the following: - Functional and technical description, explaining the principles, intentions, contracts, API, etc. - The function description and @tags (`implSpec`, `apiNote`, and `implNote`) require an empty line after them. - `@implSpec`: A specification related to API implementation, and it should let the implementer decide whether to override it. - `@apiNote`: Explain the API precautions, including whether to allow null and whether the method is thread-safe, as well as the algorithm complexity, input, and output range, exceptions, etc. - `@implNote`: A note related to API implementation, which implementers should keep in mind. - One empty line, followed by regular `@param`, `@return`, `@throws`, and other comments. - The conventional standard "block labels" are arranged in the following order: `@param`, `@return`, `@throws`. Kdoc should not contain: - Empty descriptions in tag blocks. It is better not to write Kdoc than waste code line space. - There should be no empty lines between the method/class declaration and the end of Kdoc (`*/` symbols). - `@author` tag. It doesn't matter who originally created a class when you can use `git blame` or VCS of your choice to look through the changes history. Important notes: - KDoc does not support the `@deprecated` tag. Instead, use the `@Deprecated` annotation. - The `@since` tag should be used for versions only. Do not use dates in `@since` tag, it's confusing and less accurate. If a tag block cannot be described in one line, indent the content of the new line by *four spaces* from the `@` position to achieve alignment (`@` counts as one + three spaces). **Exception:** When the descriptive text in a tag block is too long to wrap, you can indent the alignment with the descriptive text in the last line. The descriptive text of multiple tags does not need to be aligned. See [3.8 Horizontal space](#c3.8). In Kotlin, compared to Java, you can put several classes inside one file, so each class should have a Kdoc formatted comment (as stated in rule 2.1). This comment should contain @since tag. The right style is to write the application version when its functionality is released. It should be entered after the `@since` tag. **Examples:** ```kotlin /** * Description of functionality * * @since 1.6 */ ``` Other KDoc tags (such as @param type parameters and @see.) can be added as follows: ```kotlin /** * Description of functionality * * @apiNote: Important information about API * * @since 1.6 */ ``` ### 2.2 Adding comments on the file header This section describes the general rules of adding comments on the file header. ### 2.2.1 Formatting of comments in the file header Comments on the file header should be placed before the package name and imports. If you need to add more content to the comment, subsequently add it in the same format. Comments on the file header must include copyright information, without the creation date and author's name (use VCS for history management). Also, describe the content inside files that contain multiple or no classes. The following examples for Huawei describe the format of the *copyright license*: \ Chinese version: `版权所有 (c) 华为技术有限公司 2012-2020` \ English version: `Copyright (c) Huawei Technologies Co., Ltd. 2012-2020. All rights reserved.` `2012` and `2020` are the years the file was first created and the current year, respectively. Do not place **release notes** in header, use VCS to keep track of changes in file. Notable changes can be marked in individual KDocs using `@since` tag with version. Invalid example: ```kotlin /** * Release notes: * 2019-10-11: added class Foo */ class Foo ``` Valid example: ```kotlin /** * @since 2.4.0 */ class Foo ``` - The **copyright statement** can use your company's subsidiaries, as shown in the below examples: \ Chinese version: `版权所有 (c) 海思半导体 2012-2020` \ English version: `Copyright (c) Hisilicon Technologies Co., Ltd. 2012-2020. All rights reserved.` - The copyright information should not be written in KDoc style or use single-line comments. It must start from the beginning of the file. The following example is a copyright statement for Huawei, without other functional comments: ```kotlin /* * Copyright (c) Huawei Technologies Co., Ltd. 2012-2020. All rights reserved. */ ``` The following factors should be considered when writing the file header or comments for top-level classes: - File header comments must start from the top of the file. If it is a top-level file comment, there should be a blank line after the last Kdoc `*/` symbol. If it is a comment for a top-level class, the class declaration should start immediately without using a newline. - Maintain a unified format. The specific format can be formulated by the project (for example, if you use an existing opensource project), and you need to follow it. - A top-level file-Kdoc must include a copyright and functional description, especially if there is more than one top-level class. - Do not include empty comment blocks. If there is no content after the option `@apiNote`, the entire tag block should be deleted. - The industry practice is not to include historical information in the comments. The corresponding history can be found in VCS (git, svn, etc.). Therefore, it is not recommended to include historical data in the comments of the Kotlin source code. ### 2.3 Comments on the function header Comments on the function header are placed above function declarations or definitions. A newline should not exist between a function declaration and its Kdoc. Use the preceding <> style rules. As stated in Chapter 1, the function name should reflect its functionality as much as possible. Therefore, in the Kdoc, try to describe the functionality that is not mentioned in the function name. Avoid unnecessary comments on dummy coding. The function header comment's content is optional, but not limited to function description, return value, performance constraints, usage, memory conventions, algorithm implementation, reentrant requirements, etc. ### 2.4 Code comments This section describes the general rules of adding code comments. #### 2.4.1 Add a blank line between the body of the comment and Kdoc tag-blocks. It is a good practice to add a blank line between the body of the comment and Kdoc tag-blocks. Also, consider the following rules: - There must be one space between the comment character and the content of the comment. - There must be a newline between a Kdoc and the presiding code. - An empty line should not exist between a Kdoc and the code it is describing. You do not need to add a blank line before the first comment in a particular namespace (code block) (for example, between the function declaration and first comment in a function body). **Valid Examples:** ```kotlin /** * This is the short overview comment for the example interface. * * @since 1.6 */ public interface Example { // Some comments /* Since it is the first member definition in this code block, there is no need to add a blank line here */ val aField: String = ... /* Add a blank line above the comment */ // Some comments val bField: String = ... /* Add a blank line above the comment */ /** * This is a long comment with whitespace that should be split in * multiple line comments in case the line comment formatting is enabled. * /* blank line between description and Kdoc tag */ * @param fox A quick brown fox jumps over the lazy dog * @return the rounds of battle of fox and dog */ fun foo(Fox fox) /* Add a blank line above the comment */ /** * These possibilities include: Formatting of header comments * * @return the rounds of battle of fox and dog * @throws ProblemException if lazy dog wins */ fun bar() throws ProblemException { // Some comments /* Since it is the first member definition in this range, there is no need to add a blank line here */ var aVar = ... // Some comments /* Add a blank line above the comment */ fun doSome() } } ``` - Leave one single space between the comment on the right side of the code and the code. If you use conditional comments in the `if-else-if` scenario, put the comments inside the `else-if` branch or in the conditional block, but not before the `else-if`. This makes the code more understandable. When the if-block is used with curly braces, the comment should be placed on the next line after opening the curly braces. Compared to Java, the `if` statement in Kotlin statements returns a value. For this reason, a comment block can describe a whole `if-statement`. **Valid examples:** ```kotlin val foo = 100 // right-side comment val bar = 200 /* right-side comment */ // general comment for the value and whole if-else condition val someVal = if (nr % 15 == 0) { // when nr is a multiple of both 3 and 5 println("fizzbuzz") } else if (nr % 3 == 0) { // when nr is a multiple of 3, but not 5 // We print "fizz", only. println("fizz") } else if (nr % 5 == 0) { // when nr is a multiple of 5, but not 3 // we print "buzz" only. println("buzz") } else { // otherwise, we print the number. println(x) } ``` - Start all comments (including KDoc) with a space after the first symbol (`//`, `/*`, `/**` and `*`) **Valid example:** ```kotlin val x = 0 // this is a comment ``` #### 2.4.2 Do not comment on unused code blocks Do not comment on unused code blocks, including imports. Delete these code blocks immediately. A code is not used to store history. Git, svn, or other VCS tools should be used for this purpose. Unused imports increase the coupling of the code and are not conducive to maintenance. The commented out code cannot be appropriately maintained. In an attempt to reuse the code, there is a high probability that you will introduce defects that are easily missed. The correct approach is to delete the unnecessary code directly and immediately when it is not used anymore. If you need the code again, consider porting or rewriting it as changes could have occurred since you first commented on the code. #### 2.4.3 Code delivered to the client should not contain TODO/FIXME comments The code officially delivered to the client typically should not contain TODO/FIXME comments. `TODO` comments are typically used to describe modification points that need to be improved and added. For example, refactoring FIXME comments are typically used to describe known defects and bugs that will be subsequently fixed and are not critical for an application. They should all have a unified style to facilitate unified text search processing. **Example:** ```kotlin // TODO(): Jira-XXX - support new json format // FIXME: Jira-XXX - fix NPE in this code block ``` At a version development stage, these annotations can be used to highlight the issues in the code, but all of them should be fixed before a new product version is released. # 3. General formatting (typesetting) ### 3.1 File-related rules This section describes the rules related to using files in your code. #### 3.1.1 Avoid files that are too long If the file is too long and complicated, it should be split into smaller files, functions, or modules. Files should not exceed 2000 lines (non-empty and non-commented lines). It is recommended to horizontally or vertically split the file according to responsibilities or hierarchy of its parts. The only exception to this rule is code generation - the auto-generated files that are not manually modified can be longer. #### 3.1.2 Code blocks in the source file should be separated by one blank line A source file contains code blocks in the following order: copyright, package name, imports, and top-level classes. They should be separated by one blank line. a) Code blocks should be in the following order: 1. Kdoc for licensed or copyrighted files 2. `@file` annotation 3. Package name 4. Import statements 5. Top-class header and top-function header comments 6. Top-level classes or functions b) Each of the preceding code blocks should be separated by a blank line. c) Import statements are alphabetically arranged, without using line breaks and wildcards ( wildcard imports - `*`). d) **Recommendation**: One `.kt` source file should contain only one class declaration, and its name should match the filename e) Avoid empty files that do not contain the code or contain only imports/comments/package name f) Unused imports should be removed #### 3.1.3 Import statements order From top to bottom, the order is the following: 1. Android 2. Imports of packages used internally in your organization 3. Imports from other non-core dependencies 4. Java core packages 5. kotlin stdlib Each category should be alphabetically arranged. Each group should be separated by a blank line. This style is compatible with [Android import order](https://source.android.com/setup/contribute/code-style#order-import-statements). **Valid example**: ```kotlin import android.* // android import androidx.* // android import com.android.* // android import com.your.company.* // your company's libs import your.company.* // your company's libs import com.fasterxml.jackson.databind.ObjectMapper // other third-party dependencies import org.junit.jupiter.api.Assertions import java.io.IOException // java core packages import java.net.URL import kotlin.system.exitProcess // kotlin standard library import kotlinx.coroutines.* // official kotlin extension library ``` #### 3.1.4 Order of declaration parts of class-like code structures The declaration parts of class-like code structures (class, interface, etc.) should be in the following order: compile-time constants (for objects), class properties, late-init class properties, init-blocks, constructors, public methods, internal methods, protected methods, private methods, and companion object. Blank lines should separate their declaration. Notes: 1. There should be no blank lines between properties with the following **exceptions**: when there is a comment before a property on a separate line or annotations on a separate line. 2. Properties with comments/Kdoc should be separated by a newline before the comment/Kdoc. 3. Enum entries and constant properties (`const val`) in companion objects should be alphabetically arranged. The declaration part of a class or interface should be in the following order: - Compile-time constants (for objects) - Properties - Late-init class properties - Init-blocks - Constructors - Methods or nested classes. Put nested classes next to the code they are used by. If the classes are meant to be used externally, and are not referenced inside the class, put them after the companion object. - Companion object **Exception:** All variants of a `private val` logger should be placed at the beginning of the class (`private val log`, `LOG`, `logger`, etc.). #### 3.1.5 Order of declaration of top-level code structures Kotlin allows several top-level declaration types: classes, objects, interfaces, properties and functions. When declaring more than one class or zero classes (e.g. only functions), as per rule [2.2.1](#r2.2.1), you should document the whole file in the header KDoc. When declaring top-level structures, keep the following order: 1. Top-level constants and properties (following same order as properties inside a class: `const val`,`val`, `lateinit var`, `var`) 2. typealiases (grouped by their visibility modifiers) 2. Interfaces, classes and objects (grouped by their visibility modifiers) 3. Extension functions 4. Other functions **Note**: Extension functions shouldn't have receivers declared in the same file according to [rule 6.2.3](#r6.2.3) Valid example: ```kotlin package com.saveourtool.diktat.example const val CONSTANT = 42 val topLevelProperty = "String constant" internal typealias ExamplesHandler = (IExample) -> Unit interface IExample class Example : IExample private class Internal fun Other.asExample(): Example { /* ... */ } private fun Other.asInternal(): Internal { /* ... */ } fun doStuff() { /* ... */ } ``` **Note**: kotlin scripts (.kts) allow arbitrary code to be placed on the top level. When writing kotlin scripts, you should first declare all properties, classes and functions. Only then you should execute functions on top level. It is still recommended wrapping logic inside functions and avoid using top-level statements for function calls or wrapping blocks of code in top-level scope functions like `run`. Example: ```kotlin /* class declarations */ /* function declarations */ run { // call functions here } ``` ### 3.2 Braces This section describes the general rules of using braces in your code. #### 3.2.1 Using braces in conditional statements and loop blocks Braces should always be used in `if`, `else`, `for`, `do`, and `while` statements, even if the program body is empty or contains only one statement. In special Kotlin `when` statements, you do not need to use braces for single-line statements. **Valid example:** ```kotlin when (node.elementType) { FILE -> { checkTopLevelDoc(node) checkSomething() } CLASS -> checkClassElements(node) } ``` **Exception:** The only exception is ternary operator in Kotlin (a single line `if () <> else <>` ) **Invalid example:** ```kotlin val value = if (string.isEmpty()) // WRONG! 0 else 1 ``` **Valid example**: ```kotlin val value = if (string.isEmpty()) 0 else 1 // Okay ``` ```kotlin if (condition) { println("test") } else { println(0) } ``` #### 3.2.2 Opening braces are placed at the end of the line in *non-empty* blocks and block structures For *non-empty* blocks and block structures, the opening brace is placed at the end of the line. Follow the K&R style (1TBS or OTBS) for *non-empty* code blocks with braces: - The opening brace and first line of the code block are on the same line. - The closing brace is on its own new line. - The closing brace can be followed by a newline character. The only exceptions are `else`, `finally`, and `while` (from `do-while` statement), or `catch` keywords. These keywords should not be split from the closing brace by a newline character. **Exception cases**: 1) For lambdas, there is no need to put a newline character after the first (function-related) opening brace. A newline character should appear only after an arrow (`->`) (see [point 5 of Rule 3.6.2](#r3.6.2)). ```kotlin arg.map { value -> foo(value) } ``` 2) for `else`/`catch`/`finally`/`while` (from `do-while` statement) keywords closing brace should stay on the same line: ```kotlin do { if (true) { x++ } else { x-- } } while (x > 0) ``` **Valid example:** ```kotlin return arg.map { value -> while (condition()) { method() } value } return MyClass() { @Override fun method() { if (condition()) { try { something() } catch (e: ProblemException) { recover() } } else if (otherCondition()) { somethingElse() } else { lastThing() } } } ``` ### 3.3 Indentation Only spaces are permitted for indentation, and each indentation should equal `four spaces` (tabs are not permitted). If you prefer using tabs, simply configure them to change to spaces in your IDE automatically. These code blocks should be indented if they are placed on the new line, and the following conditions are met: - The code block is placed immediately after an opening brace. - The code block is placed after each operator, including the assignment operator (`+`/`-`/`&&`/`=`/etc.) - The code block is a call chain of methods: ```kotlin someObject .map() .filter() ``` - The code block is placed immediately after the opening parenthesis. - The code block is placed immediately after an arrow in lambda: ```kotlin arg.map { value -> foo(value) } ``` **Exceptions**: 1. Argument lists: \ a) Eight spaces are used to indent argument lists (both in declarations and at call sites). \ b) Arguments in argument lists can be aligned if they are on different lines. 2. Eight spaces are used if there is a newline after any binary operator. 3. Eight spaces are used for functional-like styles when the newline is placed before the dot. 4. Supertype lists: \ a) Four spaces are used if the colon before the supertype list is on a new line. \ b) Four spaces are used before each supertype, and eight spaces are used if the colon is on a new line. **Note:** there should be an indentation after all statements such as `if`, `for`, etc. However, according to this code style, such statements require braces. ```kotlin if (condition) foo() ``` **Exceptions**: - When breaking the parameter list of a method/class constructor, it can be aligned with `8 spaces`. A parameter that was moved to a new line can be on the same level as the previous argument: ```kotlin fun visit( node: ASTNode, autoCorrect: Boolean, params: KtLint.ExperimentalParams, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit ) { } ``` - Such operators as `+`/`-`/`*` can be indented with `8 spaces`: ```kotlin val abcdef = "my splitted" + " string" ``` - Opening and closing quotes in multiline string should have same indentation ```kotlin lintMethod( """ |val q = 1 | """.trimMargin() ) ``` - A list of supertypes should be indented with `4 spaces` if they are on different lines or with `8 spaces` if the leading colon is also on a separate line ```kotlin class A : B() class A : B() ``` ### 3.4 Empty blocks Avoid empty blocks, and ensure braces start on a new line. An empty code block can be closed immediately on the same line and the next line. However, a newline is recommended between opening and closing braces `{}` (see the examples below.) Generally, empty code blocks are prohibited; using them is considered a bad practice (especially for catch block). They are appropriate for overridden functions, when the base class's functionality is not needed in the class-inheritor, for lambdas used as a function and for empty function in implementation of functional interface. ```kotlin override fun foo() { } ``` **Valid examples** (note once again that generally empty blocks are prohibited): ```kotlin fun doNothing() {} fun doNothingElse() { } fun foo(bar: () -> Unit = {}) ``` **Invalid examples:** ```kotlin try { doSomething() } catch (e: Some) {} ``` Use the following valid code instead: ```kotlin try { doSomething() } catch (e: Some) { } ``` ### 3.5 Line length Line length should be less than 120 symbols. Otherwise, it should be split. If `complex property` initializing is too long, It should be split into priorities: \ 1. Logic Binary Expression (&& ||) \ 2. Comparison Binary Expression (> < == >= <= !=) \ 3. Other types (Arithmetical and Bit operation) (+ - * / % >> << *= += -= /= %= ++ -- ! in !in etc) **Invalid example:** ```kotlin val complexProperty = 1 + 2 + 3 + 4 ``` **Valid example:** ```kotlin val complexProperty = 1 + 2 + 3 + 4 ``` **Invalid example:** ```kotlin val complexProperty = (1 + 2 + 3 > 0) && ( 23 * 4 > 10 * 6) ``` **Valid example:** ```kotlin val complexProperty = (1 + 2 + 3 > 0) && (23 * 4 > 10 * 6) ``` If long line should be split in `Elvis Operator` (?:), it`s done like this **Invalid example:** ```kotlin val value = first ?: second ``` **Valid example:** ```kotlin val value = first ?: second ``` If long line in `Dot Qualified Expression` or `Safe Access Expression`, it`s done like this: **Invalid example:** ```kotlin val value = This.Is.Very.Long.Dot.Qualified.Expression ``` **Valid example:** ```kotlin val value = This.Is.Very.Long .Dot.Qualified.Expression ``` **Invalid example:** ```kotlin val value = This.Is?.Very?.Long?.Safe?.Access?.Expression ``` **Valid example:** ```kotlin val value = This.Is?.Very?.Long ?.Safe?.Access?.Expression ``` if `value arguments list` is too long, it also should be split: **Invalid example:** ```kotlin val result1 = ManyParamInFunction(firstArgument, secondArgument, thirdArgument, fourthArgument, fifthArguments) ``` **Valid example:** ```kotlin val result1 = ManyParamInFunction(firstArgument, secondArgument, thirdArgument, fourthArgument, fifthArguments) ``` If `annotation` is too long, it also should be split: **Invalid example:** ```kotlin @Query(value = "select * from table where age = 10", nativeQuery = true) fun foo() {} ``` **Valid example:** ```kotlin @Query( value = "select * from table where age = 10", nativeQuery = true) fun foo() {} ``` Long one line `function` should be split: **Invalid example:** ```kotlin fun foo() = goo().write("TooLong") ``` **Valid example:** ```kotlin fun foo() = goo().write("TooLong") ``` Long `binary expression` should be split into priorities: \ 1. Logic Binary Expression (**&&** **||**) \ 2. Comparison Binary Expression (**>** **<** **==** **>=** **<=** **!=**) \ 3. Other types (Arithmetical and Bit operation) (**+** **-** * **/** **%** **>>** **<<** **/*=** **+=** **-=** **/=** **%=** **++** **--** **!** **in** **!in** etc) **Invalid example:** ```kotlin if (( x > 100) || y < 100 && !isFoo()) {} ``` **Valid example:** ```kotlin if (( x > 100) || y < 100 && !isFoo()) {} ``` `String template` also can be split in white space in string text **Invalid example:** ```kotlin val nameString = "This is very long string template" ``` **Valid example:** ```kotlin val nameString = "This is very long" + " string template" ``` Long `Lambda argument` should be split: **Invalid example:** ```kotlin val variable = a?.filter { it.elementType == true } ?: null ``` **Valid example:** ```kotlin val variable = a?.filter { it.elementType == true } ?: null ``` Long one line `When Entry` should be split: **Invalid example:** ```kotlin when(elem) { true -> long.argument.whenEntry } ``` **Valid example:** ```kotlin when(elem) { true -> { long.argument.whenEntry } } ``` If the examples above do not fit, but the line needs to be split and this in `property`, this is fixed like thisЖ **Invalid example:** ```kotlin val element = veryLongNameFunction(firstParam) ``` **Valid example:** ```kotlin val element = varyLongNameFunction(firstParam) ``` `Eol comment` also can be split, but it depends on comment location. If this comment is on the same line with code it should be on line before: **Invalid example:** ```kotlin fun foo() { val name = "Nick" // this comment is too long } ``` **Valid example:** ```kotlin fun foo() { // this comment is too long val name = "Nick" } ``` But if this comment is on new line - it should be split to several lines: **Invalid example:** ```kotlin // This comment is too long. It should be on two lines. fun foo() {} ``` **Valid example:** ```kotlin // This comment is too long. // It should be on two lines. fun foo() {} ``` The international code style prohibits `non-Latin` (`non-ASCII`) symbols. (See [Identifiers](#r1.1.1)) However, if you still intend on using them, follow the following convention: - One wide character occupies the width of two narrow characters. The "wide" and "narrow" parts of a character are defined by its [east Asian width Unicode attribute](https://unicode.org/reports/tr11/). Typically, narrow characters are also called "half-width" characters. All characters in the ASCII character set include letters (such as `a, A`), numbers (such as `0, 3`), and punctuation spaces (such as `,` , `{`), all of which are narrow characters. Wide characters are also called "full-width" characters. Chinese characters (such as `中, 文`), Chinese punctuation (`,` , `;` ), full-width letters and numbers (such as `A、3`) are "full-width" characters. Each one of these characters represents two narrow characters. - Any line that exceeds this limit (`120 narrow symbols`) should be wrapped, as described in the [Newline section](#c3.5). **Exceptions:** 1. The long URL or long JSON method reference in KDoc. 2. The `package` and `import` statements. 3. The command line in the comment, enabling it to be cut and pasted into the shell for use. ### 3.6 Line breaks (newlines) This section contains the rules and recommendations on using line breaks. #### 3.6.1 Each line can have a maximum of one statement Each line can have a maximum of one code statement. This recommendation prohibits the use of code with `;` because it decreases code visibility. **Invalid example:** ```kotlin val a = ""; val b = "" ``` **Valid example:** ```kotlin val a = "" val b = "" ``` #### 3.6.2 Rules for line-breaking 1) Unlike Java, Kotlin allows you not to put a semicolon (`;`) after each statement separated by a newline character. There should be no redundant semicolon at the end of the lines. When a newline character is needed to split the line, it should be placed after such operators as `&&`/`||`/`+`/etc. and all infix functions (for example, `xor`). However, the newline character should be placed before operators such as `.`, `?.`, `?:`, and `::`. Note that all comparison operators, such as `==`, `>`, `<`, should not be split. **Invalid example**: ```kotlin if (node != null && test != null) {} ``` **Valid example**: ```kotlin if (node != null && test != null) { } ``` **Note:** You need to follow the functional style, meaning each function call in a chain with `.` should start at a new line if the chain of functions contains more than one call: ```kotlin val value = otherValue!! .map { x -> x } .filter { val a = true true } .size ``` **Note:** The parser prohibits the separation of the `!!` operator from the value it is checking. **Exception**: If a functional chain is used inside the branches of a ternary operator, it does not need to be split with newlines. **Valid example**: ```kotlin if (condition) list.map { foo(it) }.filter { bar(it) } else list.drop(1) ``` **Note:** If dot qualified expression is inside condition or passed as an argument - it should be replaced with new variable. **Invalid example**: ```kotlin if (node.treeParent.treeParent?.treeParent.findChildByType(IDENTIFIER) != null) {} ``` **Valid example**: ```kotlin val grandIdentifier = node .treeParent .treeParent ?.treeParent .findChildByType(IDENTIFIER) if (grandIdentifier != null) {} ``` **Second valid example**: ```kotlin val grandIdentifier = node.treeParent .treeParent ?.treeParent .findChildByType(IDENTIFIER) if (grandIdentifier != null) {} ``` 2) Newlines should be placed after the assignment operator (`=`). 3) In function or class declarations, the name of a function or constructor should not be split by a newline from the opening brace `(`. A brace should be placed immediately after the name without any spaces in declarations or at call sites. 4) Newlines should be placed right after the comma (`,`). 5) If a lambda statement contains more than one line in its body, a newline should be placed after an arrow if the lambda statement has explicit parameters. If it uses an implicit parameter (`it`), the newline character should be placed after the opening brace (`{`). The following examples illustrate this rule: **Invalid example:** ```kotlin value.map { name -> foo() bar() } ``` **Valid example:** ```kotlin value.map { name -> foo() bar() } val someValue = { node:String -> node } ``` 6) When the function contains only a single expression, it can be written as [expression function](https://kotlinlang.org/docs/reference/functions.html#single-expression-functions). The below example shows the style that should not be used. Instead of: ```kotlin override fun toString(): String { return "hi" } ``` use: ```kotlin override fun toString() = "hi" ``` 7) If an argument list in a function declaration (including constructors) or function call contains more than two arguments, these arguments should be split by newlines in the following style. **Valid example:** ```kotlin class Foo(val a: String, b: String, val c: String) { } fun foo( a: String, b: String, c: String ) { } ``` If and only if the first parameter is on the same line as an opening parenthesis, all parameters can be horizontally aligned by the first parameter. Otherwise, there should be a line break after an opening parenthesis. Kotlin 1.4 introduced a trailing comma as an optional feature, so it is generally recommended to place all parameters on a separate line and append [trailing comma](https://kotlinlang.org/docs/reference/whatsnew14.html#trailing-comma). It makes the resolving of merge conflicts easier. **Valid example:** ```kotlin fun foo( a: String, b: String, ) { } ``` same should be done for function calls/constructor arguments/e.t.c Kotlin supports trailing commas in the following cases: Enumerations Value arguments Class properties and parameters Function value parameters Parameters with optional type (including setters) Indexing suffix Lambda parameters when entry Collection literals (in annotations) Type arguments Type parameters Destructuring declarations 8) If the supertype list has more than two elements, they should be separated by newlines. **Valid example:** ```kotlin class MyFavouriteVeryLongClassHolder : MyLongHolder(), SomeOtherInterface, AndAnotherOne { } ``` ### 3.7 Using blank lines Reduce unnecessary blank lines and maintain a compact code size. By reducing unnecessary blank lines, you can display more code on one screen, which improves code readability. - Blank lines should separate content based on relevance and should be placed between groups of fields, constructors, methods, nested classes, `init` blocks, and objects (see [3.1.2](#r3.1.2)). - Do not use more than one line inside methods, type definitions, and initialization expressions. - Generally, do not use more than two consecutive blank lines in a row. - Do not put newlines in the beginning or end of code blocks with curly braces. **Valid example:** ```kotlin fun baz() { doSomething() // No need to add blank lines at the beginning and end of the code block // ... } ``` ### 3.8 Horizontal space This section describes general rules and recommendations for using spaces in the code. #### 3.8.1: Usage of whitespace for code separation Follow the recommendations below for using space to separate keywords: **Note:** These recommendations are for cases where symbols are located on the same line. However, in some cases, a line break could be used instead of a space. 1. Separate keywords (such as `if`, `when`, `for`) from the opening parenthesis with single whitespace. The only exception is the `constructor` keyword, which should not be separated from the opening parenthesis. 2. Separate keywords like `else` or `try` from the opening brace (`{`) with single whitespace. If `else` is used in a ternary-style statement without braces, there should be a single space between `else` and the statement after: `if (condition) foo() else bar()` 3. Use a **single** whitespace before all opening braces (`{`). The only exception is the passing of a lambda as a parameter inside parentheses: ```kotlin private fun foo(a: (Int) -> Int, b: Int) {} foo({x: Int -> x}, 5) // no space before '{' ``` 4. Single whitespace should be placed on both sides of binary operators. This also applies to operator-like symbols. For example: - A colon in generic structures with the `where` keyword: `where T : Type` - Arrow in lambdas: `(str: String) -> str.length()` **Exceptions:** - Two colons (`::`) are written without spaces:\ `Object::toString` - The dot separator (`.`) that stays on the same line with an object name:\ `object.toString()` - Safe access modifiers `?.` and `!!` that stay on the same line with an object name:\ `object?.toString()` - Operator `..` for creating ranges:\ `1..100` 5. Use spaces after (`,`), (`:`), and (`;`), except when the symbol is at the end of the line. However, note that this code style prohibits the use of (`;`) in the middle of a line ([see 3.3.2](#r3.2.2)). There should be no whitespaces at the end of a line. The only scenario where there should be no space after a colon is when the colon is used in the annotation to specify a use-site target (for example, `@param:JsonProperty`). There should be no spaces before `,` , `:` and `;`. **Exceptions** for spaces and colons: - When `:` is used to separate a type and a supertype, including an anonymous object (after object keyword) - When delegating to a superclass constructor or different constructor of the same class **Valid example:** ```kotlin abstract class Foo : IFoo { } class FooImpl : Foo() { constructor(x: String) : this(x) { /*...*/ } val x = object : IFoo { /*...*/ } } ``` 6. There should be *only one space* between the identifier and its type: `list: List` If the type is nullable, there should be no space before `?`. 7. When using `[]` operator (`get/set`) there should be **no** spaces between identifier and `[` : `someList[0]`. 8. There should be no space between a method or constructor name (both at declaration and at call site) and a parenthesis: `foo() {}`. Note that this sub-rule is related only to spaces; the rules for whitespaces are described in [see 3.6.2](#r3.6.2). This rule does not prohibit, for example, the following code: ```kotlin fun foo ( a: String ) ``` 9. Never put a space after `(`, `[`, `<` (when used as a bracket in templates) or before `)`, `]`, `>` (when used as a bracket in templates). 10. There should be no spaces between a prefix/postfix operator (like `!!` or `++`) and its operand. #### 3.8.2: No spaces for horizontal alignment *Horizontal alignment* refers to aligning code blocks by adding space to the code. Horizontal alignment should not be used because: - When modifying code, it takes much time for new developers to format, support, and fix alignment issues. - Long identifier names will break the alignment and lead to less presentable code. - There are more disadvantages than advantages in alignment. To reduce maintenance costs, misalignment (???) is the best choice. Recommendation: Alignment only looks suitable for `enum class`, where it can be used in table format to improve code readability: ```kotlin enum class Warnings(private val id: Int, private val canBeAutoCorrected: Boolean, private val warn: String) : Rule { PACKAGE_NAME_MISSING (1, true, "no package name declared in a file"), PACKAGE_NAME_INCORRECT_CASE (2, true, "package name should be completely in a lower case"), PACKAGE_NAME_INCORRECT_PREFIX(3, false, "package name should start from the company's domain") ; } ``` **Valid example:** ```kotlin private val nr: Int // no alignment, but looks fine private var color: Color // no alignment ``` **Invalid example**: ```kotlin private val nr: Int // aligned comment with extra spaces private val color: Color // alignment for a comment and alignment for identifier name ``` ### 3.9 Enumerations Enum values are separated by a comma and line break, with ';' placed on the new line. 1) The comma and line break characters separate enum values. Put `;` on the new line: ```kotlin enum class Warnings { A, B, C, ; } ``` This will help to resolve conflicts and reduce the number of conflicts during merging pull requests. Also, use [trailing comma](https://kotlinlang.org/docs/reference/whatsnew14.html#trailing-comma). 2) If the enum is simple (no properties, methods, and comments inside), you can declare it in a single line: ```kotlin enum class Suit { CLUBS, HEARTS, SPADES, DIAMONDS } ``` 3) Enum classes take preference (if it is possible to use it). For example, instead of two boolean properties: ```kotlin val isCelsius = true val isFahrenheit = false ``` use enum class: ```kotlin enum class TemperatureScale { CELSIUS, FAHRENHEIT } ``` - The variable value only changes within a fixed range and is defined with the enum type. - Avoid comparison with magic numbers of `-1, 0, and 1`; use enums instead. ```kotlin enum class ComparisonResult { ORDERED_ASCENDING, ORDERED_SAME, ORDERED_DESCENDING, ; } ``` ### 3.10 Variable declaration This section describes rules for the declaration of variables. #### 3.10.1 Declare one variable per line Each property or variable must be declared on a separate line. **Invalid example**: ```kotlin val n1: Int; val n2: Int ``` #### 3.10.2 Variables should be declared near the line where they are first used Declare local variables close to the point where they are first used to minimize their scope. This will also increase the readability of the code. Local variables are usually initialized during their declaration or immediately after. The member fields of the class should be declared collectively (see [Rule 3.1.2](#r3.1.2) for details on the class structure). ### 3.11 'When' expression The `when` statement must have an 'else' branch unless the condition variable is enumerated or a sealed type. Each `when` statement should contain an `else` statement group, even if it does not contain any code. **Exception:** If 'when' statement of the `enum or sealed` type contains all enum values, there is no need to have an "else" branch. The compiler can issue a warning when it is missing. ### 3.12 Annotations Each annotation applied to a class, method or constructor should be placed on its own line. Consider the following examples: 1. Annotations applied to the class, method or constructor are placed on separate lines (one annotation per line). **Valid example**: ```kotlin @MustBeDocumented @CustomAnnotation fun getNameIfPresent() { /* ... */ } ``` 2. A single annotation should be on the same line as the code it is annotating. **Valid example**: ```kotlin @CustomAnnotation class Foo {} ``` 3. Multiple annotations applied to a field or property can appear on the same line as the corresponding field. **Valid example**: ```kotlin @MustBeDocumented @CustomAnnotation val loader: DataLoader ``` ### 3.13 Block comments Block comments should be placed at the same indentation level as the surrounding code. See examples below. **Valid example**: ```kotlin class SomeClass { /* * This is * okay */ fun foo() {} } ``` **Note**: Use `/*...*/` block comments to enable automatic formatting by IDEs. ### 3.14 Modifiers and constant values This section contains recommendations regarding modifiers and constant values. #### 3.14.1 Declaration with multiple modifiers If a declaration has multiple modifiers, always follow the proper sequence. **Valid sequence:** ```kotlin public / internal / protected / private expect / actual final / open / abstract / sealed / const external override lateinit tailrec crossinline vararg suspend inner out enum / annotation companion inline / noinline reified infix operator data ``` #### 3.14.2: Separate long numerical values with an underscore An underscore character should separate long numerical values. **Note:** Using underscores simplifies reading and helps to find errors in numeric constants. ```kotlin val oneMillion = 1_000_000 val creditCardNumber = 1234_5678_9012_3456L val socialSecurityNumber = 999_99_9999L val hexBytes = 0xFF_EC_DE_5E val bytes = 0b11010010_01101001_10010100_10010010 ``` #### 3.14.3: Magic number Prefer defining constants with clear names describing what the magic number means. **Valid example**: ```kotlin class Person() { fun isAdult(age: Int): Boolean = age >= majority companion object { private const val majority = 18 } } ``` **Invalid example**: ```kotlin class Person() { fun isAdult(age: Int): Boolean = age >= 18 } ``` ### 3.15 Strings This section describes the general rules of using strings. #### 3.15.1 Concatenation of Strings String concatenation is prohibited if the string can fit on one line. Use raw strings and string templates instead. Kotlin has significantly improved the use of Strings: [String templates](https://kotlinlang.org/docs/reference/basic-types.html#string-templates), [Raw strings](https://kotlinlang.org/docs/reference/basic-types.html#string-literals). Therefore, compared to using explicit concatenation, code looks much better when proper Kotlin strings are used for short lines, and you do not need to split them with newline characters. **Invalid example**: ```kotlin val myStr = "Super string" val value = myStr + " concatenated" ``` **Valid example**: ```kotlin val myStr = "Super string" val value = "$myStr concatenated" ``` #### 3.15.2 String template format **Redundant curly braces in string templates** If there is only one variable in a string template, there is no need to use such a template. Use this variable directly. **Invalid example**: ```kotlin val someString = "${myArgument} ${myArgument.foo()}" ``` **Valid example**: ```kotlin val someString = "$myArgument ${myArgument.foo()}" ``` **Redundant string template** In case a string template contains only one variable - there is no need to use the string template. Use this variable directly. **Invalid example**: ```kotlin val someString = "$myArgument" ``` **Valid example**: ```kotlin val someString = myArgument ``` ### 3.16 Conditional Statements This section describes the general rules related to the сonditional statements. #### 3.16.1 Collapsing redundant nested if-statements The nested if-statements, when possible, should be collapsed into a single one by concatenating their conditions with the infix operator &&. This improves the readability by reducing the number of the nested language constructs. #### Simple collapse **Invalid example**: ```kotlin if (cond1) { if (cond2) { doSomething() } } ``` **Valid example**: ```kotlin if (cond1 && cond2) { doSomething() } ``` #### Compound conditions **Invalid example**: ```kotlin if (cond1) { if (cond2 || cond3) { doSomething() } } ``` **Valid example**: ```kotlin if (cond1 && (cond2 || cond3)) { doSomething() } ``` #### 3.16.2 Too complex conditions Too complex conditions should be simplified according to boolean algebra rules, if it is possible. The following rules are considered when simplifying an expression: * boolean literals are removed (e.g. `foo() || false` -> `foo()`) * double negation is removed (e.g. `!(!a)` -> `a`) * expression with the same variable are simplified (e.g. `a && b && a` -> `a && b`) * remove expression from disjunction, if they are subset of other expression (e.g. `a || (a && b)` -> `a`) * remove expression from conjunction, if they are more broad than other expression (e.g. `a && (a || b)` -> `a`) * de Morgan's rule (negation is moved inside parentheses, i.e. `!(a || b)` -> `!a && !b`) **Valid example** ```kotlin if (condition1 && condition2) { foo() } ``` **Invalid example** ```kotlin if (condition1 && condition2 && condition1) { foo() } ``` # 4. Variables and types This section is dedicated to the rules and recommendations for using variables and types in your code. ### 4.1 Variables The rules of using variables are explained in the below topics. #### 4.1.1 Do not use Float and Double types when accurate calculations are needed Floating-point numbers provide a good approximation over a wide range of values, but they cannot produce accurate results in some cases. Binary floating-point numbers are unsuitable for precise calculations because it is impossible to represent 0.1 or any other negative power of 10 in a `binary representation` with a finite length. The following code example seems to be obvious: ```kotlin val myValue = 2.0 - 1.1 println(myValue) ``` However, it will print the following value: `0.8999999999999999` Therefore, for precise calculations (for example, in finance or exact sciences), using such types as `Int`, `Long`, `BigDecimal`are recommended. The `BigDecimal` type should serve as a good choice. **Invalid example**: Float values containing more than six or seven decimal numbers will be rounded. ```kotlin val eFloat = 2.7182818284f // Float, will be rounded to 2.7182817 ``` **Valid example**: (when precise calculations are needed): ```kotlin val income = BigDecimal("2.0") val expense = BigDecimal("1.1") println(income.subtract(expense)) // you will obtain 0.9 here ``` #### 4.1.2: Comparing numeric float type values Numeric float type values should not be directly compared with the equality operator (==) or other methods, such as `compareTo()` and `equals()`. Since floating-point numbers involve precision problems in computer representation, it is better to use `BigDecimal` as recommended in [Rule 4.1.1](#r4.1.1) to make accurate computations and comparisons. The following code describes these problems. **Invalid example**: ```kotlin val f1 = 1.0f - 0.9f val f2 = 0.9f - 0.8f if (f1 == f2) { println("Expected to enter here") } else { println("But this block will be reached") } val flt1 = f1; val flt2 = f2; if (flt1.equals(flt2)) { println("Expected to enter here") } else { println("But this block will be reached") } ``` **Valid example**: ```kotlin val foo = 1.03f val bar = 0.42f if (abs(foo - bar) > 1e-6f) { println("Ok") } else { println("Not") } ``` #### 4.1.3 Try to use 'val' instead of 'var' for variable declaration [SAY_NO_TO_VAR] Variables with the `val` modifier are immutable (read-only). Using `val` variables instead of `var` variables increases code robustness and readability. This is because `var` variables can be reassigned several times in the business logic. However, in some scenarios with loops or accumulators, only `var`s are permitted. ### 4.2 Types This section provides recommendations for using types. #### 4.2.1: Use Contracts and smart cast as much as possible The Kotlin compiler has introduced [Smart Casts](https://kotlinlang.org/docs/reference/typecasts.html#smart-casts) that help reduce the size of code. **Invalid example**: ```kotlin if (x is String) { print((x as String).length) // x was already automatically cast to String - no need to use 'as' keyword here } ``` **Valid example**: ```kotlin if (x is String) { print(x.length) // x was already automatically cast to String - no need to use 'as' keyword here } ``` Also, Kotlin 1.3 introduced [Contracts](https://kotlinlang.org/docs/reference/whatsnew13.html#contracts) that provide enhanced logic for smart-cast. Contracts are used and are very stable in `stdlib`, for example: ```kotlin fun bar(x: String?) { if (!x.isNullOrEmpty()) { println("length of '$x' is ${x.length}") // smartcasted to not-null } } ``` Smart cast and contracts are a better choice because they reduce boilerplate code and features forced type conversion. **Invalid example**: ```kotlin fun String?.isNotNull(): Boolean = this != null fun foo(s: String?) { if (s.isNotNull()) s!!.length // No smartcast here and !! operator is used } ``` **Valid example**: ```kotlin fun foo(s: String?) { if (s.isNotNull()) s.length // We have used a method with contract from stdlib that helped compiler to execute smart cast } ``` #### 4.2.2: Try to use type alias to represent types making code more readable Type aliases provide alternative names for existing types. If the type name is too long, you can replace it with a shorter name, which helps to shorten long generic types. For example, code looks much more readable if you introduce a `typealias` instead of a long chain of nested generic types. We recommend using a `typealias` if the type contains **more than two** nested generic types and is longer than **25 chars**. **Invalid example**: ```kotlin val b: MutableMap> ``` **Valid example**: ```kotlin typealias FileTable = MutableMap> val b: FileTable ``` You can also provide additional aliases for function (lambda-like) types: ```kotlin typealias MyHandler = (Int, String, Any) -> Unit typealias Predicate = (T) -> Boolean ``` ### 4.3 Null safety and variable declarations Kotlin is declared as a null-safe programming language. However, to achieve compatibility with Java, it still supports nullable types. #### 4.3.1: Avoid declaring variables with nullable types, especially from Kotlin stdlib To avoid `NullPointerException` and help the compiler prevent Null Pointer Exceptions, avoid using nullable types (with `?` symbol). **Invalid example**: ```kotlin val a: Int? = 0 ``` **Valid example**: ```kotlin val a: Int = 0 ``` Nevertheless, when using Java libraries extensively, you have to use nullable types and enrich the code with `!!` and `?` symbols. Avoid using nullable types for Kotlin stdlib (declared in [official documentation](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/)). Try to use initializers for empty collections. For example, if you want to initialize a list instead of `null`, use `emptyList()`. **Invalid example**: ```kotlin val a: List? = null ``` **Valid example**: ```kotlin val a: List = emptyList() ``` #### 4.3.2: Variables of generic types should have an explicit type declaration Like in Java, classes in Kotlin may have type parameters. To create an instance of such a class, we typically need to provide type arguments: ```kotlin val myVariable: Map = emptyMap() ``` However, the compiler can inherit type parameters from the r-value (value assigned to a variable). Therefore, it will not force users to declare the type explicitly. These declarations are not recommended because programmers would need to find the return value and understand the variable type by looking at the method. **Invalid example**: ```kotlin val myVariable = emptyMap() ``` **Valid example**: ```kotlin val myVariable: Map = emptyMap() ``` #### 4.3.3 Null-safety Try to avoid explicit null checks (explicit comparison with `null`) Kotlin is declared as [Null-safe](https://kotlinlang.org/docs/reference/null-safety.html) language. However, Kotlin architects wanted Kotlin to be fully compatible with Java; that's why the `null` keyword was also introduced in Kotlin. There are several code-structures that can be used in Kotlin to avoid null-checks. For example: `?:`, `.let {}`, `.also {}`, e.t.c **Invalid example:** ```kotlin // example 1 var myVar: Int? = null if (myVar == null) { println("null") return } // example 2 if (myVar != null) { println("not null") return } // example 3 val anotherVal = if (myVar != null) { println("not null") 1 } else { 2 } // example 4 if (myVar == null) { println("null") } else { println("not null") } ``` **Valid example:** ```kotlin // example 1 var myVar: Int? = null myVar?: run { println("null") return } // example 2 myVar?.let { println("not null") return } // example 3 val anotherVal = myVar?.also { println("not null") 1 } ?: 2 // example 4 myVar?.let { println("not null") } ?: run { println("null") } ``` **Exceptions:** In the case of complex expressions, such as multiple `else-if` structures or long conditional statements, there is common sense to use explicit comparison with `null`. **Valid examples:** ```kotlin if (myVar != null) { println("not null") } else if (anotherCondition) { println("Other condition") } ``` ```kotlin if (myVar == null || otherValue == 5 && isValid) {} ``` Please also note, that instead of using `require(a != null)` with a not null check - you should use a special Kotlin function called `requireNotNull(a)`. # 5. Functions This section describes the rules of using functions in your code. ### 5.1 Function design Developers can write clean code by gaining knowledge of how to build design patterns and avoid code smells. You should utilize this approach, along with functional style, when writing Kotlin code. The concepts behind functional style are as follows: Functions are the smallest unit of combinable and reusable code. They should have clean logic, **high cohesion**, and **low coupling** to organize the code effectively. The code in functions should be simple and not conceal the author's original intentions. Additionally, it should have a clean abstraction, and control statements should be used straightforwardly. The side effects (code that does not affect a function's return value but affects global/object instance variables) should not be used for state changes of an object. The only exceptions to this are state machines. Kotlin is [designed](https://www.slideshare.net/abreslav/whos-more-functional-kotlin-groovy-scala-or-java) to support and encourage functional programming, featuring the corresponding built-in mechanisms. Also, it supports standard collections and sequences feature methods that enable functional programming (for example, `apply`, `with`, `let`, and `run`), Kotlin Higher-Order functions, function types, lambdas, and default function arguments. As [previously discussed](#r4.1.3), Kotlin supports and encourages the use of immutable types, which in turn motivates programmers to write pure functions that avoid side effects and have a corresponding output for specific input. The pipeline data flow for the pure function comprises a functional paradigm. It is easy to implement concurrent programming when you have chains of function calls, where each step features the following characteristics: 1. Simplicity 2. Verifiability 3. Testability 4. Replaceability 5. Pluggability 6. Extensibility 7. Immutable results There can be only one side effect in this data stream, which can be placed only at the end of the execution queue. #### 5.1.1 Avoid functions that are too long The function should be displayable on one screen and only implement one certain logic. If a function is too long, it often means complex and could be split or simplified. Functions should consist of 30 lines (non-empty and non-comment) in total. **Exception:** Some functions that implement complex algorithms may exceed 30 lines due to aggregation and comprehensiveness. Linter warnings for such functions **can be suppressed**. Even if a long function works well, new problems or bugs may appear due to the function's complex logic once it is modified by someone else. Therefore, it is recommended to split such functions into several separate and shorter functions that are easier to manage. This approach will enable other programmers to read and modify the code properly. #### 5.1.2 Avoid deep nesting of function code blocks, limiting to four levels The nesting depth of a function's code block is the depth of mutual inclusion between the code control blocks in the function (for example: if, for, while, and when). Each nesting level will increase the amount of effort needed to read the code because you need to remember the current "stack" (for example, entering conditional statements and loops). **Exception:** The nesting levels of the lambda expressions, local classes, and anonymous classes in functions are calculated based on the innermost function. The nesting levels of enclosing methods are not accumulated. Functional decomposition should be implemented to avoid confusion for the developer who reads the code. This will help the reader switch between contexts. #### 5.1.3 Avoid using nested functions Nested functions create a more complex function context, thereby confusing readers. With nested functions, the visibility context may not be evident to the code reader. **Invalid example**: ```kotlin fun foo() { fun nested():String { return "String from nested function" } println("Nested Output: ${nested()}") } ``` #### 5.1.4 Negated function calls Don't use negated function calls if it can be replaced with negated version of this function **Invalid example**: ```kotlin fun foo() { val list = listOf(1, 2, 3) if (!list.isEmpty()) { // Some cool logic } } ``` **Valid example**: ```kotlin fun foo() { val list = listOf(1, 2, 3) if (list.isNotEmpty()) { // Some cool logic } } ``` ### 5.2 Function arguments The rules for using function arguments are described in the below topics. #### 5.2.1 The lambda parameter of the function should be placed at the end of the argument list With such notation, it is easier to use curly brackets, leading to better code readability. **Valid example**: ```kotlin // declaration fun myFoo(someArg: Int, myLambda: () -> Unit) { // ... } // usage myFoo(1) { println("hey") } ``` #### 5.2.2 Number of function parameters should be limited to five A long argument list is a [code smell](https://en.wikipedia.org/wiki/Code_smell) that leads to less reliable code. It is recommended to reduce the number of parameters. Having **more than five** parameters leads to difficulties in maintenance and conflicts merging. If parameter groups appear in different functions multiple times, these parameters are closely related and can be encapsulated into a single Data Class. It is recommended that you use Data Classes and Maps to unify these function arguments. #### 5.2.3 Use default values for function arguments instead of overloading them In Java, default values for function arguments are prohibited. That is why the function should be overloaded when you need to create a function with fewer arguments. In Kotlin, you can use default arguments instead. **Invalid example**: ```kotlin private fun foo(arg: Int) { // ... } private fun foo() { // ... } ``` **Valid example**: ```kotlin private fun foo(arg: Int = 0) { // ... } ``` #### 5.2.4 Synchronizing code inside asynchronous code Try to avoid using `runBlocking` in asynchronous code **Invalid example**: ```kotlin GlobalScope.async { runBlocking { count++ } } ``` #### 5.2.5 Long lambdas should have explicit parameters The lambda without parameters shouldn't be too long. If a lambda is too long, it can confuse the user. Lambda without parameters should consist of 10 lines (non-empty and non-comment) in total. #### 5.2.6 Avoid using unnecessary, custom label Expressions with unnecessary, custom labels generally increase complexity and worsen the maintainability of the code. **Invalid example**: ```kotlin run lab@ { list.forEach { return@lab } } ``` **Valid example**: ```kotlin list.forEachIndexed { index, i -> return@forEachIndexed } lab@ for(i: Int in q) { for (j: Int in q) { println(i) break@lab } } ``` # 6. Classes, interfaces, and extension functions ### 6.1 Classes This section describes the rules of denoting classes in your code. #### 6.1.1 Denoting a class with a single constructor When a class has a single constructor, it should be defined as a primary constructor in the declaration of the class. If the class contains only one explicit constructor, it should be converted to a primary constructor. **Invalid example**: ```kotlin class Test { var a: Int constructor(a: Int) { this.a = a } } ``` **Valid example**: ```kotlin class Test(var a: Int) { // ... } // in case of any annotations or modifiers used on a constructor: class Test private constructor(var a: Int) { // ... } ``` #### 6.1.2 Prefer data classes instead of classes without any functional logic Some people say that the data class is a code smell. However, if you need to use it (which makes your code more simple), you can utilize the Kotlin `data class`. The main purpose of this class is to hold data, but also `data class` will automatically generate several useful methods: - equals()/hashCode() pair; - toString() - componentN() functions corresponding to the properties in their order of declaration; - copy() function Therefore, instead of using `normal` classes: ```kotlin class Test { var a: Int = 0 get() = field set(value: Int) { field = value} } class Test { var a: Int = 0 var b: Int = 0 constructor(a:Int, b: Int) { this.a = a this.b = b } } // or class Test(var a: Int = 0, var b: Int = 0) // or class Test() { var a: Int = 0 var b: Int = 0 } ``` **prefer data classes:** ```kotlin data class Test1(var a: Int = 0, var b: Int = 0) ``` **Exception 1**: Note that data classes cannot be abstract, open, sealed, or inner; that is why these types of classes cannot be changed to a data class. **Exception 2**: No need to convert a class to a data class if this class extends some other class or implements an interface. #### 6.1.3 Do not use the primary constructor if it is empty or useless The primary constructor is a part of the class header; it is placed after the class name and type parameters (optional) but can be omitted if it is not used. **Invalid example**: ```kotlin // simple case that does not need a primary constructor class Test() { var a: Int = 0 var b: Int = 0 } // empty primary constructor is not needed here // it can be replaced with a primary contructor with one argument or removed class Test() { var a = "Property" init { println("some init") } constructor(a: String): this() { this.a = a } } ``` **Valid example**: ```kotlin // the good example here is a data class; this example also shows that you should get rid of braces for the primary constructor class Test { var a: Int = 0 var b: Int = 0 } ``` #### 6.1.4 Do not use redundant init blocks in your class Several init blocks are redundant and generally should not be used in your class. The primary constructor cannot contain any code. That is why Kotlin has introduced `init` blocks. These blocks store the code to be run during the class initialization. Kotlin allows writing multiple initialization blocks executed in the same order as they appear in the class body. Even when you follow (rule 3.2)[#r3.2], this makes your code less readable as the programmer needs to keep in mind all init blocks and trace the execution of the code. Therefore, you should try to use a single `init` block to reduce the code's complexity. If you need to do some logging or make some calculations before the class property assignment, you can use powerful functional programming. This will reduce the possibility of the error if your `init` blocks' order is accidentally changed and make the code logic more coupled. It is always enough to use one `init` block to implement your idea in Kotlin. **Invalid example**: ```kotlin class YourClass(var name: String) { init { println("First initializer block that prints ${name}") } val property = "Property: ${name.length}".also(::println) init { println("Second initializer block that prints ${name.length}") } } ``` **Valid example**: ```kotlin class YourClass(var name: String) { init { println("First initializer block that prints ${name}") } val property = "Property: ${name.length}".also { prop -> println(prop) println("Second initializer block that prints ${name.length}") } } ``` The `init` block was not added to Kotlin to help you initialize your properties; it is needed for more complex tasks. Therefore if the `init` block contains only assignments of variables - move it directly to properties to be correctly initialized near the declaration. In some cases, this rule can be in clash with [6.1.1](#r6.1.1), but that should not stop you. **Invalid example**: ```kotlin class A(baseUrl: String) { private val customUrl: String init { customUrl = "$baseUrl/myUrl" } } ``` **Valid example**: ```kotlin class A(baseUrl: String) { private val customUrl = "$baseUrl/myUrl" } ``` #### 6.1.5 Explicit supertype qualification The explicit supertype qualification should not be used if there is no clash between called methods. This rule is applicable to both interfaces and classes. **Invalid example**: ```kotlin open class Rectangle { open fun draw() { /* ... */ } } class Square() : Rectangle() { override fun draw() { super.draw() // no need in super here } } ``` #### 6.1.6 Abstract class should have at least one abstract method Abstract classes are used to force a developer to implement some of its parts in their inheritors. When the abstract class has no abstract methods, it was set `abstract` incorrectly and can be converted to a regular class. **Invalid example**: ```kotlin abstract class NotAbstract { fun foo() {} fun test() {} } ``` **Valid example**: ```kotlin abstract class NotAbstract { abstract fun foo() fun test() {} } // OR class NotAbstract { fun foo() {} fun test() {} } ``` #### 6.1.7 When using the "implicit backing property" scheme, the name of real and back property should be the same Kotlin has a mechanism of [backing properties](https://kotlinlang.org/docs/reference/properties.html#backing-properties). In some cases, implicit backing is not enough and it should be done explicitly: ```kotlin private var _table: Map? = null val table: Map get() { if (_table == null) { _table = HashMap() // Type parameters are inferred } return _table ?: throw AssertionError("Set to null by another thread") } ``` In this case, the name of the backing property (`_table`) should be the same as the name of the real property (`table`) but should have an underscore (`_`) prefix. It is one of the exceptions from the [identifier names rule](#r1.2) #### 6.1.8 Avoid using custom getters and setters Kotlin has a perfect mechanism of [properties](https://kotlinlang.org/docs/reference/properties.html#properties-and-fields). Kotlin compiler automatically generates `get` and `set` methods for properties and can override them. **Invalid example:** ```kotlin class A { var size: Int = 0 set(value) { println("Side effect") field = value } // user of this class does not expect calling A.size receive size * 2 get() = field * 2 } ``` From the callee code, these methods look like access to this property: `A().isEmpty = true` for setter and `A().isEmpty` for getter. However, when `get` and `set` are overridden, it isn't very clear for a developer who uses this particular class. The developer expects to get the property value but receives some unknown value and some extra side-effect hidden by the custom getter/setter. Use extra functions instead to avoid confusion. **Valid example**: ```kotlin class A { var size: Int = 0 fun initSize(value: Int) { // some custom logic } // this will not confuse developer and he will get exactly what he expects fun goodNameThatDescribesThisGetter() = this.size * 2 } ``` **Exception:** `Private setters` are only exceptions that are not prohibited by this rule. #### 6.1.9 Never use the name of a variable in the custom getter or setter (possible_bug) If you ignored [recommendation 6.1.8](#r6.1.8), be careful with using the name of the property in your custom getter/setter as it can accidentally cause a recursive call and a `StackOverflow Error`. Use the `field` keyword instead. **Invalid example (very bad)**: ```kotlin var isEmpty: Boolean set(value) { println("Side effect") isEmpty = value } get() = isEmpty ``` #### 6.1.10 No trivial getters and setters are allowed in the code In Java, trivial getters - are the getters that are just returning the field value. Trivial setters - are merely setting the field with a value without any transformation. However, in Kotlin, trivial getters/setters are generated by default. There is no need to use it explicitly for all types of data structures in Kotlin. **Invalid example**: ```kotlin class A { var a: Int = 0 get() = field set(value: Int) { field = value } // } ``` **Valid example**: ```kotlin class A { var a: Int = 0 get() = field set(value: Int) { field = value } // } ``` #### 6.1.11 Use 'apply' for grouping object initialization In Java, before functional programming became popular, many classes from common libraries used the configuration paradigm. To use these classes, you had to create an object with the constructor with 0-2 arguments and set the fields needed to run the object. In Kotlin, to reduce the number of dummy code line and to group objects [`apply` extension](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/apply.html) was added: **Invalid example**: ```kotlin class HttpClient(var name: String) { var url: String = "" var port: String = "" var timeout = 0 fun doRequest() {} } fun main() { val httpClient = HttpClient("myConnection") httpClient.url = "http://example.com" httpClient.port = "8080" httpClient.timeout = 100 httpCLient.doRequest() } ``` **Valid example**: ```kotlin class HttpClient(var name: String) { var url: String = "" var port: String = "" var timeout = 0 fun doRequest() {} } fun main() { val httpClient = HttpClient("myConnection") .apply { url = "http://example.com" port = "8080" timeout = 100 } httpClient.doRequest() } ``` ### 6.1.12 Prefer Inline classes when a class has a single property If a class has only one immutable property, then it can be converted to the inline class. Sometimes it is necessary for business logic to create a wrapper around some type. However, it introduces runtime overhead due to additional heap allocations. Moreover, if the wrapped type is primitive, the performance hit is terrible, because primitive types are usually heavily optimized by the runtime, while their wrappers don't get any special treatment. **Invalid example**: ```kotlin class Password { val value: String } ``` **Valid example**: ```kotlin inline class Password(val value: String) ``` ### 6.2 Extension functions This section describes the rules of using extension functions in your code. [Extension functions](https://kotlinlang.org/docs/reference/extensions.html) is a killer-feature in Kotlin. It gives you a chance to extend classes that were already implemented in external libraries and helps you to make classes less heavy. Extension functions are resolved statically. #### 6.2.1 Use extension functions for making logic of classes less coupled It is recommended that for classes, the non-tightly coupled functions, which are rarely used in the class, should be implemented as extension functions where possible. They should be implemented in the same class/file where they are used. This is a non-deterministic rule, so the code cannot be checked or fixed automatically by a static analyzer. #### 6.2.2 No extension functions with the same name and signature if they extend base and inheritor classes (possible_bug) You should avoid declaring extension functions with the same name and signature if their receivers are base and inheritor classes (possible_bug), as extension functions are resolved statically. There could be a situation when a developer implements two extension functions: one is for the base class and another for the inheritor. This can lead to an issue when an incorrect method is used. **Invalid example**: ```kotlin open class A class B: A() // two extension functions with the same signature fun A.foo() = "A" fun B.foo() = "B" fun printClassName(s: A) { println(s.foo()) } // this call will run foo() method from the base class A, but // programmer can expect to run foo() from the class inheritor B fun main() { printClassName(B()) } ``` #### 6.2.3 Don't use extension functions for the class in the same file You should not use extension functions for the class in the same file, where it is defined. **Invalid example**: ```kotlin class SomeClass { } fun SomeClass.deleteAllSpaces() { } ``` #### 6.2.4 Use 'lastIndex' in case you need to get latest element of a collection You should not use property length with operation - 1, you can change this to lastIndex **Invalid example**: ```kotlin val A = "name" val B = A.length - 1 val C = A[A.length - 1] ``` **Valid example**: ```kotlin val A = "name" val B = A.lastIndex val C = A[A.lastIndex] ``` ### 6.3 Interfaces An `Interface` in Kotlin can contain declarations of abstract methods, as well as method implementations. What makes them different from abstract classes is that interfaces cannot store state. They can have properties, but these need to be abstract or to provide accessor implementations. Kotlin's interfaces can define attributes and functions. In Kotlin and Java, the interface is the main presentation means of application programming interface (API) design and should take precedence over the use of (abstract) classes. ### 6.4 Objects This section describes the rules of using objects in code. #### 6.4.1 Instead of using utility classes/objects, use extensions Avoid using utility classes/objects; use extensions instead. As described in [6.2 Extension functions](#c6.2), using extension functions is a powerful method. This enables you to avoid unnecessary complexity and class/object wrapping and use top-level functions instead. **Invalid example**: ```kotlin object StringUtil { fun stringInfo(myString: String): Int { return myString.count{ "something".contains(it) } } } StringUtil.stringInfo("myStr") ``` **Valid example**: ```kotlin fun String.stringInfo(): Int { return this.count{ "something".contains(it) } } "myStr".stringInfo() ``` #### 6.4.2 Objects should be used for Stateless Interfaces Kotlin’s objects are extremely useful when you need to implement some interface from an external library that does not have any state. There is no need to use classes for such structures. **Valid example**: ``` interface I { fun foo() } object O: I { override fun foo() {} } ``` ### 6.5 Kts Files This section describes general rules for `.kts` files #### 6.5.1 kts files should wrap logic into top-level scope It is still recommended wrapping logic inside functions and avoid using top-level statements for function calls or wrapping blocks of code in top-level scope functions like `run`. **Valid example**: ``` run { // some code } fun foo() { } ``` ================================================ FILE: RELEASING.md ================================================ # How to release a new version of diKTat * You should have permissions to push to the main repo * Simply create a new git tag with format `v*` and push it. Github workflow will perform release automatically. For example: ```bash $ git tag v1.0.0 $ git push origin --tags ``` After the release workflow has started, version number is determined from tag. Binaries are uploaded to maven repo and a new github release is created with fat jar. ================================================ FILE: _config.yml ================================================ theme: jekyll-theme-minimal ================================================ FILE: build.gradle.kts ================================================ import org.jetbrains.kotlin.incremental.createDirectory import java.nio.file.Files import java.nio.file.StandardCopyOption @Suppress("DSL_SCOPE_VIOLATION", "RUN_IN_SCRIPT") // https://github.com/gradle/gradle/issues/22797 plugins { id("com.saveourtool.diktat.buildutils.versioning-configuration") id("com.saveourtool.diktat.buildutils.git-hook-configuration") id("com.saveourtool.diktat.buildutils.code-quality-convention") id("com.saveourtool.diktat.buildutils.publishing-configuration") alias(libs.plugins.talaiot.base) java `maven-publish` } talaiot { metrics { // disabling due to problems with OSHI on some platforms performanceMetrics = false environmentMetrics = false } publishers { timelinePublisher = true } } project.description = "diKTat kotlin formatter and fixer" val libsFileName = "libs.versions.toml" val libsFile = rootProject.file("gradle/$libsFileName") val libsFileBackup = rootProject.file("gradle/${libsFileName}_backup") tasks.create("generateLibsForDiktatSnapshot") { val dir = rootProject.layout .buildDirectory .dir("diktat-snapshot") .get() .asFile val dependency = rootProject.project(":diktat-gradle-plugin") dependsOn(dependency.let { "${it.path}:publishToMavenLocal" }) inputs.file(libsFile) inputs.files(dependency.pomFile()) inputs.files(dependency.artifactFile()) inputs.property("project-version", version.toString()) outputs.dir(dir) doFirst { dir.deleteRecursively() dir.createDirectory() } doLast { Files.readAllLines(libsFile.toPath()) .map { line -> when { line.contains("diktat = ") -> "diktat = \"$version\"" else -> line } } .let { val libsFileForDiktatSnapshot = dir.resolve(libsFileName) Files.write(libsFileForDiktatSnapshot.toPath(), it) Files.move(libsFile.toPath(), libsFileBackup.toPath(), StandardCopyOption.REPLACE_EXISTING) Files.copy(libsFileForDiktatSnapshot.toPath(), libsFile.toPath()) } val artifactDir = dir.pathToMavenArtifact(dependency) .also { it.createDirectory() } Files.copy(dependency.pomFile().toPath(), artifactDir.resolve(dependency.pomFileName()).toPath()) Files.copy(dependency.artifactFile().toPath(), artifactDir.resolve(dependency.artifactFileName()).toPath()) } } tasks.create("rollbackLibsForDiktatSnapshot") { inputs.file(libsFileBackup) outputs.file(libsFile) doLast { Files.deleteIfExists(libsFile.toPath()) Files.move(libsFileBackup.toPath(), libsFile.toPath()) } } /** * @param project * @return resolved path to directory according to maven coordinate */ fun File.pathToMavenArtifact(project: Project): File = project.group.toString() .split(".") .fold(this) { dirToArtifact, newPart -> dirToArtifact.resolve(newPart) } .resolve(project.name) .resolve(project.version.toString()) /** * @return generated pom.xml for project dependency */ fun Project.pomFile(): File = layout.buildDirectory .dir("publications") .map { publicationsDir -> publicationsDir.dir("pluginMaven") .takeIf { it.asFile.exists() } ?: publicationsDir.dir("maven") } .map { it.file("pom-default.xml").asFile } .get() /** * @return file name of pom.xml for project */ fun Project.pomFileName(): String = "$name-$version.pom" /** * @return generated artifact for project dependency */ fun Project.artifactFile(): File = layout.buildDirectory .dir("libs") .map { it.file(artifactFileName()).asFile } .get() /** * @return file name of artifact for project dependency */ fun Project.artifactFileName(): String = "$name-$version.jar" ================================================ FILE: detekt-config.yml ================================================ 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' # - 'ProjectComplexityProcessor' # - 'ProjectCognitiveComplexityProcessor' # - 'ProjectLLOCProcessor' # - 'ProjectCLOCProcessor' # - 'ProjectLOCProcessor' # - 'ProjectSLOCProcessor' # - 'LicenseHeaderLoaderExtension' console-reports: active: true exclude: - 'ProjectStatisticsReport' - 'ComplexityReport' - 'NotificationReport' # - 'FindingsReport' - 'FileBasedFindingsReport' output-reports: active: true exclude: # - 'TxtOutputReport' # - 'XmlOutputReport' # - 'HtmlOutputReport' comments: active: true excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] AbsentOrWrongFileLicense: active: false licenseTemplateFile: 'license.template' licenseTemplateIsRegex: false CommentOverPrivateFunction: active: false CommentOverPrivateProperty: active: false EndOfSentenceFormat: active: false endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)' UndocumentedPublicClass: active: false searchInNestedClass: true searchInInnerClass: true searchInInnerObject: true searchInInnerInterface: true UndocumentedPublicFunction: active: false UndocumentedPublicProperty: active: false complexity: active: true ComplexCondition: active: false threshold: 4 ComplexInterface: active: false threshold: 10 includeStaticDeclarations: false includePrivateDeclarations: false CyclomaticComplexMethod: active: true threshold: 15 ignoreSingleWhenExpression: false ignoreSimpleWhenEntries: false ignoreNestingFunctions: false nestingFunctions: [run, let, apply, with, also, use, forEach, isNotNull, ifNull] LabeledExpression: active: false ignoredLabels: [] LargeClass: active: true threshold: 600 LongMethod: active: true threshold: 60 LongParameterList: active: true functionThreshold: 6 constructorThreshold: 7 ignoreDefaultParameters: false ignoreDataClasses: true ignoreAnnotated: [] MethodOverloading: active: true threshold: 6 NamedArguments: active: false threshold: 3 NestedBlockDepth: active: false threshold: 4 ReplaceSafeCallChainWithRun: active: false StringLiteralDuplication: active: false excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] threshold: 3 ignoreAnnotation: true excludeStringsWithLessThan5Characters: true ignoreStringsRegex: '$^' TooManyFunctions: active: false excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] thresholdInFiles: 11 thresholdInClasses: 11 thresholdInInterfaces: 11 thresholdInObjects: 11 thresholdInEnums: 11 ignoreDeprecated: false ignorePrivate: false ignoreOverridden: false coroutines: active: true GlobalCoroutineUsage: active: true RedundantSuspendModifier: active: true SleepInsteadOfDelay: active: true SuspendFunWithFlowReturnType: active: true empty-blocks: active: true EmptyCatchBlock: active: true allowedExceptionNameRegex: '_|(ignore|expected).*' EmptyClassBlock: active: true EmptyDefaultConstructor: active: true EmptyDoWhileBlock: active: true EmptyElseBlock: active: true EmptyFinallyBlock: active: true EmptyForBlock: active: true EmptyFunctionBlock: active: true ignoreOverridden: false EmptyIfBlock: active: true EmptyInitBlock: active: true EmptyKtFile: active: true EmptySecondaryConstructor: active: true EmptyTryBlock: active: true EmptyWhenBlock: active: true EmptyWhileBlock: active: true exceptions: active: true ExceptionRaisedInUnexpectedLocation: active: true methodNames: [toString, hashCode, equals, finalize] InstanceOfCheckForException: active: false excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] NotImplementedDeclaration: active: true ObjectExtendsThrowable: active: true PrintStackTrace: active: true RethrowCaughtException: active: true ReturnFromFinally: active: true ignoreLabeled: false SwallowedException: active: false ignoredExceptionTypes: - InterruptedException - NumberFormatException - ParseException - MalformedURLException allowedExceptionNameRegex: '_|(ignore|expected).*' ThrowingExceptionFromFinally: active: true ThrowingExceptionInMain: active: true ThrowingExceptionsWithoutMessageOrCause: active: true excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] exceptions: - IllegalArgumentException - IllegalStateException - IOException ThrowingNewInstanceOfSameException: active: true TooGenericExceptionCaught: active: true excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] exceptionNames: - ArrayIndexOutOfBoundsException - Error - Exception - IllegalMonitorStateException - NullPointerException - IndexOutOfBoundsException - RuntimeException - Throwable allowedExceptionNameRegex: '_|(ignore|expected).*' TooGenericExceptionThrown: active: true exceptionNames: - Error - Exception - Throwable - RuntimeException naming: active: true ClassNaming: active: true excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] classPattern: '[A-Z][a-zA-Z0-9]*' ConstructorParameterNaming: active: true excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] parameterPattern: '[a-z][A-Za-z0-9]*' privateParameterPattern: '[a-z][A-Za-z0-9]*' excludeClassPattern: '$^' EnumNaming: active: true excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] enumEntryPattern: '[A-Z][_a-zA-Z0-9]*' ForbiddenClassName: active: false excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] forbiddenName: [] FunctionMaxLength: active: false excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] maximumFunctionNameLength: 30 FunctionMinLength: active: false excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] minimumFunctionNameLength: 3 FunctionNaming: active: true excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] functionPattern: '([a-z][a-zA-Z0-9]*)|(`.*`)' excludeClassPattern: '$^' ignoreAnnotated: ['Composable'] FunctionParameterNaming: active: true excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] parameterPattern: '[a-z][A-Za-z0-9]*' excludeClassPattern: '$^' InvalidPackageDeclaration: active: false excludes: ['*.kts'] rootPackage: '' MatchingDeclarationName: active: true mustBeFirst: true MemberNameEqualsClassName: active: true ignoreOverridden: true NoNameShadowing: active: false NonBooleanPropertyPrefixedWithIs: active: false excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] ObjectPropertyNaming: active: true excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] constantPattern: '[A-Za-z][_A-Za-z0-9]*' propertyPattern: '[A-Za-z][_A-Za-z0-9]*' privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' PackageNaming: active: true excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*' TopLevelPropertyNaming: active: true excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] constantPattern: '[A-Z][_A-Z0-9]*' propertyPattern: '[A-Za-z][_A-Za-z0-9]*' privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*' VariableMaxLength: active: false excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] maximumVariableNameLength: 64 VariableMinLength: active: false excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] minimumVariableNameLength: 1 VariableNaming: active: true excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] variablePattern: '[a-z][A-Za-z0-9]*' privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' excludeClassPattern: '$^' performance: active: true ArrayPrimitive: active: true ForEachOnRange: active: true excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] SpreadOperator: active: true excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] UnnecessaryTemporaryInstantiation: active: true potential-bugs: active: true CastToNullableType: active: false Deprecation: active: true DontDowncastCollectionTypes: active: true EqualsAlwaysReturnsTrueOrFalse: active: true EqualsWithHashCodeExist: active: true ExitOutsideMain: active: false ExplicitGarbageCollectionCall: active: true HasPlatformType: active: false IgnoredReturnValue: active: true restrictToConfig: true returnValueAnnotations: ['*.CheckReturnValue', '*.CheckResult'] ImplicitDefaultLocale: active: false ImplicitUnitReturnType: active: false allowExplicitReturnType: true InvalidRange: active: true IteratorHasNextCallsNextMethod: active: true IteratorNotThrowingNoSuchElementException: active: true LateinitUsage: active: false excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] ignoreOnClassesPattern: '' MapGetWithNotNullAssertionOperator: active: false NullableToStringCall: active: false UnconditionalJumpStatementInLoop: active: false UnnecessaryNotNullOperator: active: true UnnecessarySafeCall: active: true UnreachableCatchBlock: active: false UnreachableCode: active: true UnsafeCallOnNullableType: active: true UnsafeCast: active: true UnusedUnaryOperator: active: false UselessPostfixExpression: active: false WrongEqualsTypeParameter: active: true style: active: true ClassOrdering: active: true CollapsibleIfStatements: active: false DataClassContainsFunctions: active: false conversionFunctionPrefix: 'to' DataClassShouldBeImmutable: active: false DestructuringDeclarationWithTooManyEntries: active: true maxDestructuringEntries: 3 EqualsNullCall: active: true EqualsOnSignatureLine: active: false ExplicitCollectionElementAccessMethod: active: false ExplicitItLambdaParameter: active: false ExpressionBodySyntax: active: false includeLineWrapping: false ForbiddenImport: active: false imports: [] forbiddenPatterns: '' ForbiddenMethodCall: active: true methods: ['kotlin.io.println', 'kotlin.io.print'] excludes: ["**/src/test/**"] ForbiddenPublicDataClass: active: true excludes: ['**'] ignorePackages: ['*.internal', '*.internal.*'] ForbiddenVoid: active: false ignoreOverridden: false ignoreUsageInGenerics: false FunctionOnlyReturningConstant: active: true ignoreOverridableFunction: true ignoreActualFunction: true excludedFunctions: 'describeContents' ignoreAnnotated: ['dagger.Provides'] LibraryCodeMustSpecifyReturnType: active: true 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 MandatoryBracesLoops: active: false MaxLineLength: active: true maxLineLength: 180 excludePackageStatements: true excludeImportStatements: true excludeCommentStatements: false MayBeConst: active: true ModifierOrder: active: true MultilineLambdaItParameter: active: false NestedClassesVisibility: active: true NewLineAtEndOfFile: active: true NoTabs: active: false OptionalAbstractKeyword: active: true OptionalUnit: active: false PreferToOverPairSyntax: active: false ProtectedMemberInFinalClass: active: true RedundantExplicitType: active: false RedundantHigherOrderMapUsage: active: true RedundantVisibilityModifierRule: active: false ReturnCount: active: false max: 4 excludedFunctions: 'equals' excludeLabeled: false excludeReturnFromLambda: true excludeGuardClauses: false SafeCast: active: true SerialVersionUIDInSerializableClass: active: true SpacingBetweenPackageAndImports: active: false ThrowsCount: active: true max: 2 TrailingWhitespace: active: false UnderscoresInNumericLiterals: active: false acceptableLength: 5 UnnecessaryAbstractClass: active: true ignoreAnnotated: ['dagger.Module'] UnnecessaryAnnotationUseSiteTarget: active: false UnnecessaryApply: active: true UnnecessaryFilter: active: false UnnecessaryInheritance: active: true UnnecessaryLet: active: false UnnecessaryParentheses: active: false UntilInsteadOfRangeTo: active: false UnusedImports: active: false UnusedPrivateClass: active: true UnusedPrivateMember: active: false allowedNames: '(_|ignored|expected|serialVersionUID)' UseArrayLiteralsInAnnotations: active: false UseCheckNotNull: active: false UseCheckOrError: active: false UseDataClass: active: false allowVars: false UseEmptyCounterpart: active: true UseIfEmptyOrIfBlank: active: true UseIfInsteadOfWhen: active: false UseIsNullOrEmpty: active: true UseOrEmpty: active: false UseRequire: active: false UseRequireNotNull: active: true UselessCallOnNotNull: active: true UtilityClassWithPublicConstructor: active: true VarCouldBeVal: active: true ignoreAnnotated: ['org.apache.maven.plugins.annotations.Parameter'] WildcardImport: active: true excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] excludeImports: ['com.saveourtool.diktat.ruleset.utils.*', 'java.util.*', 'kotlinx.android.synthetic.*', 'kotlinx.serialization'] ================================================ FILE: diktat-analysis.yml ================================================ # Common configuration - name: DIKTAT_COMMON enabled: true configuration: # put your package name here - it will be autofixed and checked domainName: com.saveourtool.diktat # testDirs: test # can also use chapter names here (not only numbers) # expected values: disabledChapters: "Naming, Comments, General, Variables, Functions, Classes" # or: "1, 2, 3, 4, 5, 6" disabledChapters: "" testDirs: test kotlinVersion: 2.1 srcDirectories: "main" # Checks that the Class/Enum/Interface name matches Pascal case - name: CLASS_NAME_INCORRECT enabled: true # Checks that CONSTANT (treated as const val from companion object or class level) is in non UPPER_SNAKE_CASE - name: CONSTANT_UPPERCASE enabled: true configuration: exceptionConstNames: "serialVersionUID" # Checks that enum value is in upper SNAKE_CASE or in PascalCase depending on the config. UPPER_SNAKE_CASE is the default, but can be changed by 'enumStyle' config - name: ENUM_VALUE enabled: true configuration: # Two options: SNAKE_CASE (default), PascalCase enumStyle: SNAKE_CASE # Checks that class which extends any Exception class has Exception suffix - name: EXCEPTION_SUFFIX enabled: true # Checks that file name has extension - name: FILE_NAME_INCORRECT enabled: true # Checks that file name matches class name, if it is only one class in file - name: FILE_NAME_MATCH_CLASS enabled: true # Checks that functions/methods which return boolean have special prefix like "is/should/e.t.c" - name: FUNCTION_BOOLEAN_PREFIX enabled: true configuration: allowedPrefixes: "" # A list of functions that return boolean and are allowed to use. Input is in a form "foo, bar". # Checks that function/method name is in lowerCamelCase - name: FUNCTION_NAME_INCORRECT_CASE enabled: true # Checks that generic name doesn't contain more than 1 letter (capital). It can be followed by numbers, example: T12, T - name: GENERIC_NAME enabled: true # Identifier length should be in range [2,64] except names that used in industry like {i, j} and 'e' for catching exceptions - name: IDENTIFIER_LENGTH enabled: true # Checks that the object matches PascalCase - name: OBJECT_NAME_INCORRECT enabled: true # Checks that package name is in correct (lower) case - name: PACKAGE_NAME_INCORRECT_CASE enabled: true # Checks that package name starts with the company's domain - name: PACKAGE_NAME_INCORRECT_PREFIX enabled: true # Checks that package name does not have incorrect symbols like underscore or non-ASCII letters/digits - name: PACKAGE_NAME_INCORRECT_SYMBOLS enabled: true # Checks that the path for a file matches with a package name - name: PACKAGE_NAME_INCORRECT_PATH enabled: true # Checks that package name is in the file - name: PACKAGE_NAME_MISSING enabled: true # Checks that variable does not have prefix (like mVariable or M_VARIABLE) - name: VARIABLE_HAS_PREFIX enabled: true # Checks that variable does not contain one single letter, only exceptions are fixed names that used in industry like {i, j} - name: VARIABLE_NAME_INCORRECT enabled: true # Checks that the name of variable is in lowerCamelCase and contains only ASCII letters - name: VARIABLE_NAME_INCORRECT_FORMAT enabled: true # Checks that functions have kdoc - name: MISSING_KDOC_ON_FUNCTION enabled: true # Checks that on file level internal or public class or function has missing KDoc - name: MISSING_KDOC_TOP_LEVEL enabled: true # Checks that accessible internal elements (protected, public, internal) in a class are documented - name: MISSING_KDOC_CLASS_ELEMENTS enabled: true # Checks that accessible method parameters are documented in KDoc - name: KDOC_WITHOUT_PARAM_TAG enabled: true # Checks that accessible method explicit return type is documented in KDoc - name: KDOC_WITHOUT_RETURN_TAG enabled: true # Checks that accessible method throw keyword is documented in KDoc - name: KDOC_WITHOUT_THROWS_TAG enabled: true # Checks that KDoc is not empty - name: KDOC_EMPTY_KDOC enabled: true # Checks that underscore is correctly used to split package naming - name: INCORRECT_PACKAGE_SEPARATOR enabled: true # Checks that code block doesn't contain kdoc comments - name: COMMENTED_BY_KDOC enabled: true # Checks that there is no @deprecated tag in kdoc - name: KDOC_NO_DEPRECATED_TAG enabled: true # Checks that there is no empty content in kdoc tags - name: KDOC_NO_EMPTY_TAGS enabled: true # Checks that there is only one space after kdoc tag - name: KDOC_WRONG_SPACES_AFTER_TAG enabled: true # Checks tags order in kDoc. `@param`, `@return`, `@throws` - name: KDOC_WRONG_TAGS_ORDER enabled: true # Checks that there is no newline of empty KDoc line (with leading asterisk) between `@param`, `@return`, `@throws` tags - name: KDOC_NO_NEWLINES_BETWEEN_BASIC_TAGS enabled: true # Checks that block of tags @param, @return, @throws is separated from previous part of KDoc by exactly one empty line - name: KDOC_NEWLINES_BEFORE_BASIC_TAGS enabled: true # Checks that special tags `@apiNote`, `@implNote`, `@implSpec` have exactly one empty line after - name: KDOC_NO_NEWLINE_AFTER_SPECIAL_TAGS enabled: true # Checks that KDoc does not contain single line with words 'return', 'get' or 'set' - name: KDOC_TRIVIAL_KDOC_ON_FUNCTION enabled: true # Checks that kdoc does not contain @author tag or date - name: KDOC_CONTAINS_DATE_OR_AUTHOR enabled: true configuration: versionRegex: \d+\.\d+\.\d+[-.\w\d]* # Checks that there is newline after header KDoc - name: HEADER_WRONG_FORMAT enabled: true # Checks that file with zero or >1 classes has header KDoc - name: HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE enabled: true # Checks that copyright exists on top of file and is properly formatted (as a block comment) - name: HEADER_MISSING_OR_WRONG_COPYRIGHT enabled: true configuration: isCopyrightMandatory: false copyrightText: '' # Checks that header kdoc is located before package directive - name: HEADER_NOT_BEFORE_PACKAGE enabled: true # Checks that file does not contain lines > maxSize - name: FILE_IS_TOO_LONG enabled: true configuration: # number of lines maxSize: '2000' # Checks that file does not contain commented out code - name: COMMENTED_OUT_CODE enabled: true # Checks that file does not contain only comments, imports and package directive - name: FILE_CONTAINS_ONLY_COMMENTS enabled: true # Orders imports alphabetically - name: FILE_UNORDERED_IMPORTS enabled: true configuration: # use logical imports grouping with sorting inside of a group useRecommendedImportsOrder: true # Checks that general order of code parts is right - name: FILE_INCORRECT_BLOCKS_ORDER enabled: true # Checks that there is exactly one line between code blocks - name: FILE_NO_BLANK_LINE_BETWEEN_BLOCKS enabled: true # Checks that there is no wildcard imports. Exception: allowedWildcards - name: FILE_WILDCARD_IMPORTS enabled: true configuration: # Allowed wildcards for imports (e.g. "import com.saveourtool.diktat.*, import org.jetbrains.kotlin.*") allowedWildcards: "kotlinx.serialization.*,com.saveourtool.diktat.ruleset.utils.*" # Checks unused imports - name: UNUSED_IMPORT enabled: true configuration: deleteUnusedImport: true # Checks that braces are used in if, else, when, for, do, and while statements. Exception: single line ternary operator statement - name: NO_BRACES_IN_CONDITIONALS_AND_LOOPS enabled: true # Checks that the declaration part of a class-like code structures (class/interface/etc.) is in the proper order - name: WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES enabled: true # Checks that properties with comments are separated by a blank line - name: BLANK_LINE_BETWEEN_PROPERTIES enabled: true # Checks top level order - name: TOP_LEVEL_ORDER enabled: true # Checks that non-empty code blocks with braces follow the K&R style (1TBS or OTBS style) - name: BRACES_BLOCK_STRUCTURE_ERROR enabled: true configuration: openBraceNewline: 'True' closeBraceNewline: 'True' # Checks that indentation is correct - name: WRONG_INDENTATION enabled: true configuration: # Is newline at the end of a file needed newlineAtEnd: true # If true: in parameter list when parameters are split by newline they are indented with two indentations instead of one extendedIndentOfParameters: false # If true: if first parameter in parameter list is on the same line as opening parenthesis, then other parameters can be aligned with it alignedParameters: true # If true, expression bodies which begin on a separate line are indented # using a continuation indent. The default is false. # # This flag is called CONTINUATION_INDENT_FOR_EXPRESSION_BODIES in IDEA and # ij_kotlin_continuation_indent_for_expression_bodies in .editorconfig. extendedIndentForExpressionBodies: false # If true: if expression is split by newline after operator like +/-/`*`, then the next line is indented with two indentations instead of one extendedIndentAfterOperators: true # If true: when dot qualified expression starts on a new line, this line will be indented with two indentations instead of one extendedIndentBeforeDot: false # The indentation size for each file indentationSize: 4 # Checks that there is no empty blocks in a file. # If allowEmptyBlocks is true, checks that it follows correct style (have a newline) - name: EMPTY_BLOCK_STRUCTURE_ERROR enabled: true configuration: # Whether a newline after `{` is required in an empty block styleEmptyBlockWithNewline: 'True' allowEmptyBlocks: 'False' # Checks that there is no more than one statement per line - name: MORE_THAN_ONE_STATEMENT_PER_LINE enabled: true # Checks that the line length is < lineLength parameter - name: LONG_LINE enabled: true configuration: lineLength: '180' # Checks that semicolons are not used at the end of a line - name: REDUNDANT_SEMICOLON enabled: true # Checks that line breaks follow code style guide: rule 3.6 - name: WRONG_NEWLINES enabled: true configuration: # If the number of parameters on one line is more than this threshold, all parameters will be placed on separate lines. maxParametersInOneLine: 2 # 3 by default. maxCallsInOneLine: 3 # Checks trailing comma - name: TRAILING_COMMA enabled: true configuration: # VALUE_ARGUMENT valueArgument: false # VALUE_PARAMETER valueParameter: false # REFERENCE_EXPRESSION indices: false # WHEN_CONDITION_WITH_EXPRESSION whenConditions: false # STRING_TEMPLATE collectionLiteral: false # TYPE_PROJECTION typeArgument: false # TYPE_PARAMETER typeParameter: false # DESTRUCTURING_DECLARATION_ENTRY destructuringDeclaration: false # Inspection that checks if a long dot qualified expression is used in condition or as an argument - name: COMPLEX_EXPRESSION enabled: true # Checks that there are not too many consecutive spaces in line - name: TOO_MANY_CONSECUTIVE_SPACES enabled: true configuration: # Maximum allowed number of consecutive spaces (not counting indentation) maxSpaces: '1' # Whether formatting for enums should be kept without checking saveInitialFormattingForEnums: false # Checks that blank lines are used correctly. # For example: triggers when there are too many blank lines between function declaration - name: TOO_MANY_BLANK_LINES enabled: true # Checks that usage of horizontal spaces doesn't violate code style guide - name: WRONG_WHITESPACE enabled: true # Checks that backticks (``) are not used in the identifier name, except the case when it is test method (marked with @Test annotation) - name: BACKTICKS_PROHIBITED enabled: true # all code blocks annotated with @Nested, @ParameterizedTest (JUnit 5) will # be ignored and not checked. ignoreAnnotated: [ Nested, ParameterizedTest, IndentationTest ] # Checks that a single line concatenation of strings is not used - name: STRING_CONCATENATION enabled: true # Checks that each when statement have else in the end - name: WHEN_WITHOUT_ELSE enabled: true # Checks that annotation is on a single line - name: ANNOTATION_NEW_LINE enabled: true # Checks that method annotated with `Preview` annotation is private and has Preview suffix - name: PREVIEW_ANNOTATION enabled: true # Checks that enum structure is correct: enum entries should be separated by comma and line break and last entry should have semicolon in the end. - name: ENUMS_SEPARATED enabled: true # Checks that value on integer or float constant is not too big - name: LONG_NUMERICAL_VALUES_SEPARATED enabled: true configuration: # Maximum number of digits which are not split maxNumberLength: '5' # Maximum number of digits between separators maxBlockLength: '3' # Checks magic number - name: MAGIC_NUMBER enabled: true configuration: # Ignore numbers from test ignoreTest: "true" # Ignore numbers ignoreNumbers: "-1, 1, 0, 2, 0U, 1U, 2U, -1L, 0L, 1L, 2L, 0UL, 1UL, 2UL" # Is ignore override hashCode function ignoreHashCodeFunction: "true" # Is ignore property ignorePropertyDeclaration: "false" # Is ignore local variable ignoreLocalVariableDeclaration: "false" # Is ignore value parameter ignoreValueParameter: "true" # Is ignore constant ignoreConstantDeclaration: "true" # Is ignore property in companion object ignoreCompanionObjectPropertyDeclaration: "true" # Is ignore numbers in enum ignoreEnums: "false" # Is ignore number in ranges ignoreRanges: "false" # Is ignore number in extension function ignoreExtensionFunctions: "false" # Is ignore number in pairs created using to ignorePairsCreatedUsingTo: "false" # Checks that order of enum values or constant property inside companion is correct - name: WRONG_DECLARATIONS_ORDER enabled: true configuration: # Whether enum members should be sorted alphabetically sortEnum: true # Whether class properties should be sorted alphabetically sortProperty: true # Checks that multiple modifiers sequence is in the correct order - name: WRONG_MULTIPLE_MODIFIERS_ORDER enabled: true # Checks that identifier has appropriate name (See table of rule 1.2 part 6) - name: CONFUSING_IDENTIFIER_NAMING enabled: true # Checks year in the copyright - name: WRONG_COPYRIGHT_YEAR enabled: true # Inspection that checks if local variables are declared close to the first usage site - name: LOCAL_VARIABLE_EARLY_DECLARATION enabled: true # Try to avoid initialize val by null (e.g. val a: Int? = null -> val a: Int = 0) - name: NULLABLE_PROPERTY_TYPE enabled: true # Inspection that checks if there is a blank line before kDoc and none after - name: WRONG_NEWLINES_AROUND_KDOC enabled: true # Inspection that checks if there is no blank lines before first comment - name: FIRST_COMMENT_NO_BLANK_LINE enabled: true # Inspection that checks if there are blank lines between code and comment and between code start token and comment's text - name: COMMENT_WHITE_SPACE enabled: true configuration: maxSpacesBeforeComment: 2 maxSpacesInComment: 1 # Inspection that checks if all comment's are inside if-else code blocks. Exception is general if comment - name: IF_ELSE_COMMENTS enabled: true # Type aliases provide alternative names for existing types when type's reference text is longer 25 chars - name: TYPE_ALIAS enabled: true configuration: typeReferenceLength: '25' # max length of type reference # Checks if casting can be omitted - name: SMART_CAST_NEEDED enabled: true # Checks that variables of generic types have explicit type declaration - name: GENERIC_VARIABLE_WRONG_DECLARATION enabled: true # Inspection that checks if string template has redundant curly braces - name: STRING_TEMPLATE_CURLY_BRACES enabled: true # Variables with `val` modifier - are immutable (read-only). Usage of such variables instead of `var` variables increases # robustness and readability of code, because `var` variables can be reassigned several times in the business logic. # This rule prohibits usage of `var`s as local variables - the only exception is accumulators and counters - name: SAY_NO_TO_VAR enabled: true # Inspection that checks if string template has redundant quotes - name: STRING_TEMPLATE_QUOTES enabled: true # Check if there are redundant nested if-statements, which could be collapsed into a single one by concatenating their conditions - name: COLLAPSE_IF_STATEMENTS enabled: true configuration: startCollapseFromNestedLevel: 2 # Checks that floating-point values are not used in arithmetic expressions - name: FLOAT_IN_ACCURATE_CALCULATIONS enabled: true # Checks that function length isn't too long - name: TOO_LONG_FUNCTION enabled: true configuration: maxFunctionLength: 35 # max length of function isIncludeHeader: false # count function's header # Warns if there are nested functions - name: AVOID_NESTED_FUNCTIONS enabled: true # Checks that lambda inside function parameters is in the end - name: LAMBDA_IS_NOT_LAST_PARAMETER enabled: true # Checks that function doesn't contains too many parameters - name: TOO_MANY_PARAMETERS enabled: true configuration: maxParameterListSize: '5' # max parameters size # Checks that function doesn't have too many nested blocks - name: NESTED_BLOCK enabled: true configuration: maxNestedBlockQuantity: '4' # Checks that function use default values, instead overloading - name: WRONG_OVERLOADING_FUNCTION_ARGUMENTS enabled: true # Checks that using runBlocking inside async block code - name: RUN_BLOCKING_INSIDE_ASYNC enabled: true # Checks that the long lambda has parameters - name: TOO_MANY_LINES_IN_LAMBDA enabled: true configuration: maxLambdaLength: 10 # max length of lambda without parameters # Checks that using unnecessary, custom label - name: CUSTOM_LABEL enabled: true # Checks that outer lambda has explicit parameter name - name: PARAMETER_NAME_IN_OUTER_LAMBDA enabled: true configuration: strictMode: true # don't let outer lambdas have `it` as parameter # Checks that property in constructor doesn't contain comment - name: KDOC_NO_CONSTRUCTOR_PROPERTY enabled: true configuration: isParamTagsForParameters: false # create param tags for parameters without val or var isParamTagsForPrivateProperties: false # create param tags for private properties isParamTagsForGenericTypes: false # create param tags for generic types # Checks that property in KDoc present in class - name: KDOC_EXTRA_PROPERTY enabled: true # There's a property in KDoc which is already present - name: KDOC_DUPLICATE_PROPERTY enabled: true # Checks that KDoc in constructor has property tag but with comment inside constructor - name: KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT enabled: true # if a class has single constructor, it should be converted to a primary constructor - name: SINGLE_CONSTRUCTOR_SHOULD_BE_PRIMARY enabled: true # Checks if class can be made as data class - name: USE_DATA_CLASS enabled: true # Checks that never use the name of a variable in the custom getter or setter - name: WRONG_NAME_OF_VARIABLE_INSIDE_ACCESSOR enabled: true # Checks that classes have only one init block - name: MULTIPLE_INIT_BLOCKS enabled: true # Checks that there are abstract functions in abstract class - name: CLASS_SHOULD_NOT_BE_ABSTRACT enabled: true # Checks if there are any trivial getters or setters - name: TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED enabled: true # Checks that no custom getters and setters are used for properties. It is a more wide rule than TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED # Kotlin compiler automatically generates `get` and `set` methods for properties and also lets the possibility to override it. # But in all cases it is very confusing when `get` and `set` are overridden for a developer who uses this particular class. # Developer expects to get the value of the property, but receives some unknown value and some extra side effect hidden by the custom getter/setter. # Use extra functions for it instead. - name: CUSTOM_GETTERS_SETTERS enabled: true # Checks if null-check was used explicitly (for example: if (a == null)) # Try to avoid explicit null checks (explicit comparison with `null`) # Kotlin is declared as [Null-safe](https://kotlinlang.org/docs/reference/null-safety.html) language. # But Kotlin architects wanted Kotlin to be fully compatible with Java, that's why `null` keyword was also introduced in Kotlin. # There are several code-structures that can be used in Kotlin to avoid null-checks. For example: `?:`, `.let {}`, `.also {}`, e.t.c - name: AVOID_NULL_CHECKS enabled: true # Checks if class instantiation can be wrapped in `apply` for better readability - name: COMPACT_OBJECT_INITIALIZATION enabled: true # Checks explicit supertype qualification - name: USELESS_SUPERTYPE enabled: true # Checks if extension function with the same signature don't have related classes - name: EXTENSION_FUNCTION_SAME_SIGNATURE enabled: true # Checks if there is empty primary constructor - name: EMPTY_PRIMARY_CONSTRUCTOR enabled: true # In case of not using field keyword in property accessors, # there should be explicit backing property with the name of real property # Example: val table get() {if (_table == null) ...} -> table should have _table - name: NO_CORRESPONDING_PROPERTY enabled: true # Checks if there is class/object that can be replace with extension function - name: AVOID_USING_UTILITY_CLASS enabled: true # If there is stateless class it is preferred to use object - name: OBJECT_IS_PREFERRED enabled: true # If there exists negated version of function you should prefer it instead of !functionCall - name: INVERSE_FUNCTION_PREFERRED enabled: true # Checks if class can be converted to inline class - name: INLINE_CLASS_CAN_BE_USED enabled: true # If file contains class, then it can't contain extension functions for the same class - name: EXTENSION_FUNCTION_WITH_CLASS enabled: true # Check if kts script contains other functions except run code - name: RUN_IN_SCRIPT enabled: true # Check if boolean expression can be simplified - name: COMPLEX_BOOLEAN_EXPRESSION enabled: true # Check if range can replace with until or `rangeTo` function with range - name: CONVENTIONAL_RANGE enabled: true configuration: isRangeToIgnore: false # Check if there is a call of print()\println() or console.log(). Assumption that it's a debug print - name: DEBUG_PRINT enabled: true # Check that typealias name is in PascalCase - name: TYPEALIAS_NAME_INCORRECT_CASE enabled: true # Should change property length - 1 to property lastIndex - name: USE_LAST_INDEX enabled: true # Only properties from the primary constructor should be documented in a @property tag in class KDoc - name: KDOC_NO_CLASS_BODY_PROPERTIES_IN_HEADER enabled: true ================================================ FILE: diktat-api/build.gradle.kts ================================================ plugins { id("com.saveourtool.diktat.buildutils.kotlin-jvm-configuration") id("com.saveourtool.diktat.buildutils.code-quality-convention") id("com.saveourtool.diktat.buildutils.publishing-default-configuration") alias(libs.plugins.kotlin.plugin.serialization) } project.description = "This module builds diktat-api" dependencies { implementation(libs.kotlin.compiler.embeddable) implementation(libs.kotlinx.serialization.core) } val generateDiktatVersionFile by tasks.registering { val outputDir = layout.buildDirectory.dir("generated/src").get().asFile val versionsFile = outputDir.resolve("generated/DiktatVersion.kt") val diktatVersion = version.toString() inputs.property("diktat version", diktatVersion) outputs.dir(outputDir) doFirst { versionsFile.parentFile.mkdirs() versionsFile.writeText( """ package generated const val DIKTAT_VERSION = "$diktatVersion" """.trimIndent() ) } } kotlin.sourceSets.getByName("main") { kotlin.srcDir( generateDiktatVersionFile.map { it.outputs.files.singleFile } ) } sequenceOf("diktatFix", "diktatCheck").forEach { diktatTaskName -> tasks.findByName(diktatTaskName)?.dependsOn( generateDiktatVersionFile, tasks.named("compileKotlin"), tasks.named("processResources"), ) } ================================================ FILE: diktat-api/src/main/kotlin/com/saveourtool/diktat/Constants.kt ================================================ /** * This file contains common constants for Diktat */ package com.saveourtool.diktat /** * Common application name, that is used in plugins and can be used to Suppress all diktat inspections on the * particular code block with @Suppress("diktat") */ const val DIKTAT = "diktat" /** * Default file name for config file */ const val DIKTAT_ANALYSIS_CONF = "diktat-analysis.yml" ================================================ FILE: diktat-api/src/main/kotlin/com/saveourtool/diktat/DiktatProcessor.kt ================================================ package com.saveourtool.diktat import com.saveourtool.diktat.api.DiktatCallback import java.nio.file.Path /** * Processor to run `diktat` */ interface DiktatProcessor { /** * Run `diktat fix` on provided [file] using [callback] for detected errors and returned formatted file content. * * @param file * @param callback * @return result of `diktat fix` */ fun fix(file: Path, callback: DiktatCallback): String /** * Run `diktat fix` on provided [code] using [callback] for detected errors and returned formatted code. * * @param code * @param virtualPath a path which should be taken into account in processing of [code] * @param callback * @return result of `diktat fix` */ fun fix( code: String, virtualPath: Path?, callback: DiktatCallback, ): String /** * Run `diktat check` on provided [file] using [callback] for detected errors. * * @param file * @param callback */ fun check(file: Path, callback: DiktatCallback) /** * Run `diktat check` on provided [code] using [callback] for detected errors. * * @param code * @param virtualPath a path which should be taken into account in processing of [code] * @param callback */ fun check( code: String, virtualPath: Path?, callback: DiktatCallback, ) } ================================================ FILE: diktat-api/src/main/kotlin/com/saveourtool/diktat/DiktatProcessorFactory.kt ================================================ package com.saveourtool.diktat import com.saveourtool.diktat.api.DiktatRuleSet /** * A factory to create [DiktatProcessor] using [DiktatRuleSet] */ @FunctionalInterface interface DiktatProcessorFactory : Function1 { /** * @param diktatRuleSet * @return created [DiktatProcessor] using [DiktatRuleSet] */ override operator fun invoke(diktatRuleSet: DiktatRuleSet): DiktatProcessor } ================================================ FILE: diktat-api/src/main/kotlin/com/saveourtool/diktat/DiktatRunner.kt ================================================ package com.saveourtool.diktat import com.saveourtool.diktat.api.DiktatProcessorListener import com.saveourtool.diktat.api.DiktatProcessorListener.Companion.countErrorsAsProcessorListener import com.saveourtool.diktat.api.DiktatReporter import java.nio.file.Path import java.util.concurrent.atomic.AtomicInteger import kotlin.io.path.readText import kotlin.io.path.writeText private typealias RunAction = (DiktatProcessor, DiktatProcessorListener) -> Unit /** * A runner for diktat on bunch of files using baseline and reporter * * @param diktatProcessor * @property diktatReporter */ data class DiktatRunner( private val diktatProcessor: DiktatProcessor, val diktatReporter: DiktatReporter, ) { private fun doRun( args: DiktatRunnerArguments, runAction: RunAction, ): Int { val errorCounter = AtomicInteger() runAction( diktatProcessor, DiktatProcessorListener( args.loggingListener, diktatReporter, errorCounter.countErrorsAsProcessorListener() ), ) return errorCounter.get() } /** * Run `diktat fix` for all [DiktatRunnerArguments.files]. * * @param args * @param fileUpdateNotifier notifier about updated files * @return count of detected errors */ fun fixAll( args: DiktatRunnerArguments, fileUpdateNotifier: (Path) -> Unit, ): Int = doRun(args) { processor, listener -> listener.beforeAll(args.files) args.files.forEach { file -> listener.before(file) val formattedContent = processor.fix(file) { error, isCorrected -> listener.onError(file, error, isCorrected) } val fileContent = file.readText(Charsets.UTF_8) if (fileContent != formattedContent) { fileUpdateNotifier(file) file.writeText(formattedContent, Charsets.UTF_8) } listener.after(file) } listener.afterAll() } /** * Run `diktat check` for all [DiktatRunnerArguments.files]. * * @param args * @return count of detected errors */ fun checkAll( args: DiktatRunnerArguments, ): Int = doRun(args) { processor, listener -> listener.beforeAll(args.files) args.files.forEach { file -> listener.before(file) processor.check(file) { error, isCorrected -> listener.onError(file, error, isCorrected) } listener.after(file) } listener.afterAll() } } ================================================ FILE: diktat-api/src/main/kotlin/com/saveourtool/diktat/DiktatRunnerArguments.kt ================================================ package com.saveourtool.diktat import com.saveourtool.diktat.api.DiktatProcessorListener import com.saveourtool.diktat.api.DiktatReporterCreationArguments import java.io.InputStream import java.nio.file.Path /** * Arguments for [DiktatRunner] * * @property configInputStream an input stream with config to load Diktat's rules or null to use default configs * @property sourceRootDir a common root dir for all provided [files] * @property files a collection of files which needs to be fixed * @property baselineFile an optional path to file with baseline * @property reporterArgsList list of arguments to create reporters to report result * @property loggingListener listener to log diktat runner phases, [DiktatProcessorListener.empty] by default */ data class DiktatRunnerArguments( val configInputStream: InputStream?, val sourceRootDir: Path?, val files: Collection, val baselineFile: Path?, val reporterArgsList: List = emptyList(), val loggingListener: DiktatProcessorListener = DiktatProcessorListener.empty, ) ================================================ FILE: diktat-api/src/main/kotlin/com/saveourtool/diktat/DiktatRunnerFactory.kt ================================================ package com.saveourtool.diktat import com.saveourtool.diktat.api.DiktatBaseline import com.saveourtool.diktat.api.DiktatBaseline.Companion.skipKnownErrors import com.saveourtool.diktat.api.DiktatBaselineFactory import com.saveourtool.diktat.api.DiktatProcessorListener import com.saveourtool.diktat.api.DiktatReporter import com.saveourtool.diktat.api.DiktatReporterFactory import com.saveourtool.diktat.api.DiktatRuleConfig import com.saveourtool.diktat.api.DiktatRuleConfigReader import com.saveourtool.diktat.api.DiktatRuleSet import com.saveourtool.diktat.api.DiktatRuleSetFactory import java.nio.file.Path /** * A factory to create [DiktatRunner] * * @param diktatRuleConfigReader a reader for [DiktatRuleConfig] * @param diktatRuleSetFactory a factory for [DiktatRuleSet] * @param diktatProcessorFactory a factory for [DiktatProcessor] * @param diktatBaselineFactory a factory for [DiktatBaseline] * @property diktatReporterFactory a factory for [DiktatReporter] */ class DiktatRunnerFactory( private val diktatRuleConfigReader: DiktatRuleConfigReader, private val diktatRuleSetFactory: DiktatRuleSetFactory, private val diktatProcessorFactory: DiktatProcessorFactory, private val diktatBaselineFactory: DiktatBaselineFactory, val diktatReporterFactory: DiktatReporterFactory, ) : Function1 { /** * @param args * @return an instance of [DiktatRunner] created using [args] */ override fun invoke(args: DiktatRunnerArguments): DiktatRunner { val diktatRuleConfigs = args.configInputStream?.let { diktatRuleConfigReader(it) }.orEmpty() val diktatRuleSet = diktatRuleSetFactory(diktatRuleConfigs) val processor = diktatProcessorFactory(diktatRuleSet) val (baseline, baselineGenerator) = resolveBaseline(args.baselineFile, args.sourceRootDir) val reporter = args.reporterArgsList .map { diktatReporterFactory(it) } .let { DiktatReporter.union(it) } return DiktatRunner( diktatProcessor = processor, diktatReporter = DiktatReporter(reporter.skipKnownErrors(baseline), baselineGenerator), ) } private fun resolveBaseline( baselineFile: Path?, sourceRootDir: Path?, ): Pair = baselineFile ?.let { diktatBaselineFactory.tryToLoad(it, sourceRootDir) } ?.let { it to DiktatProcessorListener.empty } ?: run { val baselineGenerator = baselineFile?.let { diktatBaselineFactory.generator(it, sourceRootDir) } ?: DiktatProcessorListener.empty DiktatBaseline.empty to baselineGenerator } } ================================================ FILE: diktat-api/src/main/kotlin/com/saveourtool/diktat/api/DiktatBaseline.kt ================================================ package com.saveourtool.diktat.api import com.saveourtool.diktat.util.DiktatProcessorListenerWrapper import java.nio.file.Path /** * A base interface for Baseline */ fun interface DiktatBaseline { /** * @param file * @return a set of [DiktatError] found in baseline by [file] */ fun errorsByFile(file: Path): Set companion object { /** * Empty [DiktatBaseline] */ val empty: DiktatBaseline = DiktatBaseline { _ -> emptySet() } /** * @param baseline * @return wrapped [DiktatProcessorListener] which skips known errors based on [baseline] */ fun DiktatProcessorListener.skipKnownErrors(baseline: DiktatBaseline): DiktatProcessorListener = object : DiktatProcessorListenerWrapper( this@skipKnownErrors ) { override fun doOnError( wrappedValue: DiktatProcessorListener, file: Path, error: DiktatError, isCorrected: Boolean ) { if (!baseline.errorsByFile(file).contains(error)) { wrappedValue.onError(file, error, isCorrected) } } override fun doBeforeAll(wrappedValue: DiktatProcessorListener, files: Collection) = wrappedValue.beforeAll(files) override fun doBefore(wrappedValue: DiktatProcessorListener, file: Path) = wrappedValue.before(file) override fun doAfter(wrappedValue: DiktatProcessorListener, file: Path) = wrappedValue.after(file) override fun doAfterAll(wrappedValue: DiktatProcessorListener) = wrappedValue.afterAll() } } } ================================================ FILE: diktat-api/src/main/kotlin/com/saveourtool/diktat/api/DiktatBaselineFactory.kt ================================================ package com.saveourtool.diktat.api import java.nio.file.Path /** * A factory to load or generate [DiktatBaseline] */ interface DiktatBaselineFactory { /** * @param baselineFile * @param sourceRootDir a dir to detect relative path for processing files * @return Loaded [DiktatBaseline] from [baselineFile] or null if it gets an error in loading */ fun tryToLoad( baselineFile: Path, sourceRootDir: Path?, ): DiktatBaseline? /** * @param baselineFile * @param sourceRootDir a dir to detect relative path for processing files * @return [DiktatProcessorListener] which generates baseline in [baselineFile] */ fun generator( baselineFile: Path, sourceRootDir: Path?, ): DiktatProcessorListener } ================================================ FILE: diktat-api/src/main/kotlin/com/saveourtool/diktat/api/DiktatCallback.kt ================================================ package com.saveourtool.diktat.api /** * Callback for diktat process */ @FunctionalInterface fun interface DiktatCallback : Function2 { /** * Performs this callback on the given [error] taking into account [isCorrected] flag. * * @param error the error found by diktat * @param isCorrected true if the error fixed by diktat */ override fun invoke(error: DiktatError, isCorrected: Boolean) companion object { /** * [DiktatCallback] that does nothing */ val empty: DiktatCallback = DiktatCallback { _, _ -> } } } ================================================ FILE: diktat-api/src/main/kotlin/com/saveourtool/diktat/api/DiktatError.kt ================================================ package com.saveourtool.diktat.api /** * Error found by `diktat` * * @property line line number (one-based) * @property col column number (one-based) * @property ruleId rule id * @property detail error message * @property canBeAutoCorrected true if the found error can be fixed */ data class DiktatError( val line: Int, val col: Int, val ruleId: String, val detail: String, val canBeAutoCorrected: Boolean = false, ) ================================================ FILE: diktat-api/src/main/kotlin/com/saveourtool/diktat/api/DiktatErrorEmitter.kt ================================================ package com.saveourtool.diktat.api /** * The **file-specific** error emitter, initialized and used in [DiktatRule] implementations. * * Since the file is indirectly a part of the state of a `DiktatRule`, the same * `DiktatRule` instance should **never be re-used** to check more than a single * file, or confusing effects (incl. race conditions) will occur. * * @see DiktatRule */ fun interface DiktatErrorEmitter : Function3 { /** * @param offset * @param errorMessage * @param canBeAutoCorrected */ override fun invoke( offset: Int, errorMessage: String, canBeAutoCorrected: Boolean ) } ================================================ FILE: diktat-api/src/main/kotlin/com/saveourtool/diktat/api/DiktatProcessorListener.kt ================================================ package com.saveourtool.diktat.api import com.saveourtool.diktat.util.DiktatProcessorListenerWrapper import java.nio.file.Path import java.util.concurrent.atomic.AtomicInteger private typealias DiktatProcessorListenerIterable = Iterable /** * A listener for [com.saveourtool.diktat.DiktatProcessor] */ interface DiktatProcessorListener { /** * Called once, before [com.saveourtool.diktat.DiktatProcessor] starts process a bunch of files. * * @param files */ fun beforeAll(files: Collection): Unit = Unit /** * Called before each file when [com.saveourtool.diktat.DiktatProcessor] starts to process it. * * @param file */ fun before(file: Path): Unit = Unit /** * Called on each error when [com.saveourtool.diktat.DiktatProcessor] detects such one. * * @param file * @param error * @param isCorrected */ fun onError( file: Path, error: DiktatError, isCorrected: Boolean ): Unit = Unit /** * Called after each file when [com.saveourtool.diktat.DiktatProcessor] finished to process it. * * @param file */ fun after(file: Path): Unit = Unit /** * Called once, after the processing of [com.saveourtool.diktat.DiktatProcessor] finished. */ fun afterAll(): Unit = Unit companion object { /** * An instance of [DiktatProcessorListener] that does nothing */ @Suppress("EMPTY_BLOCK_STRUCTURE_ERROR") val empty = object : DiktatProcessorListener {} /** * @param listeners * @return a single [DiktatProcessorListener] which uses all provided [listeners] */ fun union(listeners: DiktatProcessorListenerIterable): DiktatProcessorListener = object : DiktatProcessorListenerWrapper(listeners) { override fun doBeforeAll(wrappedValue: DiktatProcessorListenerIterable, files: Collection) = wrappedValue.forEach { it.beforeAll(files) } override fun doBefore(wrappedValue: DiktatProcessorListenerIterable, file: Path) = wrappedValue.forEach { it.before(file) } override fun doOnError( wrappedValue: DiktatProcessorListenerIterable, file: Path, error: DiktatError, isCorrected: Boolean ) = wrappedValue.forEach { it.onError(file, error, isCorrected) } override fun doAfter(wrappedValue: DiktatProcessorListenerIterable, file: Path) = wrappedValue.forEach { it.after(file) } override fun doAfterAll(wrappedValue: DiktatProcessorListenerIterable) = wrappedValue.forEach(DiktatProcessorListener::afterAll) } /** * @param listeners * @return a single [DiktatProcessorListener] which uses all provided [listeners] */ operator fun invoke(vararg listeners: DiktatProcessorListener): DiktatProcessorListener = union(listeners.asIterable()) /** * @return An implementation of [DiktatProcessorListener] which counts [DiktatError]s */ fun AtomicInteger.countErrorsAsProcessorListener(): DiktatProcessorListener = object : DiktatProcessorListener { override fun onError( file: Path, error: DiktatError, isCorrected: Boolean ) { if (!isCorrected) { incrementAndGet() } } } } } ================================================ FILE: diktat-api/src/main/kotlin/com/saveourtool/diktat/api/DiktatReporterCreationArguments.kt ================================================ /** * Contains a base interface and implementations for a container with arguments to create a reporter */ package com.saveourtool.diktat.api import java.io.OutputStream import java.nio.file.Path /** * Arguments to create [DiktatReporter] using [DiktatReporterFactory] */ sealed interface DiktatReporterCreationArguments { /** * Type of [DiktatReporter] which needs to be created */ val reporterType: DiktatReporterType /** * Output for [DiktatReporter] */ val outputStream: OutputStream /** * Should [outputStream] be closed af the end of [DiktatReporter] */ val closeOutputStreamAfterAll: Boolean /** * Directory to base source root to report relative paths in [DiktatReporter] */ val sourceRootDir: Path? companion object { /** * @param reporterType type of [DiktatReporter] * @param outputStream stdout will be used when it's empty * @param sourceRootDir a dir to detect relative path for processing files * @param colorNameInPlain a color name for colorful output which is applicable for plain ([DiktatReporterType.PLAIN]) reporter only, * `null` means to disable colorization. * @param groupByFileInPlain a flag `groupByFile` which is applicable for plain ([DiktatReporterType.PLAIN]) reporter only. * @return created [DiktatReporter] */ operator fun invoke( reporterType: DiktatReporterType, outputStream: OutputStream?, sourceRootDir: Path?, colorNameInPlain: String? = null, groupByFileInPlain: Boolean? = null, ): DiktatReporterCreationArguments { val (outputStreamOrStdout, closeOutputStreamAfterAll) = outputStream?.let { it to true } ?: (System.`out` to false) return if (reporterType == DiktatReporterType.PLAIN) { PlainDiktatReporterCreationArguments( outputStreamOrStdout, closeOutputStreamAfterAll, sourceRootDir, colorNameInPlain, groupByFileInPlain ) } else { require(colorNameInPlain == null) { "colorization is applicable only for plain reporter" } require(groupByFileInPlain == null) { "groupByFile is applicable only for plain reporter" } DiktatReporterCreationArgumentsImpl( reporterType, outputStreamOrStdout, closeOutputStreamAfterAll, sourceRootDir ) } } } } /** * Implementation of [DiktatReporterCreationArguments] for [DiktatReporterType.PLAIN] * * @property outputStream * @property closeOutputStreamAfterAll * @property sourceRootDir * @property colorName name of color for colorful output, `null` means to disable colorization. * @property groupByFile */ data class PlainDiktatReporterCreationArguments( override val outputStream: OutputStream, override val closeOutputStreamAfterAll: Boolean, override val sourceRootDir: Path?, val colorName: String? = null, val groupByFile: Boolean? = null, ) : DiktatReporterCreationArguments { override val reporterType: DiktatReporterType = DiktatReporterType.PLAIN } /** * @property reporterType * @property outputStream * @property closeOutputStreamAfterAll * @property sourceRootDir */ private data class DiktatReporterCreationArgumentsImpl( override val reporterType: DiktatReporterType, override val outputStream: OutputStream, override val closeOutputStreamAfterAll: Boolean, override val sourceRootDir: Path?, ) : DiktatReporterCreationArguments ================================================ FILE: diktat-api/src/main/kotlin/com/saveourtool/diktat/api/DiktatReporterFactory.kt ================================================ package com.saveourtool.diktat.api typealias DiktatReporter = DiktatProcessorListener /** * A factory to create [DiktatReporter] */ interface DiktatReporterFactory : Function1 { /** * Names of color for plain output */ val colorNamesInPlain: Set /** * @param args * @return created [DiktatReporter] */ override operator fun invoke( args: DiktatReporterCreationArguments, ): DiktatReporter } ================================================ FILE: diktat-api/src/main/kotlin/com/saveourtool/diktat/api/DiktatReporterType.kt ================================================ package com.saveourtool.diktat.api /** * @property id * @property extension */ enum class DiktatReporterType( val id: String, val extension: String, ) { CHECKSTYLE("checkstyle", "xml"), HTML("html", "html"), JSON("json", "json"), PLAIN("plain", "txt"), PLAIN_GROUP_BY_FILE("plain-group-by-file", "txt"), SARIF("sarif", "sarif"), ; } ================================================ FILE: diktat-api/src/main/kotlin/com/saveourtool/diktat/api/DiktatRule.kt ================================================ package com.saveourtool.diktat.api import org.jetbrains.kotlin.com.intellij.lang.ASTNode /** * This is a base interface for diktat's rules */ interface DiktatRule : Function3 { /** * A unique ID of this rule. */ val id: String /** * This method is going to be executed for each node in AST (in DFS fashion). * * @param node AST node * @param autoCorrect indicates whether rule should attempt autocorrection * @param emitter a way for rule to notify about a violation (lint error) */ override fun invoke( node: ASTNode, autoCorrect: Boolean, emitter: DiktatErrorEmitter, ) } ================================================ FILE: diktat-api/src/main/kotlin/com/saveourtool/diktat/api/DiktatRuleConfig.kt ================================================ package com.saveourtool.diktat.api import kotlinx.serialization.Serializable /** * Configuration of individual [DiktatRule] * * @property name name of the rule * @property enabled * @property configuration a map of strings with configuration options * @property ignoreAnnotated if a code block is marked with these annotations - it will not be checked by this rule */ @Serializable data class DiktatRuleConfig( val name: String, val enabled: Boolean = true, val configuration: Map = emptyMap(), val ignoreAnnotated: Set = emptySet(), ) /** * Finds [DiktatRuleConfig] for particular [DiktatRuleNameAware] object. * * @param rule a [DiktatRuleNameAware] which configuration will be returned * @return [DiktatRuleConfig] for a particular rule if it is found, else null */ fun List.findByRuleName(rule: DiktatRuleNameAware): DiktatRuleConfig? = this.find { it.name == rule.ruleName() } /** * checking if in yml config particular rule is enabled or disabled * (!) the default value is "true" (in case there is no config specified) * * @param rule a [DiktatRuleNameAware] which is being checked * @return true if rule is enabled in configuration, else false */ fun List.isRuleEnabled(rule: DiktatRuleNameAware): Boolean { val ruleMatched = findByRuleName(rule) return ruleMatched?.enabled ?: true } /** * @param rule diktat inspection * @param annotations set of annotations that are annotating a block of code * @return true if the code block is marked with annotation that is in `ignored list` in the rule */ fun List.isAnnotatedWithIgnoredAnnotation(rule: DiktatRuleNameAware, annotations: Set): Boolean = findByRuleName(rule) ?.ignoreAnnotated ?.map { it.trim() } ?.map { it.trim('"') } ?.intersect(annotations) ?.isNotEmpty() ?: false ================================================ FILE: diktat-api/src/main/kotlin/com/saveourtool/diktat/api/DiktatRuleConfigReader.kt ================================================ package com.saveourtool.diktat.api import java.io.InputStream /** * A reader for [DiktatRuleConfig] */ fun interface DiktatRuleConfigReader : Function1> { /** * @param inputStream * @return parsed [DiktatRuleConfig]s */ override operator fun invoke(inputStream: InputStream): List } ================================================ FILE: diktat-api/src/main/kotlin/com/saveourtool/diktat/api/DiktatRuleNameAware.kt ================================================ package com.saveourtool.diktat.api /** * This interface represents *name* of individual inspection in rule set. */ interface DiktatRuleNameAware { /** * @return name of this [DiktatRule] */ fun ruleName(): String } ================================================ FILE: diktat-api/src/main/kotlin/com/saveourtool/diktat/api/DiktatRuleSet.kt ================================================ package com.saveourtool.diktat.api /** * A group of [DiktatRule]'s as a single set. * * @property rules diktat rules. */ data class DiktatRuleSet( val rules: List ) ================================================ FILE: diktat-api/src/main/kotlin/com/saveourtool/diktat/api/DiktatRuleSetFactory.kt ================================================ package com.saveourtool.diktat.api /** * A factory which creates a [DiktatRuleSet]. */ fun interface DiktatRuleSetFactory : Function1, DiktatRuleSet> { /** * @param rulesConfig all configurations for rules * @return the default instance of [DiktatRuleSet] */ override operator fun invoke(rulesConfig: List): DiktatRuleSet } ================================================ FILE: diktat-api/src/main/kotlin/com/saveourtool/diktat/common/config/rules/LegacyAliases.kt ================================================ /** * Contains typealias for legacy support */ package com.saveourtool.diktat.common.config.rules import com.saveourtool.diktat.api.DiktatRuleConfig import com.saveourtool.diktat.api.DiktatRuleNameAware const val DIKTAT_CONF_PROPERTY = "diktat.config.path" /** * this constant will be used everywhere in the code to mark usage of Diktat ruleset * * Should be removed from Diktat's code and should be presented only in `diktat-ruleset` */ const val DIKTAT_RULE_SET_ID = "diktat-ruleset" typealias RulesConfig = DiktatRuleConfig typealias Rule = DiktatRuleNameAware ================================================ FILE: diktat-api/src/main/kotlin/com/saveourtool/diktat/util/DiktatProcessorListenerWrapper.kt ================================================ package com.saveourtool.diktat.util import com.saveourtool.diktat.api.DiktatError import com.saveourtool.diktat.api.DiktatProcessorListener import java.nio.file.Path /** * A common wrapper for [DiktatProcessorListener] * * @property wrappedValue */ open class DiktatProcessorListenerWrapper( val wrappedValue: T, ) : DiktatProcessorListener { override fun beforeAll(files: Collection): Unit = doBeforeAll(wrappedValue, files) /** * Called once, before [com.saveourtool.diktat.DiktatProcessor] starts process a bunch of files. * * @param wrappedValue * @param files */ protected open fun doBeforeAll(wrappedValue: T, files: Collection): Unit = Unit override fun before(file: Path): Unit = doBefore(wrappedValue, file) /** * Called before each file when [com.saveourtool.diktat.DiktatProcessor] starts to process it. * * @param wrappedValue * @param file */ protected open fun doBefore(wrappedValue: T, file: Path): Unit = Unit override fun onError( file: Path, error: DiktatError, isCorrected: Boolean ): Unit = doOnError(wrappedValue, file, error, isCorrected) /** * Called on each error when [com.saveourtool.diktat.DiktatProcessor] detects such one. * * @param wrappedValue * @param file * @param error * @param isCorrected */ protected open fun doOnError( wrappedValue: T, file: Path, error: DiktatError, isCorrected: Boolean ): Unit = Unit override fun after(file: Path): Unit = doAfter(wrappedValue, file) /** * Called after each file when [com.saveourtool.diktat.DiktatProcessor] finished to process it. * * @param wrappedValue * @param file */ protected open fun doAfter(wrappedValue: T, file: Path): Unit = Unit override fun afterAll(): Unit = doAfterAll(wrappedValue) /** * Called once, after the processing of [com.saveourtool.diktat.DiktatProcessor] finished. * * @param wrappedValue */ protected open fun doAfterAll(wrappedValue: T): Unit = Unit companion object { /** * @return wrapped value [T] if it's possible */ inline fun DiktatProcessorListener.tryUnwrap(): T? = (this as? DiktatProcessorListenerWrapper<*>) ?.wrappedValue ?.let { it as? T } /** * @return wrapped value [T] or an error */ inline fun DiktatProcessorListener.unwrap(): T = tryUnwrap() ?: error("Unsupported wrapper of ${DiktatProcessorListener::class.java.simpleName} to ${T::class.simpleName}: ${this.javaClass.name}") } } ================================================ FILE: diktat-api/src/main/kotlin/com/saveourtool/diktat/util/FileUtils.kt ================================================ /** * Utility methods to work with file paths. */ package com.saveourtool.diktat.util import java.nio.file.Path import kotlin.io.path.extension private const val KOTLIN_EXTENSION = "kt" private const val KOTLIN_SCRIPT_EXTENSION = KOTLIN_EXTENSION + "s" /** * Checks if [this] [String] is a name of a kotlin script file by checking whether file extension equals 'kts' * * @return true if this is a kotlin script file name, false otherwise */ fun String.isKotlinScript() = endsWith(".$KOTLIN_SCRIPT_EXTENSION", true) /** * Check if [this] [Path] is a kotlin script by checking whether an extension equals to 'kts' * * @return true if this is a kotlin script file name, false otherwise */ fun Path.isKotlinScript() = this.extension.lowercase() == KOTLIN_SCRIPT_EXTENSION /** * Check if [this] [Path] is a kotlin code or script by checking whether an extension equals to `kt` or 'kts' * * @return true if this is a kotlin code or script file name, false otherwise */ fun Path.isKotlinCodeOrScript() = this.extension.lowercase() in setOf(KOTLIN_EXTENSION, KOTLIN_SCRIPT_EXTENSION) ================================================ FILE: diktat-cli/README.md ================================================ # _diktat-cli_, the command-line client for [_diktat_](https://github.com/saveourtool/diktat) --- # Table of contents 1. [Features](#features) 2. [Usage](#usage) 3. [Option summary](#option-summary) 4. [Exit code](#exit-codes) --- ## Features * Self-executable JAR in _UNIX Shell_ (requires installed _JAVA_) * BSD-compatible * Also works in Windows (_Git Bash_, _Cygwin_, or _MSys2_) via the dedicated _diktat.cmd_ * Can be used as a regular uber JAR ## Usage ```shell diktat [OPTION]... [FILE]... ``` ## Option summary | Command-line switch | Meaning | |:-------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `-c CONFIG`, `--config=CONFIG` | Specify the location of the YAML configuration file. By default, `diktat-analysis.yml` in the current directory is used. | | `-m MODE`, `--mode MODE` | Mode of `diktat` controls that `diktat` fixes or only finds any deviations from the code style. | | `-r REPORTER`, `--reporter=REPORTER` | The reporter to use to errors to `output`, one of: `plain`, `plain_group_by_file`, `json`, `sarif`, `checkstyle`, `html`. | | `-o OUTPUT`, `--output=OUTPUT` | Redirect the reporter output to a file. Must be provided when the reporter is provided. | | `--group-by-file` | A flag to group found errors by files. | | `--color COLOR` | Colorize the output, one of: `BLACK`, `RED`, `GREEN`, `YELLOW`, `BLUE`, `MAGENTA`, `CYAN`, `LIGHT_GRAY`, `DARK_GRAY`, `LIGHT_RED`, `LIGHT_GREEN`, `LIGHT_YELLOW`, `LIGHT_BLUE`, `LIGHT_MAGENTA`, `LIGHT_CYAN`, `WHITE` | | `-l`, `--log-level` | Control the log level. | | `-h`, `--help` | Display the help text and exit. | | `-l`, `--license` | Display the license and exit. | | `-v`, `--verbose` | Enable the verbose output. | | `-V`, `--version` | Output version information and exit. | ## Exit codes | Exit code | Meaning | |:----------|:--------------------------------------------------------------------------------------------------------------------| | 0 | _diKTat_ found no errors in your code | | 1 | _diKTat_ reported some errors in your code | | 2 | The JVM was not found (probably, you need to set up the JVM explicitly, using the `JAVA_HOME` environment variable) | | 3 | Incompatible _Bash_ version | ================================================ FILE: diktat-cli/build.gradle.kts ================================================ import com.saveourtool.diktat.buildutils.configurePublications import com.github.jengelman.gradle.plugins.shadow.ShadowExtension import org.jetbrains.kotlin.incremental.createDirectory @Suppress("DSL_SCOPE_VIOLATION", "RUN_IN_SCRIPT") // https://github.com/gradle/gradle/issues/22797 plugins { id("com.saveourtool.diktat.buildutils.kotlin-jvm-configuration") id("com.saveourtool.diktat.buildutils.code-quality-convention") id("com.saveourtool.diktat.buildutils.publishing-configuration") alias(libs.plugins.kotlin.plugin.serialization) alias(libs.plugins.shadow) } project.description = "This module builds diktat-cli to run diktat as CLI using ktlint" dependencies { implementation(projects.diktatRunner) implementation(libs.kotlinx.cli) implementation(libs.kotlinx.serialization.core) implementation(libs.kotlin.logging) implementation(libs.slf4j.api) implementation(libs.log4j2.core) implementation(libs.log4j2.slf4j2) testImplementation(projects.diktatKtlintEngine) testImplementation(projects.diktatRules) testImplementation(projects.diktatCommonTest) testImplementation(libs.kaml) testImplementation(libs.junit.jupiter) testImplementation(libs.junit.platform.suite) testImplementation(libs.assertj.core) } val addLicenseTask: TaskProvider = tasks.register("addLicense") { val licenseFile = rootProject.file("LICENSE") val outputDir = layout.buildDirectory .dir("generated/src") .get() .asFile inputs.file(licenseFile) outputs.dir(outputDir) doLast { licenseFile.copyTo( outputDir.resolve("META-INF").resolve("diktat") .also { it.createDirectory() } .resolve(licenseFile.name), overwrite = true ) } } sourceSets.getByName("main") { resources.srcDir( addLicenseTask.map { it.outputs.files.singleFile } ) } tasks.shadowJar { archiveClassifier.set("") manifest { attributes["Main-Class"] = "com.saveourtool.diktat.DiktatMainKt" attributes["Multi-Release"] = true } duplicatesStrategy = DuplicatesStrategy.FAIL } tasks.register("shadowExecutableJar") { group = "Distribution" dependsOn(tasks.shadowJar) val scriptFile = project.file("src/main/script/header-diktat.sh") val shadowJarFile = tasks.shadowJar .get() .outputs .files .singleFile val outputFile = project.layout .buildDirectory .file(shadowJarFile.name.removeSuffix(".jar")) inputs.files(scriptFile, shadowJarFile) outputs.file(outputFile) doLast { outputFile.get() .asFile .apply { writeBytes(scriptFile.readBytes()) appendBytes(shadowJarFile.readBytes()) setExecutable(true, false) } } } // disable default jar tasks.named("jar") { enabled = false } // it triggers shadowJar with default build tasks { build { dependsOn(shadowJar) } test { dependsOn(shadowJar) } } publishing { publications { // it creates a publication for shadowJar create("shadow") { // https://github.com/johnrengelman/shadow/issues/417#issuecomment-830668442 project.extensions.configure { component(this@create) } } } } configurePublications() ================================================ FILE: diktat-cli/src/main/kotlin/com/saveourtool/diktat/DiktatMain.kt ================================================ /** * The file contains main method */ package com.saveourtool.diktat import com.saveourtool.diktat.api.DiktatProcessorListener import com.saveourtool.diktat.cli.DiktatMode import com.saveourtool.diktat.cli.DiktatProperties import io.github.oshai.kotlinlogging.KotlinLogging import java.nio.file.Path import java.nio.file.Paths import kotlin.io.path.absolutePathString import kotlin.system.exitProcess private val log = KotlinLogging.logger { } private val loggingListener = object : DiktatProcessorListener { override fun before(file: Path) { log.debug { "Start processing the file: $file" } } } fun main(args: Array) { val properties = DiktatProperties.parse(diktatReporterFactory, args) properties.configureLogger() log.debug { "Loading diktatRuleSet using config ${properties.config}" } val currentFolder = Paths.get(".").toAbsolutePath().normalize() val diktatRunnerArguments = properties.toRunnerArguments( sourceRootDir = currentFolder, loggingListener = loggingListener, ) val diktatRunner = diktatRunnerFactory(diktatRunnerArguments) val unfixedErrors = when (properties.mode) { DiktatMode.CHECK -> diktatRunner.checkAll(diktatRunnerArguments) DiktatMode.FIX -> diktatRunner.fixAll(diktatRunnerArguments) { updatedFile -> log.warn { "Original and formatted content differ, writing to ${updatedFile.absolutePathString()}..." } } } if (unfixedErrors > 0) { exitProcess(1) } } ================================================ FILE: diktat-cli/src/main/kotlin/com/saveourtool/diktat/cli/DiktatMode.kt ================================================ package com.saveourtool.diktat.cli import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable /** * Mode of `diktat` */ @Serializable enum class DiktatMode { @SerialName("check") CHECK, @SerialName("fix") FIX, ; } ================================================ FILE: diktat-cli/src/main/kotlin/com/saveourtool/diktat/cli/DiktatProperties.kt ================================================ package com.saveourtool.diktat.cli import com.saveourtool.diktat.DIKTAT import com.saveourtool.diktat.DIKTAT_ANALYSIS_CONF import com.saveourtool.diktat.DiktatRunnerArguments import com.saveourtool.diktat.ENGINE_INFO import com.saveourtool.diktat.api.DiktatProcessorListener import com.saveourtool.diktat.api.DiktatReporterCreationArguments import com.saveourtool.diktat.api.DiktatReporterFactory import com.saveourtool.diktat.api.DiktatReporterType import com.saveourtool.diktat.util.isKotlinCodeOrScript import com.saveourtool.diktat.util.listFiles import generated.DIKTAT_VERSION import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.core.LoggerContext import org.slf4j.event.Level import java.io.OutputStream import java.nio.file.Path import java.nio.file.Paths import kotlin.io.path.absolute import kotlin.io.path.createDirectories import kotlin.io.path.exists import kotlin.io.path.inputStream import kotlin.io.path.outputStream import kotlin.system.exitProcess import kotlinx.cli.ArgParser import kotlinx.cli.ArgType import kotlinx.cli.default import kotlinx.cli.vararg /** * @param reporterType * @param output * @param groupByFileInStdout * @param colorNameInStdout * @param logLevel * @property config path to `diktat-analysis.yml` * @property mode mode of `diktat` * @property patterns */ data class DiktatProperties( val config: String?, val mode: DiktatMode, private val reporterType: DiktatReporterType?, private val output: String?, private val groupByFileInStdout: Boolean?, private val colorNameInStdout: String?, private val logLevel: Level, val patterns: List, ) { /** * Configure logger level using [logLevel] */ fun configureLogger() { // set log level LogManager.getContext(false) .let { it as LoggerContext } .also { ctx -> ctx.configuration.rootLogger.level = when (logLevel) { Level.ERROR -> org.apache.logging.log4j.Level.ERROR Level.WARN -> org.apache.logging.log4j.Level.WARN Level.INFO -> org.apache.logging.log4j.Level.INFO Level.DEBUG -> org.apache.logging.log4j.Level.DEBUG Level.TRACE -> org.apache.logging.log4j.Level.TRACE } } .updateLoggers() } /** * @param sourceRootDir * @param loggingListener * @return [DiktatRunnerArguments] created from [DiktatProperties] */ fun toRunnerArguments( sourceRootDir: Path, loggingListener: DiktatProcessorListener, ): DiktatRunnerArguments { val stdoutReporterCreationArguments = DiktatReporterCreationArguments( reporterType = DiktatReporterType.PLAIN, outputStream = null, sourceRootDir = sourceRootDir, groupByFileInPlain = groupByFileInStdout, colorNameInPlain = colorNameInStdout, ) val reporterCreationArguments = reporterType?.let { DiktatReporterCreationArguments( reporterType = it, outputStream = getRequiredReporterOutput(), sourceRootDir = sourceRootDir, ) } return DiktatRunnerArguments( configInputStream = config?.let { Paths.get(it).inputStream() } ?: Paths.get(DIKTAT_ANALYSIS_CONF).takeIf { it.exists() }?.inputStream(), sourceRootDir = sourceRootDir, files = getFiles(sourceRootDir), baselineFile = null, reporterArgsList = listOfNotNull(stdoutReporterCreationArguments, reporterCreationArguments), loggingListener = loggingListener, ) } private fun getFiles(sourceRootDir: Path): Collection = sourceRootDir.listFiles(patterns = patterns.toTypedArray()) .filter { file -> file.isKotlinCodeOrScript() } .toList() private fun getRequiredReporterOutput(): OutputStream = output ?.let { Paths.get(it).absolute() } ?.also { it.parent.createDirectories() } ?.outputStream() ?: throw IllegalArgumentException("A file for the reporter output is not provided") companion object { /** * @param diktatReporterFactory * @param args cli arguments * @return parsed [DiktatProperties] */ @Suppress( "LongMethod", "TOO_LONG_FUNCTION" ) fun parse( diktatReporterFactory: DiktatReporterFactory, args: Array, ): DiktatProperties { val parser = ArgParser(DIKTAT) val config: String? by parser.config() val mode: DiktatMode by parser.diktatMode() val reporterType: DiktatReporterType? by parser.reporterType() val output: String? by parser.output() val groupByFile: Boolean? by parser.groupByFile() val colorName: String? by parser.colorName(diktatReporterFactory) val logLevel: Level by parser.logLevel() val patterns: List by parser.argument( type = ArgType.String, description = "A list of files to process by diktat" ).vararg() parser.addOptionAndShowTextWithExit( fullName = "version", shortName = "V", description = "Output version information and exit.", args = args, ) { """ Diktat: $DIKTAT_VERSION $ENGINE_INFO """.trimIndent() } parser.addOptionAndShowTextWithExit( fullName = "license", shortName = null, description = "Display the license and exit.", args = args, ) { val resourceName = "META-INF/diktat/LICENSE" DiktatProperties::class.java .classLoader .getResource(resourceName) ?.readText() ?: error("Resource $resourceName not found") } parser.parse(args) return DiktatProperties( config = config, mode = mode, reporterType = reporterType, output = output, groupByFileInStdout = groupByFile, colorNameInStdout = colorName, logLevel = logLevel, patterns = patterns, ) } /** * @return a single and optional [String] for location of config as parsed cli arg */ private fun ArgParser.config() = option( type = ArgType.String, fullName = "config", shortName = "c", description = "Specify the location of the YAML configuration file. By default, $DIKTAT_ANALYSIS_CONF in the current directory is used.", ) /** * @return a single type of [DiktatMode] as parsed cli arg. [DiktatMode.CHECK] is default value */ private fun ArgParser.diktatMode() = option( type = ArgType.Choice(), fullName = "mode", shortName = "m", description = "Mode of `diktat` controls that `diktat` fixes or only finds any deviations from the code style." ).default(DiktatMode.CHECK) /** * @return a single and optional type of [DiktatReporterType] as parsed cli arg */ private fun ArgParser.reporterType() = option( type = ArgType.Choice(), fullName = "reporter", shortName = "r", description = "The reporter to use to log errors to output." ) /** * @return a single and optional [String] for output as parsed cli arg */ private fun ArgParser.output() = option( type = ArgType.String, fullName = "output", shortName = "o", description = "Redirect the reporter output to a file. Must be provided when the reporter is provided.", ) /** * @return an optional flag to enable a grouping errors by files */ private fun ArgParser.groupByFile() = option( type = ArgType.Boolean, fullName = "group-by-file", shortName = null, description = "A flag to group found errors by files." ) /** * @param diktatReporterFactory * @return a single and optional color name as parsed cli args */ private fun ArgParser.colorName(diktatReporterFactory: DiktatReporterFactory) = option( type = ArgType.Choice( choices = diktatReporterFactory.colorNamesInPlain.toList(), toVariant = { it }, variantToString = { it }, ), fullName = "color", shortName = null, description = "Colorize the output.", ) /** * @return a single log leve as parser cli args. [Level.INFO] is default value */ private fun ArgParser.logLevel() = option( type = ArgType.Choice(), fullName = "log-level", shortName = "l", description = "Control the log level.", ).default(Level.INFO) private fun ArgParser.addOptionAndShowTextWithExit( fullName: String, shortName: String?, description: String, args: Array, contentSupplier: () -> String ) { // add here to print in help option( type = ArgType.Boolean, fullName = fullName, shortName = shortName, description = description ) if (args.contains("--$fullName") || shortName?.let { args.contains("-$it") } == true) { @Suppress("DEBUG_PRINT", "ForbiddenMethodCall") println(contentSupplier()) exitProcess(0) } } } } ================================================ FILE: diktat-cli/src/main/kotlin/com/saveourtool/diktat/util/CliUtils.kt ================================================ /** * This class contains util methods to operate with java.nio.file.Path for CLI */ package com.saveourtool.diktat.util import java.io.File import java.nio.file.FileSystems import java.nio.file.InvalidPathException import java.nio.file.Path import java.nio.file.Paths import kotlin.io.path.ExperimentalPathApi import kotlin.io.path.Path import kotlin.io.path.absolutePathString import kotlin.io.path.exists import kotlin.io.path.walk private const val NEGATIVE_PREFIX_PATTERN = "!" private const val PARENT_DIRECTORY_PREFIX = 3 private const val PARENT_DIRECTORY_UNIX = "../" private const val PARENT_DIRECTORY_WINDOWS = "..\\" // all roots private val roots: Set = FileSystems.getDefault() .rootDirectories .asSequence() .map { it.absolutePathString() } .toSet() /** * Lists all files in [this] directory based on [patterns] * * @param patterns a path to a file or a directory (all files from this directory will be returned) or an [Ant-style path pattern](https://ant.apache.org/manual/dirtasks.html#patterns) * @return [Sequence] of files as [Path] matched to provided [patterns] */ fun Path.listFiles( vararg patterns: String, ): Sequence { val (includePatterns, excludePatterns) = patterns.partition { !it.startsWith(NEGATIVE_PREFIX_PATTERN) } val exclude by lazy { doListFiles(excludePatterns.map { it.removePrefix(NEGATIVE_PREFIX_PATTERN) }) .toSet() } return doListFiles(includePatterns).filterNot { exclude.contains(it) } } @OptIn(ExperimentalPathApi::class) private fun Path.doListFiles(patterns: List): Sequence = patterns .asSequence() .flatMap { pattern -> tryToResolveIfExists(pattern, this)?.walk() ?: walkByGlob(pattern) } .map { it.normalize() } .map { it.toAbsolutePath() } .distinct() /** * Create a matcher and return a filter that uses it. * * @param glob glob pattern to filter files * @return a sequence of files which matches to [glob] */ @OptIn(ExperimentalPathApi::class) private fun Path.walkByGlob(glob: String): Sequence = if (glob.startsWith(PARENT_DIRECTORY_UNIX) || glob.startsWith(PARENT_DIRECTORY_WINDOWS)) { parent?.walkByGlob(glob.substring(PARENT_DIRECTORY_PREFIX)) ?: emptySequence() } else { getAbsoluteGlobAndRoot(glob, this) .let { (absoluteGlob, root) -> absoluteGlob .replace("([^\\\\])\\\\([^\\\\])".toRegex(), "$1\\\\\\\\$2") // encode Windows separators .let { root.fileSystem.getPathMatcher("glob:$it") } .let { matcher -> root.walk().filter { matcher.matches(it) } } } } private fun String.findRoot(): Path = substring(0, indexOf('*')) .let { withoutAsterisks -> withoutAsterisks.substring(0, withoutAsterisks.lastIndexOfAny(charArrayOf('\\', '/'))) } .let { Path(it) } /** * @param candidate * @param currentDirectory * @return path or null if path is invalid or doesn't exist */ private fun tryToResolveIfExists(candidate: String, currentDirectory: Path): Path? = try { Paths.get(candidate).takeIf { it.exists() } ?: currentDirectory.resolve(candidate).takeIf { it.exists() } } catch (e: InvalidPathException) { null } private fun getAbsoluteGlobAndRoot(glob: String, currentFolder: Path): Pair = when { glob.startsWith("**") -> glob to currentFolder roots.any { glob.startsWith(it, true) } -> glob to glob.findRoot() else -> "${currentFolder.absolutePathString()}${File.separatorChar}$glob" to currentFolder } ================================================ FILE: diktat-cli/src/main/script/diktat.cmd ================================================ @echo off rem rem diKTat command-line client for Windows rem rem Uses Git Bash, so requires Git to be installed. rem set "git_install_location=%ProgramFiles%\Git" set "git_url=https://github.com/git-for-windows/git/releases/latest" if exist "%git_install_location%" ( setlocal set "PATH=%git_install_location%\usr\bin;%PATH%" for /f "usebackq tokens=*" %%p in (`cygpath "%~dpn0"`) do bash --noprofile --norc %%p %* ) else ( echo Expecting Git for Windows at %git_install_location%; please install it from %git_url% start %git_url% ) ================================================ FILE: diktat-cli/src/main/script/header-diktat.sh ================================================ #!/usr/bin/env bash # # vim:ai et sw=4 si sta ts=4: # # External variables used: # # - JAVA_HOME # - GITHUB_ACTIONS # Bash strict mode, # see http://redsymbol.net/articles/unofficial-bash-strict-mode/. set -euo pipefail IFS=$'\n' function error() { local message message="$*" if [[ "${GITHUB_ACTIONS:=false}" == 'true' ]] then # Echoing to GitHub. echo "::error::${message}" elif [[ -t 1 ]] then # Echoing to a terminal. echo -e "\e[1m$(basename "$0"): \e[31merror:\e[0m ${message}" >&2 else # Echoing to a pipe. echo "$(basename "$0"): error: ${message}" >&2 fi } # Exit codes. # The code of 1 is returned by ktlint in the event of failure. declare -ir ERROR_JAVA_NOT_FOUND=2 declare -ir ERROR_INCOMPATIBLE_BASH_VERSION=3 if (( BASH_VERSINFO[0] < 4 )) then error "bash version ${BASH_VERSION} is too old, version 4+ is required" exit ${ERROR_INCOMPATIBLE_BASH_VERSION} fi JAVA_ARGS=() DIKTAT_JAR="$0" # Locates Java, preferring JAVA_HOME. # # The 1st variable expansion prevents the "unbound variable" error if JAVA_HOME # is unset. function find_java() { if [[ -n "${JAVA_HOME:=}" ]] then case "$(uname -s)" in 'MINGW32_NT-'* | 'MINGW64_NT-'* | 'MSYS_NT-'* ) JAVA_HOME="$(cygpath "${JAVA_HOME}")" ;; esac JAVA="${JAVA_HOME}/bin/java" # Update the PATH, just in case export PATH="${JAVA_HOME}/bin:${PATH}" elif [[ -x "$(which java 2>/dev/null)" ]] then JAVA="$(which java 2>/dev/null)" else error 'Java is not found' exit ${ERROR_JAVA_NOT_FOUND} fi } # On Windows, converts a UNIX path to Windows. Should be invoked before a path # is passed to any of the Windows-native tools (e.g.: `java`). # # On UNIX, just returns the 1st argument. function native_path() { case "$(uname -s)" in 'MINGW32_NT-'* | 'MINGW64_NT-'* | 'MSYS_NT-'* ) cygpath --windows "$1" ;; *) echo "$1" ;; esac } find_java JAVA_ARGS+=('-Xmx512m') JAVA_ARGS+=('-jar' "$(native_path "${DIKTAT_JAR}")") exec "${JAVA}" "${JAVA_ARGS[@]}" "$@" ================================================ FILE: diktat-cli/src/test/kotlin/com/saveourtool/diktat/smoke/DiktatCliTest.kt ================================================ package com.saveourtool.diktat.smoke import com.saveourtool.diktat.test.framework.util.checkForkedJavaHome import com.saveourtool.diktat.test.framework.util.deleteIfExistsSilently import com.saveourtool.diktat.test.framework.util.inheritJavaHome import com.saveourtool.diktat.test.framework.util.isWindows import com.saveourtool.diktat.test.framework.util.temporaryDirectory import io.github.oshai.kotlinlogging.KotlinLogging import org.assertj.core.api.SoftAssertions.assertSoftly import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir import java.nio.file.Path import java.nio.file.Paths import kotlin.io.path.ExperimentalPathApi import kotlin.io.path.PathWalkOption import kotlin.io.path.absolutePathString import kotlin.io.path.copyTo import kotlin.io.path.createDirectories import kotlin.io.path.div import kotlin.io.path.isDirectory import kotlin.io.path.readText import kotlin.io.path.walk class DiktatCliTest { @Test fun `Run diKTat from cli`() { cliTest("examples/maven/src/main/kotlin/Test.kt") } @Test fun `Run diKTat from cli (absolute paths)`() { cliTest(tempDir.resolve("examples/maven/src/main/kotlin/Test.kt").absolutePathString()) } @Test fun `Run diKTat from cli (glob paths, 1 of 4)`() { cliTest("examples/maven/src/main/kotlin/*.kt") } @Test fun `Run diKTat from cli (glob paths, 2 of 4)`() { cliTest("examples/**/main/kotlin/*.kt") } @Test fun `Run diKTat from cli (glob paths, 3 of 4)`() { cliTest("examples/**/*.kt") } @Test fun `Run diKTat from cli (glob paths, 4 of 4)`() { cliTest("**/*.kt") } @Suppress("TOO_LONG_FUNCTION") private fun cliTest( vararg cliArgs: String, ) { assertSoftly { softly -> val diktatCliLog = (tempDir / "log.txt").apply { parent.createDirectories() deleteIfExistsSilently() } val processBuilder = createProcessBuilderWithCmd(*cliArgs).apply { redirectErrorStream(true) redirectOutput(ProcessBuilder.Redirect.appendTo(diktatCliLog.toFile())) /* * Inherit JAVA_HOME for the child process. */ inheritJavaHome() temporaryDirectory(tempDir / ".tmp") } val diktatCliProcess = processBuilder.start() val exitCode = diktatCliProcess.waitFor() softly.assertThat(exitCode).describedAs("The exit code of Diktat CLI").isOne softly.assertThat(diktatCliLog).isRegularFile val diktatCliOutput = diktatCliLog.readText() val commandLine = processBuilder.command().joinToString(separator = " ") softly.assertThat(diktatCliOutput) .describedAs("The output of \"$commandLine\"") .isNotEmpty .contains("[VARIABLE_NAME_INCORRECT_FORMAT]") .doesNotContain("WARNING:") } } private fun createProcessBuilderWithCmd(vararg cliArgs: String): ProcessBuilder { return when { System.getProperty("os.name").isWindows() -> arrayOf(*javaArgs, DIKTAT_CLI_JAR, *defaultArgs, *cliArgs) else -> arrayOf("sh", "-c", arrayOf(*javaArgs, DIKTAT_CLI_JAR, *defaultArgs, *cliArgs).joinToString(" ")) }.let { args -> ProcessBuilder(*args).directory(tempDir.toFile()) } } companion object { private val logger = KotlinLogging.logger {} private val javaArgs = arrayOf("java", "-showversion", "-jar") private val defaultArgs = arrayOf("--log-level", "debug") @JvmStatic @TempDir internal var tempDir: Path = Paths.get("/invalid") @BeforeAll @JvmStatic @OptIn(ExperimentalPathApi::class) internal fun beforeAll() { assertSoftly { softly -> checkForkedJavaHome() logger.info { "The temp directory for the test is $tempDir." } val sourceDirectory = Paths.get("../examples") val targetDirectory = (tempDir / "examples").also { it.createDirectories() } sourceDirectory.walk(PathWalkOption.INCLUDE_DIRECTORIES).forEach { file -> if (file.isDirectory()) { targetDirectory.resolve(sourceDirectory.relativize(file)).createDirectories() } else { val dest = targetDirectory.resolve(sourceDirectory.relativize(file)) file.copyTo(dest) } } copyDiktatCli(softly, tempDir / DIKTAT_CLI_JAR) val defaultConfigFile = Paths.get("../diktat-analysis.yml") softly.assertThat(defaultConfigFile) .describedAs("Default config file for diktat") .isRegularFile defaultConfigFile.copyTo(tempDir / "diktat-analysis.yml", overwrite = true) } } } } ================================================ FILE: diktat-cli/src/test/kotlin/com/saveourtool/diktat/smoke/DiktatSaveSmokeTest.kt ================================================ package com.saveourtool.diktat.smoke import com.saveourtool.diktat.api.DiktatError import com.saveourtool.diktat.test.framework.processing.TestComparatorUnit import com.saveourtool.diktat.test.framework.util.checkForkedJavaHome import com.saveourtool.diktat.test.framework.util.deleteIfExistsSilently import com.saveourtool.diktat.test.framework.util.inheritJavaHome import com.saveourtool.diktat.test.framework.util.isWindows import com.saveourtool.diktat.test.framework.util.temporaryDirectory import io.github.oshai.kotlinlogging.KotlinLogging import org.assertj.core.api.Assertions.fail import org.assertj.core.api.SoftAssertions.assertSoftly import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.condition.DisabledOnOs import org.junit.jupiter.api.condition.OS import java.net.URL import java.nio.file.Path import kotlin.io.path.absolute import kotlin.io.path.copyTo import kotlin.io.path.createDirectories import kotlin.io.path.div import kotlin.io.path.readText @DisabledOnOs(OS.MAC) class DiktatSaveSmokeTest : DiktatSmokeTestBase() { override fun fixAndCompare( config: Path, expected: String, test: String, ) { saveSmokeTest(config, test) } // do nothing, we can't check unfixed lint errors here override fun assertUnfixedLintErrors(diktatErrorConsumer: (List) -> Unit) = Unit /** * @param testPath path to file with code that will be transformed by formatter, loaded by [TestComparatorUnit.resourceReader] * @param configFilePath path of diktat-analysis file */ @Suppress("TOO_LONG_FUNCTION") private fun saveSmokeTest( configFilePath: Path, testPath: String ) { assertSoftly { softly -> softly.assertThat(configFilePath).isRegularFile val configFile = (baseDirectoryPath / "diktat-analysis.yml").apply { parent.createDirectories() } val saveLog = (baseDirectoryPath / "tmpSave.txt").apply { parent.createDirectories() deleteIfExistsSilently() } configFilePath.copyTo(configFile, overwrite = true) val processBuilder = createProcessBuilderWithCmd(testPath).apply { redirectErrorStream(true) redirectOutput(ProcessBuilder.Redirect.appendTo(saveLog.toFile())) /* * Inherit JAVA_HOME for the child process. */ inheritJavaHome() temporaryDirectory(baseDirectoryPath / TEMP_DIRECTORY) } val saveProcess = processBuilder.start() val saveExitCode = saveProcess.waitFor() softly.assertThat(saveExitCode).describedAs("The exit code of SAVE").isZero softly.assertThat(saveLog).isRegularFile val saveOutput = saveLog.readText() val saveCommandLine = processBuilder.command().joinToString(separator = " ") softly.assertThat(saveOutput) .describedAs("The output of \"$saveCommandLine\"") .isNotEmpty .contains("SUCCESS") } } /** * @param testPath path to file with code that will be transformed by formatter, loaded by [TestComparatorUnit.resourceReader] * @return ProcessBuilder */ private fun createProcessBuilderWithCmd(testPath: String): ProcessBuilder { val savePath = baseDirectoryPath.resolve(getSaveForCurrentOs()).toString() val saveArgs = arrayOf( baseDirectoryPath.resolve("src/main/kotlin").toString(), testPath, "--log", "all" ) return when { System.getProperty("os.name").isWindows() -> arrayOf(savePath, *saveArgs) else -> arrayOf("sh", "-c", "chmod 777 $savePath ; $savePath ${saveArgs.joinToString(" ")}") }.let { args -> ProcessBuilder(*args) } } companion object { private val logger = KotlinLogging.logger {} private const val SAVE_VERSION: String = "0.3.4" private const val TEMP_DIRECTORY = ".save-cli" private val baseDirectoryPath by lazy { tempDir.absolute() } private fun getSaveForCurrentOs(): String { val osName = System.getProperty("os.name") return when { osName.startsWith("Linux", ignoreCase = true) -> "save-$SAVE_VERSION-linuxX64.kexe" osName.startsWith("Mac", ignoreCase = true) -> "save-$SAVE_VERSION-macosX64.kexe" osName.isWindows() -> "save-$SAVE_VERSION-mingwX64.exe" else -> fail("SAVE doesn't support $osName (version ${System.getProperty("os.version")})") } } private fun downloadFile(from: URL, to: Path) = downloadFile(from, to, baseDirectoryPath) @BeforeAll @JvmStatic internal fun beforeAll() { assertSoftly { softly -> checkForkedJavaHome() logger.info { "The base directory for the smoke test is $baseDirectoryPath." } val diktat = baseDirectoryPath / DIKTAT_CLI_JAR copyDiktatCli(softly, diktat) val save = baseDirectoryPath / getSaveForCurrentOs() downloadFile(URL("https://github.com/saveourtool/save-cli/releases/download/v$SAVE_VERSION/${getSaveForCurrentOs()}"), save) } } } } ================================================ FILE: diktat-cli/src/test/kotlin/com/saveourtool/diktat/smoke/DiktatSmokeTest.kt ================================================ package com.saveourtool.diktat.smoke import com.saveourtool.diktat.api.DiktatError import com.saveourtool.diktat.ktlint.format import com.saveourtool.diktat.ruleset.config.DiktatRuleConfigYamlReader import com.saveourtool.diktat.ruleset.rules.DiktatRuleSetFactoryImpl import com.saveourtool.diktat.test.framework.processing.TestComparatorUnit import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeEach import java.nio.file.Path import kotlin.io.path.inputStream /** * Test for [DiktatRuleSetFactoryImpl] in autocorrect mode as a whole. All rules are applied to a file. * Note: ktlint uses initial text from a file to calculate line and column from offset. Because of that line/col of unfixed errors * may change after some changes to text or other rules. */ class DiktatSmokeTest : DiktatSmokeTestBase() { private val unfixedLintErrors: MutableList = mutableListOf() override fun fixAndCompare( config: Path, expected: String, test: String, ) { val result = getTestComparatorUnit(config) .compareFilesFromResources(expected, test) result.assertSuccessful() } @BeforeEach internal fun setUp() { unfixedLintErrors.clear() } override fun assertUnfixedLintErrors(diktatErrorConsumer: (List) -> Unit) { diktatErrorConsumer(unfixedLintErrors) } private fun getTestComparatorUnit(config: Path) = TestComparatorUnit( resourceReader = { tempDir.resolve("src/main/kotlin").resolve(it).normalize() }, function = { testFile -> format( ruleSetSupplier = { val diktatRuleConfigReader = DiktatRuleConfigYamlReader() val diktatRuleSetFactory = DiktatRuleSetFactoryImpl() diktatRuleSetFactory(diktatRuleConfigReader(config.inputStream())) }, file = testFile, cb = { lintError, _ -> unfixedLintErrors.add(lintError) }, ) }, ) } ================================================ FILE: diktat-cli/src/test/kotlin/com/saveourtool/diktat/smoke/DiktatSmokeTestBase.kt ================================================ @file:Suppress( "MISSING_KDOC_CLASS_ELEMENTS", "MISSING_KDOC_ON_FUNCTION", "BACKTICKS_PROHIBITED", ) package com.saveourtool.diktat.smoke import com.saveourtool.diktat.api.DiktatError import com.saveourtool.diktat.common.config.rules.DIKTAT_COMMON import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.config.DiktatRuleConfigYamlReader import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.constants.Warnings.EMPTY_BLOCK_STRUCTURE_ERROR import com.saveourtool.diktat.ruleset.constants.Warnings.FILE_NAME_MATCH_CLASS import com.saveourtool.diktat.ruleset.constants.Warnings.HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE import com.saveourtool.diktat.ruleset.constants.Warnings.KDOC_NO_EMPTY_TAGS import com.saveourtool.diktat.ruleset.constants.Warnings.MISSING_KDOC_CLASS_ELEMENTS import com.saveourtool.diktat.ruleset.constants.Warnings.MISSING_KDOC_ON_FUNCTION import com.saveourtool.diktat.ruleset.constants.Warnings.MISSING_KDOC_TOP_LEVEL import com.saveourtool.diktat.ruleset.constants.Warnings.WRONG_INDENTATION import com.saveourtool.diktat.ruleset.constants.Warnings.WRONG_NEWLINES import com.saveourtool.diktat.ruleset.rules.chapter1.FileNaming import com.saveourtool.diktat.ruleset.rules.chapter2.comments.CommentsRule import com.saveourtool.diktat.ruleset.rules.chapter2.comments.HeaderCommentRule import com.saveourtool.diktat.ruleset.rules.chapter2.kdoc.KdocComments import com.saveourtool.diktat.ruleset.rules.chapter2.kdoc.KdocFormatting import com.saveourtool.diktat.ruleset.rules.chapter2.kdoc.KdocMethods import com.saveourtool.diktat.ruleset.rules.chapter3.EmptyBlock import com.saveourtool.diktat.ruleset.rules.chapter3.files.SemicolonsRule import com.saveourtool.diktat.ruleset.rules.chapter6.classes.InlineClassesRule import com.saveourtool.diktat.ruleset.utils.indentation.IndentationConfig import com.saveourtool.diktat.ruleset.utils.indentation.IndentationConfig.Companion.EXTENDED_INDENT_AFTER_OPERATORS import com.saveourtool.diktat.ruleset.utils.indentation.IndentationConfig.Companion.EXTENDED_INDENT_BEFORE_DOT import com.saveourtool.diktat.ruleset.utils.indentation.IndentationConfig.Companion.EXTENDED_INDENT_FOR_EXPRESSION_BODIES import com.charleskorn.kaml.Yaml import com.charleskorn.kaml.YamlConfiguration import generated.WarningNames import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.junit.jupiter.api.Timeout import org.junit.jupiter.api.io.TempDir import java.nio.file.Path import java.nio.file.Paths import java.time.LocalDate import java.util.concurrent.TimeUnit.SECONDS import kotlin.io.path.ExperimentalPathApi import kotlin.io.path.PathWalkOption import kotlin.io.path.copyTo import kotlin.io.path.createDirectories import kotlin.io.path.createTempFile import kotlin.io.path.inputStream import kotlin.io.path.isDirectory import kotlin.io.path.moveTo import kotlin.io.path.name import kotlin.io.path.toPath import kotlin.io.path.walk import kotlin.io.path.writeText import kotlinx.serialization.builtins.ListSerializer typealias RuleToConfig = Map> /** * Base class for smoke test classes */ abstract class DiktatSmokeTestBase { /** * Disable some of the rules. * * @param rulesToDisable * @param rulesToOverride */ @Suppress("UnsafeCallOnNullableType") private fun prepareOverriddenRulesConfig(rulesToDisable: List = emptyList(), rulesToOverride: RuleToConfig = emptyMap()): Path { val rulesConfig = DiktatRuleConfigYamlReader().invoke(Paths.get(DEFAULT_CONFIG_PATH).inputStream()) .toMutableList() .also { rulesConfig -> rulesToDisable.forEach { warning -> rulesConfig.removeIf { it.name == warning.name } rulesConfig.add(RulesConfig(warning.name, enabled = false, configuration = emptyMap())) } rulesToOverride.forEach { (warning, configuration) -> rulesConfig.removeIf { it.name == warning } rulesConfig.add(RulesConfig(warning, enabled = true, configuration = configuration)) } } .toList() return createTempFile() .also { it.writeText( Yaml(configuration = YamlConfiguration(strictMode = true)) .encodeToString(ListSerializer(RulesConfig.serializer()), rulesConfig) ) } } @Test @Tag("DiktatRuleSetProvider") @Timeout(TEST_TIMEOUT_SECONDS, unit = SECONDS) fun `regression - should not fail if package is not set`() { val configFilePath = prepareOverriddenRulesConfig(listOf(Warnings.PACKAGE_NAME_MISSING, Warnings.PACKAGE_NAME_INCORRECT_PATH, Warnings.PACKAGE_NAME_INCORRECT_PREFIX)) fixAndCompare(configFilePath, "DefaultPackageExpected.kt", "DefaultPackageTest.kt") } @Test @Tag("DiktatRuleSetProvider") @Timeout(TEST_TIMEOUT_SECONDS, unit = SECONDS) fun `smoke test #8 - anonymous function`() { fixAndCompare(prepareOverriddenRulesConfig(), "Example8Expected.kt", "Example8Test.kt") } @Test @Tag("DiktatRuleSetProvider") @Timeout(TEST_TIMEOUT_SECONDS, unit = SECONDS) fun `smoke test #7`() { fixAndCompare(prepareOverriddenRulesConfig(), "Example7Expected.kt", "Example7Test.kt") } @Test @Tag("DiktatRuleSetProvider") @Timeout(TEST_TIMEOUT_SECONDS, unit = SECONDS) fun `smoke test #6`() { val configFilePath = prepareOverriddenRulesConfig( rulesToDisable = emptyList(), rulesToOverride = mapOf( WRONG_INDENTATION.name to mapOf( EXTENDED_INDENT_FOR_EXPRESSION_BODIES to "true", EXTENDED_INDENT_AFTER_OPERATORS to "true", EXTENDED_INDENT_BEFORE_DOT to "true", ) ) ) fixAndCompare(configFilePath, "Example6Expected.kt", "Example6Test.kt") } @Test @Tag("DiktatRuleSetProvider") @Timeout(TEST_TIMEOUT_SECONDS, unit = SECONDS) fun `smoke test #5`() { val configFilePath = prepareOverriddenRulesConfig(emptyList(), mapOf( Warnings.HEADER_MISSING_OR_WRONG_COPYRIGHT.name to mapOf( "isCopyrightMandatory" to "true", "copyrightText" to """|Copyright 2018-${LocalDate.now().year} John Doe. | Licensed under the Apache License, Version 2.0 (the "License"); | you may not use this file except in compliance with the License. | You may obtain a copy of the License at | | http://www.apache.org/licenses/LICENSE-2.0 """.trimMargin() ), DIKTAT_COMMON to mapOf( "domainName" to "com.saveourtool.diktat", "kotlinVersion" to "1.3.7" ) ) ) fixAndCompare(configFilePath, "Example5Expected.kt", "Example5Test.kt") assertUnfixedLintErrors { unfixedLintErrors -> assertThat(unfixedLintErrors).let { errors -> errors.doesNotContain( DiktatError( line = 1, col = 1, ruleId = "diktat-ruleset:${CommentsRule.NAME_ID}", detail = "${Warnings.COMMENTED_OUT_CODE.warnText()} /*" ) ) errors.contains( DiktatError(12, 1, "diktat-ruleset:${InlineClassesRule.NAME_ID}", "${Warnings.INLINE_CLASS_CAN_BE_USED.warnText()} class Some") ) } } } @Test @Tag("DiktatRuleSetProvider") @Timeout(TEST_TIMEOUT_SECONDS, unit = SECONDS) fun `smoke test #4`() { fixAndCompare(prepareOverriddenRulesConfig(), "Example4Expected.kt", "Example4Test.kt") } @Test @Tag("DiktatRuleSetProvider") @Timeout(TEST_TIMEOUT_SECONDS, unit = SECONDS) fun `smoke test #3`() { fixAndCompare(prepareOverriddenRulesConfig(), "Example3Expected.kt", "Example3Test.kt") } @Test @Tag("DiktatRuleSetProvider") @Timeout(TEST_TIMEOUT_SECONDS, unit = SECONDS) fun `regression - shouldn't throw exception in cases similar to #371`() { fixAndCompare(prepareOverriddenRulesConfig(), "Bug1Expected.kt", "Bug1Test.kt") } @Test @Tag("DiktatRuleSetProvider") @Timeout(TEST_TIMEOUT_SECONDS, unit = SECONDS) fun `smoke test #2`() { val configFilePath = prepareOverriddenRulesConfig( rulesToDisable = emptyList(), rulesToOverride = mapOf( WRONG_INDENTATION.name to mapOf( EXTENDED_INDENT_AFTER_OPERATORS to "true", EXTENDED_INDENT_BEFORE_DOT to "true", ) ) ) fixAndCompare(configFilePath, "Example2Expected.kt", "Example2Test.kt") assertUnfixedLintErrors { unfixedLintErrors -> assertThat(unfixedLintErrors).containsExactlyInAnyOrder( DiktatError(1, 1, "$DIKTAT_RULE_SET_ID:${HeaderCommentRule.NAME_ID}", "${HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE.warnText()} there are 2 declared classes and/or objects", false), DiktatError(34, 3, "$DIKTAT_RULE_SET_ID:${EmptyBlock.NAME_ID}", "${EMPTY_BLOCK_STRUCTURE_ERROR.warnText()} empty blocks are forbidden unless it is function with override keyword", false), ) } } @Test @Tag("DiktatRuleSetProvider") @Timeout(TEST_TIMEOUT_SECONDS, unit = SECONDS) fun `smoke test #1`() { val configFilePath = prepareOverriddenRulesConfig( rulesToDisable = emptyList(), rulesToOverride = mapOf( WRONG_INDENTATION.name to mapOf( EXTENDED_INDENT_AFTER_OPERATORS to "true", EXTENDED_INDENT_FOR_EXPRESSION_BODIES to "true", ) ) ) fixAndCompare(configFilePath, "Example1Expected.kt", "Example1Test.kt") assertUnfixedLintErrors { unfixedLintErrors -> assertThat(unfixedLintErrors).containsExactlyInAnyOrder( DiktatError(1, 1, "$DIKTAT_RULE_SET_ID:${FileNaming.NAME_ID}", "${FILE_NAME_MATCH_CLASS.warnText()} Example1Test.kt vs Example", false), DiktatError(3, 1, "$DIKTAT_RULE_SET_ID:${KdocComments.NAME_ID}", "${MISSING_KDOC_TOP_LEVEL.warnText()} Example", false), DiktatError(4, 5, "$DIKTAT_RULE_SET_ID:${KdocComments.NAME_ID}", "${MISSING_KDOC_CLASS_ELEMENTS.warnText()} isValid", false), DiktatError(6, 5, "$DIKTAT_RULE_SET_ID:${KdocComments.NAME_ID}", "${MISSING_KDOC_CLASS_ELEMENTS.warnText()} foo", false), DiktatError(8, 5, "$DIKTAT_RULE_SET_ID:${KdocComments.NAME_ID}", "${MISSING_KDOC_CLASS_ELEMENTS.warnText()} foo", false), DiktatError(8, 5, "$DIKTAT_RULE_SET_ID:${KdocMethods.NAME_ID}", "${MISSING_KDOC_ON_FUNCTION.warnText()} foo", false), DiktatError(8, 19, "$DIKTAT_RULE_SET_ID:${EmptyBlock.NAME_ID}", "${EMPTY_BLOCK_STRUCTURE_ERROR.warnText()} empty blocks are forbidden unless it is function with override keyword", false), DiktatError(13, 8, "$DIKTAT_RULE_SET_ID:${KdocFormatting.NAME_ID}", "${KDOC_NO_EMPTY_TAGS.warnText()} @return", false), DiktatError(19, 8, "$DIKTAT_RULE_SET_ID:${KdocFormatting.NAME_ID}", "${KDOC_NO_EMPTY_TAGS.warnText()} @return", false), DiktatError(27, 8, "$DIKTAT_RULE_SET_ID:${KdocFormatting.NAME_ID}", "${KDOC_NO_EMPTY_TAGS.warnText()} @return", false), DiktatError(36, 8, "$DIKTAT_RULE_SET_ID:${KdocFormatting.NAME_ID}", "${KDOC_NO_EMPTY_TAGS.warnText()} @return", false), ) } } @Test @Tag("DiktatRuleSetProvider") @Timeout(TEST_TIMEOUT_SECONDS, unit = SECONDS) fun `smoke test with kts files #2`() { fixAndCompare(prepareOverriddenRulesConfig(), "script/SimpleRunInScriptExpected.kts", "script/SimpleRunInScriptTest.kts") } @Test @Tag("DiktatRuleSetProvider") @Timeout(TEST_TIMEOUT_SECONDS, unit = SECONDS) fun `smoke test with kts files with package name`() { fixAndCompare(prepareOverriddenRulesConfig(), "script/PackageInScriptExpected.kts", "script/PackageInScriptTest.kts") } @Test @Tag("DiktatRuleSetProvider") @Timeout(TEST_TIMEOUT_SECONDS, unit = SECONDS) fun `regression - should correctly handle tags with empty lines`() { fixAndCompare(prepareOverriddenRulesConfig(), "KdocFormattingMultilineTagsExpected.kt", "KdocFormattingMultilineTagsTest.kt") } @Test @Tag("DiktatRuleSetProvider") @Timeout(TEST_TIMEOUT_SECONDS, unit = SECONDS) fun `regression - FP of local variables rule`() { fixAndCompare(prepareOverriddenRulesConfig(), "LocalVariableWithOffsetExpected.kt", "LocalVariableWithOffsetTest.kt") } @Test @Tag("DiktatRuleSetProvider") @Timeout(TEST_TIMEOUT_SECONDS, unit = SECONDS) fun `fix can cause long line`() { val configFilePath = prepareOverriddenRulesConfig( rulesToDisable = emptyList(), rulesToOverride = mapOf( WRONG_INDENTATION.name to mapOf( EXTENDED_INDENT_AFTER_OPERATORS to "false", ) ) ) fixAndCompare(configFilePath, "ManyLineTransformInLongLineExpected.kt", "ManyLineTransformInLongLineTest.kt") } @Test @Tag("DiktatRuleSetProvider") fun `smoke test with multiplatform project layout`() { fixAndCompare( prepareOverriddenRulesConfig(), "../../jsMain/kotlin/com/saveourtool/diktat/scripts/ScriptExpected.kt", "../../jsMain/kotlin/com/saveourtool/diktat/scripts/ScriptTest.kt" ) } @Test @Tag("DiktatRuleSetProvider") fun `smoke test with kts files`() { val configFilePath = prepareOverriddenRulesConfig( emptyList(), mapOf( WRONG_INDENTATION.name to mapOf( IndentationConfig.NEWLINE_AT_END to "false", ) ) ) // so that trailing newline isn't checked, because it's incorrectly read in tests and we are comparing file with itself // file name is `gradle_` so that IDE doesn't suggest to import gradle project val tmpFilePath = "../../../build.gradle.kts" fixAndCompare(configFilePath, tmpFilePath, tmpFilePath) assertUnfixedLintErrors { unfixedLintErrors -> assertThat(unfixedLintErrors).isEmpty() } } @Test @Tag("DiktatRuleSetProvider") fun `smoke test with gradle script plugin`() { fixAndCompare(prepareOverriddenRulesConfig(), "kotlin-library-expected.gradle.kts", "kotlin-library.gradle.kts") assertUnfixedLintErrors { unfixedLintErrors -> assertThat(unfixedLintErrors).containsExactly( DiktatError( 2, 1, "$DIKTAT_RULE_SET_ID:${CommentsRule.NAME_ID}", "[COMMENTED_OUT_CODE] you should not comment out code, " + "use VCS to save it in history and delete this block: import org.jetbrains.kotlin.gradle.dsl.jvm", false ) ) } } @Test @Tag("DiktatRuleSetProvider") fun `disable chapters`() { val configFilePath = prepareOverriddenRulesConfig( emptyList(), mapOf( DIKTAT_COMMON to mapOf( "domainName" to "com.saveourtool.diktat", "disabledChapters" to "Naming,3,4,5,Classes" ) ) ) fixAndCompare(configFilePath, "Example1-2Expected.kt", "Example1-2Test.kt") assertUnfixedLintErrors { unfixedLintErrors -> assertThat(unfixedLintErrors).containsExactlyInAnyOrder( DiktatError(3, 1, "$DIKTAT_RULE_SET_ID:${KdocComments.NAME_ID}", "${MISSING_KDOC_TOP_LEVEL.warnText()} example", false), DiktatError(3, 16, "$DIKTAT_RULE_SET_ID:${KdocComments.NAME_ID}", "${MISSING_KDOC_CLASS_ELEMENTS.warnText()} isValid", false), DiktatError(6, 5, "$DIKTAT_RULE_SET_ID:${KdocComments.NAME_ID}", "${MISSING_KDOC_CLASS_ELEMENTS.warnText()} foo", false), DiktatError(6, 5, "$DIKTAT_RULE_SET_ID:${KdocMethods.NAME_ID}", "${MISSING_KDOC_ON_FUNCTION.warnText()} foo", false), DiktatError(8, 5, "$DIKTAT_RULE_SET_ID:${KdocComments.NAME_ID}", "${MISSING_KDOC_CLASS_ELEMENTS.warnText()} foo", false), DiktatError(13, 4, "$DIKTAT_RULE_SET_ID:${KdocFormatting.NAME_ID}", "${KDOC_NO_EMPTY_TAGS.warnText()} @return", false), DiktatError(20, 4, "$DIKTAT_RULE_SET_ID:${KdocFormatting.NAME_ID}", "${KDOC_NO_EMPTY_TAGS.warnText()} @return", false), DiktatError(28, 4, "$DIKTAT_RULE_SET_ID:${KdocFormatting.NAME_ID}", "${KDOC_NO_EMPTY_TAGS.warnText()} @return", false), DiktatError(37, 4, "$DIKTAT_RULE_SET_ID:${KdocFormatting.NAME_ID}", "${KDOC_NO_EMPTY_TAGS.warnText()} @return", false), ) } } @Test @Tag("DiktatRuleSetProvider") @Timeout(TEST_TIMEOUT_SECONDS, unit = SECONDS) fun `regression - should not fail if file has unnecessary semicolons`() { fixAndCompare(prepareOverriddenRulesConfig(), "SemicolonsExpected.kt", "SemicolonsTest.kt") } @Test @Tag("DiktatRuleSetProvider") @Timeout(TEST_TIMEOUT_SECONDS, unit = SECONDS) fun `should add newlines between interfaces`() { fixAndCompare(prepareOverriddenRulesConfig(), "NewlinesAfterInterfacesExpected.kt", "NewlinesAfterInterfacesTest.kt") } abstract fun fixAndCompare( config: Path, expected: String, test: String, ) abstract fun assertUnfixedLintErrors(diktatErrorConsumer: (List) -> Unit) companion object { private const val DEFAULT_CONFIG_PATH = "../diktat-analysis.yml" private const val ROOT_RESOURCE_FILE_PATH = "test/smoke" private const val TEST_TIMEOUT_SECONDS = 30L @JvmStatic @TempDir internal var tempDir: Path = Paths.get("/invalid") @BeforeAll @JvmStatic @OptIn(ExperimentalPathApi::class) internal fun createTmpFiles() { val resourceFilePath = DiktatSmokeTestBase::class.java .classLoader .getResource(ROOT_RESOURCE_FILE_PATH) .let { resource -> requireNotNull(resource) { "$ROOT_RESOURCE_FILE_PATH not found" } } .toURI() .toPath() resourceFilePath.walk(PathWalkOption.INCLUDE_DIRECTORIES).forEach { file -> if (file.isDirectory()) { tempDir.resolve(resourceFilePath.relativize(file)).createDirectories() } else { val dest = tempDir.resolve(resourceFilePath.relativize(file)) file.copyTo(dest) when (file.name) { "build.gradle.kts_" -> dest.moveTo(dest.parent.resolve("build.gradle.kts")) "Example1Test.kt" -> dest.copyTo(dest.parent.resolve("Example1-2Test.kt")) } } } } } } ================================================ FILE: diktat-cli/src/test/kotlin/com/saveourtool/diktat/smoke/DiktatSmokeTestUtils.kt ================================================ @file:Suppress("HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE") package com.saveourtool.diktat.smoke import com.saveourtool.diktat.test.framework.util.retry import io.github.oshai.kotlinlogging.KotlinLogging import org.assertj.core.api.Assertions.fail import org.assertj.core.api.SoftAssertions import java.net.URL import java.nio.file.Path import kotlin.io.path.Path import kotlin.io.path.copyTo import kotlin.io.path.exists import kotlin.io.path.listDirectoryEntries import kotlin.io.path.outputStream import kotlin.io.path.relativeToOrSelf import kotlin.system.measureNanoTime internal const val BUILD_DIRECTORY = "build/libs" internal const val DIKTAT_CLI_JAR = "diktat.jar" internal const val DIKTAT_CLI_JAR_GLOB = "diktat-cli-*.jar" private val logger = KotlinLogging.logger {} /** * Downloads the file from a remote URL, retrying if necessary. * * @param from the remote URL to download from. * @param to the target path. * @param baseDirectory the directory against which [to] should be relativized * if it's absolute. */ @Suppress("FLOAT_IN_ACCURATE_CALCULATIONS") internal fun downloadFile( from: URL, to: Path, baseDirectory: Path, ) { logger.info { "Downloading $from to ${to.relativeToOrSelf(baseDirectory)}..." } @Suppress("MAGIC_NUMBER") val attempts = 5 val lazyDefault: (Throwable) -> Unit = { error -> fail("Failure downloading $from after $attempts attempt(s)", error) } retry(attempts, lazyDefault = lazyDefault) { from.openStream().use { source -> to.outputStream().use { target -> val bytesCopied: Long val timeNanos = measureNanoTime { bytesCopied = source.copyTo(target) } logger.info { "$bytesCopied byte(s) copied in ${timeNanos / 1000 / 1e3} ms." } } } } } /** * Copies the diktat-cli.jar with assertions * * @param softAssertions * @param to the target path. */ internal fun copyDiktatCli( softAssertions: SoftAssertions, to: Path ) { /* * The fat JAR should reside in the same directory as `save*` and * be named `diktat.jar` * (see `diktat-cli/src/test/resources/test/smoke/save.toml`). */ val buildDirectory = Path(BUILD_DIRECTORY) softAssertions.assertThat(buildDirectory) .isDirectory val diktatFrom = buildDirectory .takeIf(Path::exists) ?.listDirectoryEntries(DIKTAT_CLI_JAR_GLOB) ?.singleOrNull() softAssertions.assertThat(diktatFrom) .describedAs(diktatFrom?.toString() ?: "$BUILD_DIRECTORY/$DIKTAT_CLI_JAR_GLOB") .isNotNull .isRegularFile diktatFrom?.copyTo(to, overwrite = true) } ================================================ FILE: diktat-cli/src/test/kotlin/com/saveourtool/diktat/util/CliUtilsKtTest.kt ================================================ package com.saveourtool.diktat.util import org.assertj.core.api.Assertions import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test import org.junit.jupiter.api.condition.EnabledOnOs import org.junit.jupiter.api.condition.OS import org.junit.jupiter.api.io.TempDir import java.io.File import java.nio.file.Path import java.nio.file.Paths import kotlin.io.path.absolutePathString import kotlin.io.path.createDirectory import kotlin.io.path.createFile import kotlin.io.path.writeText class CliUtilsKtTest { @Test fun listByFilesWithLeadingAsterisks() { Assertions.assertThat(tmpDir.listFiles("**/Test1.kt").toList()) .containsExactlyInAnyOrder( tmpDir.resolve("folder1").resolve("subFolder11").resolve("Test1.kt"), tmpDir.resolve("folder1").resolve("subFolder12").resolve("Test1.kt"), tmpDir.resolve("folder2").resolve("Test1.kt"), ) } @Test fun listByFilesWithGlobalPath() { Assertions.assertThat(tmpDir.listFiles("${tmpDir.absolutePathString()}${File.separator}**${File.separator}Test2.kt").toList()) .containsExactlyInAnyOrder( tmpDir.resolve("folder1").resolve("subFolder11").resolve("Test2.kt"), tmpDir.resolve("folder2").resolve("Test2.kt"), ) } @Test fun listByFilesWithGlobalPattern() { Assertions.assertThat(tmpDir.resolve("folder2").listFiles("${tmpDir.absolutePathString()}${File.separator}**${File.separator}Test2.kt").toList()) .containsExactlyInAnyOrder( tmpDir.resolve("folder1").resolve("subFolder11").resolve("Test2.kt"), tmpDir.resolve("folder2").resolve("Test2.kt"), ) } @Test fun listByFilesWithRelativePath() { Assertions.assertThat(tmpDir.listFiles("folder1/subFolder11/*.kt").toList()) .containsExactlyInAnyOrder( tmpDir.resolve("folder1").resolve("subFolder11").resolve("Test1.kt"), tmpDir.resolve("folder1").resolve("subFolder11").resolve("Test2.kt"), ) } @Test @EnabledOnOs(OS.WINDOWS) fun listByFilesWithRelativePathWindows() { Assertions.assertThat(tmpDir.listFiles("folder1\\subFolder11\\*.kt").toList()) .containsExactlyInAnyOrder( tmpDir.resolve("folder1").resolve("subFolder11").resolve("Test1.kt"), tmpDir.resolve("folder1").resolve("subFolder11").resolve("Test2.kt"), ) } @Test fun listByFilesWithEmptyResult() { Assertions.assertThat(tmpDir.listFiles("**/*.kts").toList()) .isEmpty() } @Test fun listByFilesWithParentFolder() { Assertions.assertThat(tmpDir.resolve("folder1").listFiles("../*/*.kt").toList()) .containsExactlyInAnyOrder( tmpDir.resolve("folder2").resolve("Test1.kt"), tmpDir.resolve("folder2").resolve("Test2.kt"), tmpDir.resolve("folder2").resolve("Test3.kt"), ) } @Test fun listByFilesWithFolder() { Assertions.assertThat(tmpDir.listFiles("folder2").toList()) .containsExactlyInAnyOrder( tmpDir.resolve("folder2").resolve("Test1.kt"), tmpDir.resolve("folder2").resolve("Test2.kt"), tmpDir.resolve("folder2").resolve("Test3.kt"), ) Assertions.assertThat(tmpDir.listFiles("folder1").toList()) .containsExactlyInAnyOrder( tmpDir.resolve("folder1").resolve("subFolder11").resolve("Test1.kt"), tmpDir.resolve("folder1").resolve("subFolder11").resolve("Test2.kt"), tmpDir.resolve("folder1").resolve("subFolder12").resolve("Test1.kt"), ) } @Test fun listByFilesWithNegative() { Assertions.assertThat(tmpDir.listFiles("**/*.kt", "!**/subFolder11/*.kt", "!**/Test3.kt").toList()) .containsExactlyInAnyOrder( tmpDir.resolve("folder1").resolve("subFolder12").resolve("Test1.kt"), tmpDir.resolve("folder2").resolve("Test1.kt"), tmpDir.resolve("folder2").resolve("Test2.kt"), ) } companion object { @JvmStatic @TempDir internal var tmpDir: Path = Paths.get("/invalid") @BeforeAll @JvmStatic internal fun setupHierarchy() { tmpDir.resolveAndCreateDirectory("folder1") .also { folder1 -> folder1.resolveAndCreateDirectory("subFolder11") .also { subFolder11 -> subFolder11.resolveAndCreateFile("Test1.kt") subFolder11.resolveAndCreateFile("Test2.kt") } folder1.resolveAndCreateDirectory("subFolder12") .also { subFolder12 -> subFolder12.resolveAndCreateFile("Test1.kt") } } tmpDir.resolveAndCreateDirectory("folder2") .also { folder2 -> folder2.resolveAndCreateFile("Test1.kt") folder2.resolveAndCreateFile("Test2.kt") folder2.resolveAndCreateFile("Test3.kt") } } private fun Path.resolveAndCreateDirectory(name: String): Path = resolve(name).also { it.createDirectory() } private fun Path.resolveAndCreateFile(name: String): Path = resolve(name).also { it.createFile().writeText("Test file: $name") } } } ================================================ FILE: diktat-cli/src/test/resources/test/smoke/.editorconfig ================================================ # https://editorconfig.org root = true [{*.kt,*.kts}] # disable ktlint rules ktlint_standard = disabled ktlint_experimental = disabled ktlint_test = disabled ktlint_custom = disabled ================================================ FILE: diktat-cli/src/test/resources/test/smoke/.gitignore ================================================ /diktat.jar /diktat-analysis.yml /ktlint /save-*-mingwX64.exe /tmpSave.txt /.save-cli/ ================================================ FILE: diktat-cli/src/test/resources/test/smoke/build.gradle.kts_ ================================================ import com.saveourtool.diktat.generation.docs.generateAvailableRules plugins { kotlin("jvm") version "1.4.21" id("com.saveourtool.diktat") version "2.0.0" } repositories { mavenCentral() maven { url = uri("https://example.com") } } val generateAvailableRules by tasks.register("generateAvailableRules") { dependsOn("generateRulesMapping") doFirst { generateAvailableRules(rootDir, file("$rootDir/../wp")) } } val updateDocumentation = tasks.register("updateDocumentation") { dependsOn( "generateRulesMapping", "generateAvailableRules", "generateFullDoc", "generateCodeStyle" ) } diktat { debug = true inputs { include("buildSrc/**/*.kt", "*.kts") } } ================================================ FILE: diktat-cli/src/test/resources/test/smoke/save.toml ================================================ [general] execCmd="java -showversion -jar diktat.jar --log-level debug" tags = ["smokeTest"] description = "SmokeTest" suiteName = "SmokeTest" language = "Kotlin" expectedWarningsPattern = "// ;warn:?(.*):(\\d*): (.+)" timeOutMillis = 3600000 ["fix and warn"] ["fix and warn".fix] execFlags="--mode fix" ["fix and warn".warn] lineCaptureGroup = 1 columnCaptureGroup = 2 messageCaptureGroup = 3 lineCaptureGroupOut = 2 columnCaptureGroupOut = 3 messageCaptureGroupOut = 4 actualWarningsPattern = "(\\w+\\..+):(\\d+):(\\d+): (\\[.*\\].*)$" exactWarningsMatch = false warningTextHasColumn = true warningTextHasLine = true ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/jsMain/kotlin/com/saveourtool/diktat/scripts/ScriptExpected.kt ================================================ package com.saveourtool.diktat.scripts import kotlinx.browser.document fun main() { (document.getElementById("myId") as HTMLElement).click() } ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/jsMain/kotlin/com/saveourtool/diktat/scripts/ScriptTest.kt ================================================ package com.saveourtool.diktat.js import kotlinx.browser.document fun main() { (document.getElementById("myId") as HTMLElement).click() } ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/main/kotlin/Bug1Expected.kt ================================================ // ;warn:$line:1: [FILE_NAME_MATCH_CLASS] file name is incorrect - it should match with the class described in it if there is the only one class declared: Bug1Expected.kt vs D{{.*}} package com.saveourtool.diktat // ;warn:$line:1: [MISSING_KDOC_TOP_LEVEL] all public and internal top-level classes and functions should have Kdoc: D (cannot be auto-corrected){{.*}} // ;warn:7: [CLASS_NAME_INCORRECT] class/enum/interface name should be in PascalCase and should contain only latin (ASCII) letters or numbers: D{{.*}} // ;warn:7: [IDENTIFIER_LENGTH] identifier's length is incorrect, it should be in range of [2, 64] symbols: D (cannot be auto-corrected){{.*}} class D { // ;warn:$line:5: [MISSING_KDOC_CLASS_ELEMENTS] all public, internal and protected classes, functions and variables inside the class should have Kdoc: x (cannot be auto-corrected){{.*}} val x = 0 /** // ;warn:8: [KDOC_NO_EMPTY_TAGS] no empty descriptions in tag blocks are allowed: @return (cannot be auto-corrected){{.*}} * @return */ fun bar(): Bar { // ;warn:19: [MAGIC_NUMBER] avoid using magic numbers, instead define constants with clear names describing what the magic number means: 42 (cannot be auto-corrected){{.*}} val qux = 42 return Bar(qux) } } /** * @param foo */ fun readFile(foo: Foo) { var bar: Bar } ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/main/kotlin/Bug1Test.kt ================================================ package test.smoke.src.main.kotlin fun readFile(foo: Foo) { var bar: Bar } class D {val x = 0 fun bar(): Bar {val qux = 42; return Bar(qux)} } ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/main/kotlin/DefaultPackageExpected.kt ================================================ // ;warn:1:1: [FILE_NAME_MATCH_CLASS] file name is incorrect - it should match with the class described in it if there is the only one class declared: DefaultPackageExpected.kt vs Example{{.*}} // ;warn:3:1: [FILE_INCORRECT_BLOCKS_ORDER] general structure of kotlin source file is wrong, parts are in incorrect order: @file:Suppress{{.*}} @file:Suppress( "PACKAGE_NAME_MISSING", "PACKAGE_NAME_INCORRECT_PATH", "PACKAGE_NAME_INCORRECT_PREFIX" ) /** * Dolor sit amet */ class Example ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/main/kotlin/DefaultPackageTest.kt ================================================ @file:Suppress( "PACKAGE_NAME_MISSING", "PACKAGE_NAME_INCORRECT_PATH", "PACKAGE_NAME_INCORRECT_PREFIX" ) /** * Dolor sit amet */ class Example ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/main/kotlin/Example1-2Expected.kt ================================================ package test.smoke class example{ @get : JvmName ( "getIsValid" ) val isValid = true fun Foo.foo() { } val foo:Int =1 /** * @param x * @param y * @return */ fun bar(x :Int,y:Int) :Int { return x+ y} /** * @param sub * @return */ fun String.countSubStringOccurrences(sub: String): Int { // println("sub: $sub") return this.split(sub).size - 1 } /** * @return */ fun String.splitPathToDirs(): List = this.replace("\\", "/") .replace("//", "/") .split("/") /** * @param x * @param y * @return */ fun foo(x : Int , y: Int ): Int { return x + (y + bar(x,y) ) } } ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/main/kotlin/Example1Expected.kt ================================================ // ;warn:$line:1: [FILE_NAME_MATCH_CLASS] file name is incorrect - it should match with the class described in it if there is the only one class declared: Example1Expected.kt vs Example{{.*}} package com.saveourtool.diktat // ;warn:$line:1: [MISSING_KDOC_TOP_LEVEL] all public and internal top-level classes and functions should have Kdoc: Example (cannot be auto-corrected){{.*}} class Example { // ;warn:$line:5: [MISSING_KDOC_CLASS_ELEMENTS] all public, internal and protected classes, functions and variables inside the class should have Kdoc: isValid (cannot be auto-corrected){{.*}} @get:JvmName("getIsValid") val isValid = true // ;warn:$line:5: [MISSING_KDOC_CLASS_ELEMENTS] all public, internal and protected classes, functions and variables inside the class should have Kdoc: foo (cannot be auto-corrected){{.*}} val foo: Int = 1 // ;warn:$line:5: [MISSING_KDOC_CLASS_ELEMENTS] all public, internal and protected classes, functions and variables inside the class should have Kdoc: foo (cannot be auto-corrected){{.*}} // ;warn:$line-1:5: [MISSING_KDOC_ON_FUNCTION] all public, internal and protected functions should have Kdoc with proper tags: foo (cannot be auto-corrected){{.*}} // ;warn:19: [EMPTY_BLOCK_STRUCTURE_ERROR] incorrect format of empty block: empty blocks are forbidden unless it is function with override keyword (cannot be auto-corrected){{.*}} fun Foo.foo() { } /** * @param x * @param y // ;warn:8: [KDOC_NO_EMPTY_TAGS] no empty descriptions in tag blocks are allowed: @return (cannot be auto-corrected){{.*}} * @return */ fun bar(x: Int, y: Int): Int = x + y /** * @param sub // ;warn:8: [KDOC_NO_EMPTY_TAGS] no empty descriptions in tag blocks are allowed: @return (cannot be auto-corrected){{.*}} * @return */ fun String.countSubStringOccurrences(sub: String): Int { // println("sub: $sub") return this.split(sub).size - 1 } /** // ;warn:8: [KDOC_NO_EMPTY_TAGS] no empty descriptions in tag blocks are allowed: @return (cannot be auto-corrected){{.*}} * @return */ fun String.splitPathToDirs(): List = this.replace("\\", "/").replace("//", "/") .split("/") /** * @param x * @param y // ;warn:8: [KDOC_NO_EMPTY_TAGS] no empty descriptions in tag blocks are allowed: @return (cannot be auto-corrected){{.*}} * @return */ fun foo(x: Int, y: Int): Int = x + (y + bar(x, y) ) } ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/main/kotlin/Example1Test.kt ================================================ package test.smoke class example{ @get : JvmName ( "getIsValid" ) val isValid = true fun Foo.foo() { } val foo:Int =1 fun bar(x :Int,y:Int) :Int { return x+ y} fun String.countSubStringOccurrences(sub: String): Int { // println("sub: $sub") return this.split(sub).size - 1 } fun String.splitPathToDirs(): List = this.replace("\\", "/") .replace("//", "/") .split("/") fun foo(x : Int , y: Int ): Int { return x + (y + bar(x,y) ) } } ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/main/kotlin/Example2Expected.kt ================================================ // ;warn:$line:1: [HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE] files that contain multiple or no classes should contain description of what is inside of this file: there are 2 declared classes and/or objects (cannot be auto-corrected){{.*}} package com.saveourtool.diktat import org.slf4j.LoggerFactory import java.io.IOException import java.util.Properties /** * @property foo * @property bar */ @ExperimentalStdlibApi public data class Example(val foo: Int, val bar: Double) : SuperExample("lorem ipsum") // ;warn:36:3: [EMPTY_BLOCK_STRUCTURE_ERROR] incorrect format of empty block: empty blocks are forbidden unless it is function with override keyword (cannot be auto-corrected){{.*}} private class TestException : Exception() /* this class is unused */ // private class Test : RuntimeException() /** * Creates a docker container with [file], prepared to execute it * * @param runConfiguration a [RunConfiguration] for the supplied binary * @param file a file that will be included as an executable * @param resources additional resources * @param containerName * @return id of created container or null if it wasn't created * @throws DockerException if docker daemon has returned an error * @throws DockerException if docker daemon has returned an error * @throws RuntimeException if an exception not specific to docker has occurred */ internal fun createWithFile(runConfiguration: RunConfiguration, containerName: String, file: File, resources: Collection = emptySet() ) {} private fun foo(node: ASTNode) { when (node.elementType) { CLASS, FUN, PRIMARY_CONSTRUCTOR, SECONDARY_CONSTRUCTOR -> checkAnnotation(node) } val qwe = a && b val qwe = a && b // comment if (x) { foo() } setOf(IOException(), Properties(), LoggerFactory()) } ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/main/kotlin/Example2Test.kt ================================================ package test.smoke import java.io.IOException import java.util.Properties import kotlin.system.exitProcess import org.slf4j.LoggerFactory data @ExperimentalStdlibApi public class Example(val foo:Int, val bar:Double):SuperExample("lorem ipsum") private class Test : Exception() /* this class is unused */ //private class Test : RuntimeException() /** * Creates a docker container with [file], prepared to execute it * * @param runConfiguration a [RunConfiguration] for the supplied binary * @param file a file that will be included as an executable * @param resources additional resources * @throws DockerException if docker daemon has returned an error * @throws DockerException if docker daemon has returned an error * @throws RuntimeException if an exception not specific to docker has occurred * @return id of created container or null if it wasn't created */ internal fun createWithFile(runConfiguration: RunConfiguration, containerName: String, file: File, resources: Collection = emptySet()) {} private fun foo (node: ASTNode) { when (node.elementType) { CLASS, FUN, PRIMARY_CONSTRUCTOR, SECONDARY_CONSTRUCTOR -> checkAnnotation(node) } val qwe = a && b val qwe = a && b if (x) // comment foo() setOf(IOException(), Properties(), LoggerFactory()) } ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/main/kotlin/Example3Expected.kt ================================================ // ;warn:$line:1: [HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE] files that contain multiple or no classes should contain description of what is inside of this file: there are 6 declared classes and/or objects (cannot be auto-corrected){{.*}} package com.saveourtool.diktat /* * Copyright (c) Your Company Name Here. 2010-2020 */ /** * @property name */ class HttpClient(var name: String) { // ;warn:$line:5: [MISSING_KDOC_CLASS_ELEMENTS] all public, internal and protected classes, functions and variables inside the class should have Kdoc: url (cannot be auto-corrected){{.*}} var url: String = "" // ;warn:$line:5: [MISSING_KDOC_CLASS_ELEMENTS] all public, internal and protected classes, functions and variables inside the class should have Kdoc: port (cannot be auto-corrected){{.*}} var port: String = "" // ;warn:$line:5: [MISSING_KDOC_CLASS_ELEMENTS] all public, internal and protected classes, functions and variables inside the class should have Kdoc: timeout (cannot be auto-corrected){{.*}} var timeout = 0 // ;warn:$line:5: [MISSING_KDOC_CLASS_ELEMENTS] all public, internal and protected classes, functions and variables inside the class should have Kdoc: doRequest (cannot be auto-corrected){{.*}} // ;warn:$line-1:5: [MISSING_KDOC_ON_FUNCTION] all public, internal and protected functions should have Kdoc with proper tags: doRequest (cannot be auto-corrected){{.*}} // ;warn:21: [EMPTY_BLOCK_STRUCTURE_ERROR] incorrect format of empty block: empty blocks are forbidden unless it is function with override keyword (cannot be auto-corrected){{.*}} fun doRequest() {} } // ;warn:$line:1: [MISSING_KDOC_TOP_LEVEL] all public and internal top-level classes and functions should have Kdoc: Example (cannot be auto-corrected){{.*}} class Example { // ;warn:$line:5: [MISSING_KDOC_CLASS_ELEMENTS] all public, internal and protected classes, functions and variables inside the class should have Kdoc: foo (cannot be auto-corrected){{.*}} // ;warn:$line-1:5: [MISSING_KDOC_ON_FUNCTION] all public, internal and protected functions should have Kdoc with proper tags: foo (cannot be auto-corrected){{.*}} fun foo() { if (condition1 && condition2) { bar() } if (condition3) { if (condition4) { foo() } else { bar() } } else { foo() } } } // ;warn:$line:1: [MISSING_KDOC_TOP_LEVEL] all public and internal top-level classes and functions should have Kdoc: IssueType (cannot be auto-corrected){{.*}} enum class IssueType { PROJECT_STRUCTURE, TESTS, VCS } // ;warn:$line:1: [MISSING_KDOC_TOP_LEVEL] all public and internal top-level classes and functions should have Kdoc: IssueType2 (cannot be auto-corrected){{.*}} enum class IssueType2 { PROJECT_STRUCTURE, TESTS, VCS, ; /** * @param bar // ;warn:8: [KDOC_NO_EMPTY_TAGS] no empty descriptions in tag blocks are allowed: @return (cannot be auto-corrected){{.*}} * @return */ fun foo(bar: Int) = bar companion object } // ;warn:$line:1: [MISSING_KDOC_TOP_LEVEL] all public and internal top-level classes and functions should have Kdoc: IssueType3 (cannot be auto-corrected){{.*}} enum class IssueType3 { // ;warn:5: [IDENTIFIER_LENGTH] identifier's length is incorrect, it should be in range of [2, 64] symbols: A (cannot be auto-corrected){{.*}} A, // ;warn:5: [CONFUSING_IDENTIFIER_NAMING] it's a bad name for identifier: better name is: bt, nxt (cannot be auto-corrected){{.*}} // ;warn:5: [IDENTIFIER_LENGTH] identifier's length is incorrect, it should be in range of [2, 64] symbols: B (cannot be auto-corrected){{.*}} B, // ;warn:5: [IDENTIFIER_LENGTH] identifier's length is incorrect, it should be in range of [2, 64] symbols: C (cannot be auto-corrected){{.*}} C, // ;warn:5: [CONFUSING_IDENTIFIER_NAMING] it's a bad name for identifier: better name is: obj, dgt (cannot be auto-corrected){{.*}} // ;warn:5: [IDENTIFIER_LENGTH] identifier's length is incorrect, it should be in range of [2, 64] symbols: D (cannot be auto-corrected){{.*}} D, ; } // ;warn:$line:1: [MISSING_KDOC_TOP_LEVEL] all public and internal top-level classes and functions should have Kdoc: Foo (cannot be auto-corrected){{.*}} // ;warn:$line+4:8: [KDOC_NO_NEWLINE_AFTER_SPECIAL_TAGS] in KDoc there should be exactly one empty line after special tags: @implNote{{.*}} // ;warn:$line+5:23: [EMPTY_BLOCK_STRUCTURE_ERROR] incorrect format of empty block: empty blocks are forbidden unless it is function with override keyword (cannot be auto-corrected){{.*}} class Foo { /** * @implNote lorem ipsum */ private fun foo() {} } // ;warn:$line:1: [MISSING_KDOC_TOP_LEVEL] all public and internal top-level classes and functions should have Kdoc: mains (cannot be auto-corrected){{.*}} // ;warn:$line-1:1: [MISSING_KDOC_ON_FUNCTION] all public, internal and protected functions should have Kdoc with proper tags: mains (cannot be auto-corrected){{.*}} fun mains() { val httpClient = HttpClient("myConnection") .apply { url = "http://example.com" port = "8080" // ;warn:23: [MAGIC_NUMBER] avoid using magic numbers, instead define constants with clear names describing what the magic number means: 100 (cannot be auto-corrected){{.*}} timeout = 100 } httpClient.doRequest() } ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/main/kotlin/Example3Test.kt ================================================ /* * Copyright (c) Your Company Name Here. 2010-2020 */ class HttpClient { var name: String var url: String = "" var port: String = "" var timeout = 0 constructor(name: String) { this.name = name } fun doRequest() {} } fun mains() { val httpClient = HttpClient("myConnection") .apply { url = "http://example.com" port = "8080" timeout = 100 } httpClient.doRequest() } class Example { fun foo() { if (condition1) if (condition2) bar() if (condition3) if (condition4) foo() else bar() else foo() } } enum class IssueType { VCS, PROJECT_STRUCTURE, TESTS } enum class IssueType2 { VCS, PROJECT_STRUCTURE, TESTS; companion object fun foo(bar: Int) = bar } enum class IssueType3 { A, C, B, D, ; } class Foo { /** * @implNote lorem ipsum */ private fun foo() {} } ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/main/kotlin/Example4Expected.kt ================================================ // ;warn:$line:1: [FILE_NAME_MATCH_CLASS] file name is incorrect - it should match with the class described in it if there is the only one class declared: Example4Expected.kt vs SpecialTagsInKdoc{{.*}} package com.saveourtool.diktat // ;warn:$line:1: [MISSING_KDOC_TOP_LEVEL] all public and internal top-level classes and functions should have Kdoc: SpecialTagsInKdoc (cannot be auto-corrected){{.*}} class SpecialTagsInKdoc { /** * Empty function to test KDocs * @apiNote foo * * @implSpec bar * * @implNote baz * // ;warn:8: [KDOC_NO_EMPTY_TAGS] no empty descriptions in tag blocks are allowed: @return (cannot be auto-corrected){{.*}} * @return */ fun test() = Unit } // ;warn:$line:1: [MISSING_KDOC_TOP_LEVEL] all public and internal top-level classes and functions should have Kdoc: `method name incorrect, part 4` (cannot be auto-corrected){{.*}} // ;warn:$line-1:1: [MISSING_KDOC_ON_FUNCTION] all public, internal and protected functions should have Kdoc with proper tags: `method name incorrect, part 4` (cannot be auto-corrected){{.*}} // ;warn:5: [BACKTICKS_PROHIBITED] backticks should not be used in identifier's naming. The only exception test methods marked with @Test annotation: `method name incorrect, part 4` (cannot be auto-corrected){{.*}} fun `method name incorrect, part 4`() { val code = """ class TestPackageName { fun methODTREE(): String { } } """.trimIndent() lintMethod(code, LintError(2, 7, ruleId, "${FUNCTION_NAME_INCORRECT_CASE.warnText()} methODTREE", true)) foo // we are calling bar .bar() bar /* This is a block comment */ .foo() } // ;warn:$line:1: [MISSING_KDOC_TOP_LEVEL] all public and internal top-level classes and functions should have Kdoc: foo (cannot be auto-corrected){{.*}} // ;warn:$line-1:1: [MISSING_KDOC_ON_FUNCTION] all public, internal and protected functions should have Kdoc with proper tags: foo (cannot be auto-corrected){{.*}} fun foo() { foo( 0, { obj -> obj.bar() } ) bar( 0, { obj -> obj.bar() } ) } // ;warn:$line:1: [MISSING_KDOC_TOP_LEVEL] all public and internal top-level classes and functions should have Kdoc: bar (cannot be auto-corrected){{.*}} // ;warn:$line-1:1: [MISSING_KDOC_ON_FUNCTION] all public, internal and protected functions should have Kdoc with proper tags: bar (cannot be auto-corrected){{.*}} fun bar() { val diktatExtension = project.extensions.create(DIKTAT_EXTENSION, DiktatExtension::class.java).apply { inputs = project.fileTree("src").apply { include("**/*.kt") } reporter = PlainReporter(System.out) } } // ;warn:$line:1: [MISSING_KDOC_TOP_LEVEL] all public and internal top-level classes and functions should have Kdoc: boo (cannot be auto-corrected){{.*}} // ;warn:$line-1:1: [MISSING_KDOC_ON_FUNCTION] all public, internal and protected functions should have Kdoc with proper tags: boo (cannot be auto-corrected){{.*}} @Suppress("") fun boo() { val y = "akgjsaujtmaksdkfasakgjsaujtmaksdkfasakgjsaujtmaksdkfasakgjsaujtm" + " aksdkfasfasakgjsaujtmaksdfasafasakgjsaujtmaksdfasakgjsaujtmaksdfasakgjsaujtmaksdfasakgjsaujtmaksdfasakgjsaujtmaksdkgjsaujtmaksdfasakgjsaujtmaksd" } ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/main/kotlin/Example4Test.kt ================================================ package com.saveourtool.diktat class SpecialTagsInKdoc { /** * Empty function to test KDocs * @apiNote foo * @implSpec bar * * * @implNote baz */ fun test() = Unit } fun `method name incorrect, part 4`() { val code = """ class TestPackageName { fun methODTREE(): String { } } """.trimIndent() lintMethod(code, LintError(2, 7, ruleId, "${FUNCTION_NAME_INCORRECT_CASE.warnText()} methODTREE", true)) foo // we are calling bar .bar() bar /* This is a block comment */ .foo() } fun foo() { foo( 0, { obj -> obj.bar() } ) bar( 0, { obj -> obj.bar() } ) } fun bar() { val diktatExtension = project.extensions.create(DIKTAT_EXTENSION, DiktatExtension::class.java) diktatExtension.inputs = project.fileTree("src").apply { include("**/*.kt") } diktatExtension.reporter = PlainReporter(System.out) } @Suppress("") fun boo() { val y = "akgjsaujtmaksdkfasakgjsaujtmaksdkfasakgjsaujtmaksdkfasakgjsaujtm aksdkfasfasakgjsaujtmaksdfasafasakgjsaujtmaksdfasakgjsaujtmaksdfasakgjsaujtmaksdfasakgjsaujtmaksdfasakgjsaujtmaksdkgjsaujtmaksdfasakgjsaujtmaksd" } ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/main/kotlin/Example5Expected.kt ================================================ // ;warn:$line:1: [FILE_NAME_MATCH_CLASS] file name is incorrect - it should match with the class described in it if there is the only one class declared: Example5Expected.kt vs Some{{.*}} /* Copyright 2018-2024 John Doe. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 */ package com.saveourtool.diktat // ;warn:$line:1: [MISSING_KDOC_TOP_LEVEL] all public and internal top-level classes and functions should have Kdoc: Some (cannot be auto-corrected){{.*}} // ;warn:$line-1:1: [USE_DATA_CLASS] this class can be converted to a data class: Some (cannot be auto-corrected){{.*}} class Some { // ;warn:$line:5: [MISSING_KDOC_CLASS_ELEMENTS] all public, internal and protected classes, functions and variables inside the class should have Kdoc: config (cannot be auto-corrected){{.*}} val config = Config() } ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/main/kotlin/Example5Test.kt ================================================ package com.saveourtool.diktat class Some { val config = Config() } ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/main/kotlin/Example6Expected.kt ================================================ // ;warn:$line:1: [HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE] files that contain multiple or no classes should contain description of what is inside of this file: there are 0 declared classes and/or objects (cannot be auto-corrected){{.*}} package com.saveourtool.diktat val foo = """ some cool text """.trimIndent() val bar = """ | some | text """.trimMargin() val text = """ x """ val dockerFileAsText = """ FROM $baseImage someTest COPY resources $resourcesPath RUN /bin/bash """.trimIndent() // RUN command shouldn't matter because it will be replaced on container creation ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/main/kotlin/Example6Test.kt ================================================ package com.saveourtool.diktat val foo = """ some cool text """.trimIndent() val bar = """ | some | text """.trimMargin() val text = """ x """ val dockerFileAsText = """ FROM $baseImage someTest COPY resources $resourcesPath RUN /bin/bash """.trimIndent() // RUN command shouldn't matter because it will be replaced on container creation ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/main/kotlin/Example7Expected.kt ================================================ // ;warn:$line:1: [HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE] files that contain multiple or no classes should contain description of what is inside of this file: there are 0 declared classes and/or objects (cannot be auto-corrected){{.*}} package com.saveourtool.diktat // ;warn:$line:1: [MISSING_KDOC_TOP_LEVEL] all public and internal top-level classes and functions should have Kdoc: foo (cannot be auto-corrected){{.*}} // ;warn:$line-1:1: [MISSING_KDOC_ON_FUNCTION] all public, internal and protected functions should have Kdoc with proper tags: foo (cannot be auto-corrected){{.*}} fun foo() { val prop: Int = 0 prop ?: run { println("prop is null") bar() } prop?.let { baz() gaz() } prop?.let { doAnotherSmth() } ?: doSmth() } // ;warn:$line:1: [MISSING_KDOC_TOP_LEVEL] all public and internal top-level classes and functions should have Kdoc: fooo (cannot be auto-corrected){{.*}} // ;warn:$line-1:1: [MISSING_KDOC_ON_FUNCTION] all public, internal and protected functions should have Kdoc with proper tags: fooo (cannot be auto-corrected){{.*}} fun fooo() { if (a) { bar() } else b?.let { baz() } ?: run { qux() } } ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/main/kotlin/Example7Test.kt ================================================ package com.saveourtool.diktat fun foo() { val prop: Int? = null if (prop == null) { println("prop is null") bar() } if (prop != null) { baz() gaz() } if (prop == null) { doSmth() } else { doAnotherSmth() } } fun fooo() { if (a) { bar() } else b?.let { baz() } ?: run { qux() } } ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/main/kotlin/Example8Expected.kt ================================================ // ;warn:$line:1: [HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE] files that contain multiple or no classes should contain description of what is inside of this file: there are 0 declared classes and/or objects (cannot be auto-corrected) (diktat-ruleset:header-comment) package com.saveourtool.diktat // ;warn:$line:1: [MISSING_KDOC_TOP_LEVEL] all public and internal top-level classes and functions should have Kdoc: foo (cannot be auto-corrected) (diktat-ruleset:kdoc-comments) // ;warn:$line-1:1: [MISSING_KDOC_ON_FUNCTION] all public, internal and protected functions should have Kdoc with proper tags: foo (cannot be auto-corrected) (diktat-ruleset:kdoc-methods) fun foo() { // ;warn:28: [WRONG_WHITESPACE] incorrect usage of whitespaces for code separation: , should have 0 space(s) before and 1 space(s) after, but has 0 space(s) after (diktat-ruleset:horizontal-whitespace) val sum: (Int, Int, Int,) -> Int = fun( x, y, z ): Int = x + y + x // ;warn:5: [DEBUG_PRINT] use a dedicated logging library: found println() (cannot be auto-corrected) (diktat-ruleset:debug-print) println(sum(8, 8, 8)) } // ;warn:$line:1: [MISSING_KDOC_TOP_LEVEL] all public and internal top-level classes and functions should have Kdoc: boo (cannot be auto-corrected) (diktat-ruleset:kdoc-comments) // ;warn:$line-1:1: [MISSING_KDOC_ON_FUNCTION] all public, internal and protected functions should have Kdoc with proper tags: boo (cannot be auto-corrected) (diktat-ruleset:kdoc-methods) fun boo() { // ;warn:27: [DEBUG_PRINT] use a dedicated logging library: found println() (cannot be auto-corrected) (diktat-ruleset:debug-print) val message = fun() = println("Hello") } ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/main/kotlin/Example8Test.kt ================================================ package com.saveourtool.diktat fun foo() { val sum: (Int, Int, Int,) -> Int = fun( x, y, z ): Int { return x + y + x } println(sum(8, 8, 8)) } fun boo() { val message = fun()=println("Hello") } ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/main/kotlin/KdocFormattingMultilineTagsExpected.kt ================================================ // ;warn:$line:1: [HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE] files that contain multiple or no classes should contain description of what is inside of this file: there are 0 declared classes and/or objects (cannot be auto-corrected) (diktat-ruleset:header-comment) package com.saveourtool.diktat /** * @param bar lorem ipsum * * dolor sit amet // ;warn:4: [KDOC_NO_EMPTY_TAGS] no empty descriptions in tag blocks are allowed: @return (cannot be auto-corrected) (diktat-ruleset:kdoc-formatting) * @return */ fun foo1(bar: Bar): Baz { // placeholder } /** * @param bar lorem ipsum * * dolor sit amet // ;warn:4: [KDOC_NO_EMPTY_TAGS] no empty descriptions in tag blocks are allowed: @return (cannot be auto-corrected) (diktat-ruleset:kdoc-formatting) * @return */ fun foo2(bar: Bar): Baz { // placeholder } ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/main/kotlin/KdocFormattingMultilineTagsTest.kt ================================================ /** * @param bar lorem ipsum * * dolor sit amet */ fun foo1(bar: Bar): Baz { // placeholder } /** * @param bar lorem ipsum * * dolor sit amet * */ fun foo2(bar: Bar): Baz { // placeholder } ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/main/kotlin/LocalVariableWithOffsetExpected.kt ================================================ // ;warn:$line:1: [HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE] files that contain multiple or no classes should contain description of what is inside of this file: there are 0 declared classes and/or objects (cannot be auto-corrected) (diktat-ruleset:header-comment) package com.saveourtool.diktat // ;warn:$line:1: [MISSING_KDOC_TOP_LEVEL] all public and internal top-level classes and functions should have Kdoc: boo (cannot be auto-corrected) (diktat-ruleset:kdoc-comments) override fun boo() { val listTestResult: MutableList = mutableListOf() files.chunked(warnPluginConfig.batchSize ?: 1).map { chunk -> handleTestFile(chunk.map { it.single() }, warnPluginConfig, generalConfig) }.forEach { listTestResult.addAll(it) } } ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/main/kotlin/LocalVariableWithOffsetTest.kt ================================================ package com.saveourtool.diktat override fun boo() { val listTestResult: MutableList = mutableListOf() files.chunked(warnPluginConfig.batchSize ?: 1).map { chunk -> handleTestFile(chunk.map { it.single() }, warnPluginConfig, generalConfig) }.forEach { listTestResult.addAll(it) } } ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/main/kotlin/ManyLineTransformInLongLineExpected.kt ================================================ package com.saveourtool.diktat fun foo() { (1 or 2 or 3 or 4 or 5 or 6 or 7 or 8 or 9 or 10 or 11 or 12 or 13 or 14 or 15 or 16 or 17 or 18 or 19 or 20 or 21 or 22 or 23 or 24 or 25 or 26 or 27 or 28 or 29 or 30 or 31 ?: 32 or 33 or 34 or 35 or 36 or 37 or 38 or 39 ?: 40 or 41 or 42 or 43 or 44 or 45 or 46 or 47 or 48 or 49 or 50 or 51 or 52 or 53 + 54 or 55 or 56 or 57 or 58 or 59 ?: 60 or 61 or 62 or 63 or 64 or 65 or 66 - 67 or 68 or 69 or 70 or 1 + 2 or 3 or 4 or 5 or 6 or 7 or 8 or 9 or 10 or 11 or 12 or 13 or 14 or 15 or 16 or 17 or 18 + 19 - 20 or 21 or 22 or 23 or 24 or 25 or 26 or 27 or 28 or 29 or 30 or 31 or 32 or 33 or 34 or 35 or 36 or 37 or 38 or 39 or 40 or 41 || 42 or 43 or 44 or 45 or 46 or 47 && 48 or 49 || 50 or 51 or 52 or 53 or 54 or 55 or 56 or 57 or 58 or 59 or 60 or 61 or 62 or 63 or 64 or 65 or 66 or 67 or 68 or 69 or 70 or 1 or 2 or 3 or 4 or 5 or 6 or 7 or 8 or 9 or 10 or 11 or 12 or 13 or 14 or 15 or 16 or 17 or 18 or 19 or 20 or 21 or 22 or 23 or 24 or 25 or 26 or 27 or 28 or 29 or 30 or 31 or 32 or 33 or 34 or 35 or 36 or 37 or 38 or 39 or 40 or 41 or 42 or 43 or 44 or 45 or 46 or 47 or 48 or 49 or 50 or 51 or 52 or 53 or 54 or 55 or 56 or 57 or 58 or 59 or 60 or 61 or 62 or 63 or 64 or 65 or 66 or 67 or 68 or 69 or 70) } ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/main/kotlin/ManyLineTransformInLongLineTest.kt ================================================ package com.saveourtool.diktat fun foo(){ (1 or 2 or 3 or 4 or 5 or 6 or 7 or 8 or 9 or 10 or 11 or 12 or 13 or 14 or 15 or 16 or 17 or 18 or 19 or 20 or 21 or 22 or 23 or 24 or 25 or 26 or 27 or 28 or 29 or 30 or 31 ?: 32 or 33 or 34 or 35 or 36 or 37 or 38 or 39 ?: 40 or 41 or 42 or 43 or 44 or 45 or 46 or 47 or 48 or 49 or 50 or 51 or 52 or 53 + 54 or 55 or 56 or 57 or 58 or 59 ?: 60 or 61 or 62 or 63 or 64 or 65 or 66 - 67 or 68 or 69 or 70 or 1 + 2 or 3 or 4 or 5 or 6 or 7 or 8 or 9 or 10 or 11 or 12 or 13 or 14 or 15 or 16 or 17 or 18 + 19 - 20 or 21 or 22 or 23 or 24 or 25 or 26 or 27 or 28 or 29 or 30 or 31 or 32 or 33 or 34 or 35 or 36 or 37 or 38 or 39 or 40 or 41 || 42 or 43 or 44 or 45 or 46 or 47 && 48 or 49 || 50 or 51 or 52 or 53 or 54 or 55 or 56 or 57 or 58 or 59 or 60 or 61 or 62 or 63 or 64 or 65 or 66 or 67 or 68 or 69 or 70 or 1 or 2 or 3 or 4 or 5 or 6 or 7 or 8 or 9 or 10 or 11 or 12 or 13 or 14 or 15 or 16 or 17 or 18 or 19 or 20 or 21 or 22 or 23 or 24 or 25 or 26 or 27 or 28 or 29 or 30 or 31 or 32 or 33 or 34 or 35 or 36 or 37 or 38 or 39 or 40 or 41 or 42 or 43 or 44 or 45 or 46 or 47 or 48 or 49 or 50 or 51 or 52 or 53 or 54 or 55 or 56 or 57 or 58 or 59 or 60 or 61 or 62 or 63 or 64 or 65 or 66 or 67 or 68 or 69 or 70) } ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/main/kotlin/NewlinesAfterInterfacesExpected.kt ================================================ package com.saveourtool.diktat class A : B(), C

, D {} ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/main/kotlin/NewlinesAfterInterfacesTest.kt ================================================ package com.saveourtool.diktat class A : B(), C

, D {} ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/main/kotlin/SemicolonsExpected.kt ================================================ package com.saveourtool.diktat import io.micrometer.core.instrument.MeterRegistry import io.micrometer.core.instrument.MultiGauge import io.micrometer.core.instrument.Tags import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Component @Component class ActiveBinsMetric(meterRegistry: MeterRegistry, private val binRepository: BinRepository) { private val metric = MultiGauge.builder(ACTIVE_BINS_METRIC_NAME) .register(meterRegistry) @Scheduled(fixedDelay = DELAY) fun queryDb() { metric.register( binRepository .countActiveWithPartsNumber() .toRangeMap() .map { MultiGauge.Row.of( Tags.of(NUMBER_OF_EGGS_LABEL, it.key), it.value ) }, true) } private fun List.toRangeMap(): MutableMap { var total = 0L val map = mutableMapOf() numberOfEggsBuckets.forEach { map[it] = 0 } this.forEach { total += it.numberOfBins when (it.numberOfParts) { 1 -> map[EGG_1_BUCKET_LABEL] = it.numberOfBins 2 -> map[EGG_2_BUCKET_LABEL] = it.numberOfBins 3 -> map[EGG_3_BUCKET_LABEL] = it.numberOfBins in 4..5 -> map[EGG_4_5_BUCKET_LABEL] = it.numberOfBins in 7..9 -> map[EGG_7_9_BUCKET_LABEL] = it.numberOfBins in 10..Int.MAX_VALUE -> map[EGG_OVER_10_BUCKET_LABEL] = it.numberOfBins } } map[ALL_ACTIVE_BINS_LABEL] = total return map } companion object { private const val ACTIVE_BINS_METRIC_NAME = "c.concurrent.bins" private const val ALL_ACTIVE_BINS_LABEL = "total" private const val EGG_1_BUCKET_LABEL = "1" private const val EGG_2_BUCKET_LABEL = "2" private const val EGG_3_BUCKET_LABEL = "3" private const val EGG_4_5_BUCKET_LABEL = "4-5" private const val EGG_7_9_BUCKET_LABEL = "7-9" private const val EGG_OVER_10_BUCKET_LABEL = "10+" private const val NUMBER_OF_EGGS_LABEL = "numberOfEggs" private val numberOfEggsBuckets = setOf( EGG_1_BUCKET_LABEL, EGG_2_BUCKET_LABEL, EGG_3_BUCKET_LABEL, EGG_4_5_BUCKET_LABEL, EGG_7_9_BUCKET_LABEL, EGG_OVER_10_BUCKET_LABEL, ALL_ACTIVE_BINS_LABEL) } } ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/main/kotlin/SemicolonsTest.kt ================================================ import io.micrometer.core.instrument.MeterRegistry import io.micrometer.core.instrument.MultiGauge import io.micrometer.core.instrument.Tags; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @Component class ActiveBinsMetric(meterRegistry: MeterRegistry, private val binRepository: BinRepository) { companion object { private const val ACTIVE_BINS_METRIC_NAME = "c.concurrent.bins"; private const val NUMBER_OF_EGGS_LABEL = "numberOfEggs"; private const val ALL_ACTIVE_BINS_LABEL = "total"; private const val EGG_1_BUCKET_LABEL = "1"; private const val EGG_2_BUCKET_LABEL = "2"; private const val EGG_3_BUCKET_LABEL = "3"; private const val EGG_4_5_BUCKET_LABEL = "4-5"; private const val EGG_7_9_BUCKET_LABEL = "7-9"; private const val EGG_OVER_10_BUCKET_LABEL = "10+"; private val numberOfEggsBuckets = setOf( EGG_1_BUCKET_LABEL, EGG_2_BUCKET_LABEL, EGG_3_BUCKET_LABEL, EGG_4_5_BUCKET_LABEL, EGG_7_9_BUCKET_LABEL, EGG_OVER_10_BUCKET_LABEL, ALL_ACTIVE_BINS_LABEL); } private val metric = MultiGauge.builder(ACTIVE_BINS_METRIC_NAME) .register(meterRegistry); @Scheduled(fixedDelay = DELAY) fun queryDB() { metric.register( binRepository .countActiveWithPartsNumber() .toRangeMap() .map { MultiGauge.Row.of( Tags.of(NUMBER_OF_EGGS_LABEL, it.key), it.value ) }, true); } private fun List.toRangeMap(): MutableMap { var total = 0L; val map = mutableMapOf(); numberOfEggsBuckets.forEach { map[it] = 0 }; this.forEach { total += it.numberOfBins when (it.numberOfParts) { 1 -> map[EGG_1_BUCKET_LABEL] = it.numberOfBins 2 -> map[EGG_2_BUCKET_LABEL] = it.numberOfBins 3 -> map[EGG_3_BUCKET_LABEL] = it.numberOfBins in 4..5 -> map[EGG_4_5_BUCKET_LABEL] = it.numberOfBins in 7..9 -> map[EGG_7_9_BUCKET_LABEL] = it.numberOfBins in 10..Int.MAX_VALUE -> map[EGG_OVER_10_BUCKET_LABEL] = it.numberOfBins }; }; map[ALL_ACTIVE_BINS_LABEL] = total; return map; } } ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/main/kotlin/kotlin-library-expected.gradle.kts ================================================ import org.gradle.kotlin.dsl.plugins // import org.jetbrains.kotlin.gradle.dsl.jvm plugins { kotlin("jvm") } ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/main/kotlin/kotlin-library.gradle.kts ================================================ import org.gradle.kotlin.dsl.plugins //import org.jetbrains.kotlin.gradle.dsl.jvm plugins { kotlin("jvm") } ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/main/kotlin/save.toml ================================================ [general] execCmd="java -showversion -jar diktat.jar --log-level debug" tags = ["smokeTest"] description = "SmokeTest" suiteName = "SmokeTest" language = "Kotlin" expectedWarningsPattern = "// ;warn:?(.*):(\\d*): (.+)" ["fix and warn"] ["fix and warn".fix] execFlags="--mode fix" ["fix and warn".warn] actualWarningsPattern = "(\\w+\\..+):(\\d+):(\\d+): (\\[.*\\].*)$" exactWarningsMatch = false ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/main/kotlin/script/PackageInScriptExpected.kts ================================================ // ;warn:$line:1: [HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE] files that contain multiple or no classes should contain description of what is inside of this file: there are 0 declared classes and/or objects (cannot be auto-corrected) (diktat-ruleset:header-comment) package com.saveourtool.diktat.script run { // ;warn:5: [DEBUG_PRINT] use a dedicated logging library: found println() (cannot be auto-corrected) (diktat-ruleset:debug-print) println("hello world!") } // ;warn:$line:1: [MISSING_KDOC_ON_FUNCTION] all public, internal and protected functions should have Kdoc with proper tags: foo (cannot be auto-corrected) (diktat-ruleset:kdoc-methods) fun foo() { // ;warn:5: [DEBUG_PRINT] use a dedicated logging library: found println() (cannot be auto-corrected) (diktat-ruleset:debug-print) println() } // ;warn:5: [IDENTIFIER_LENGTH] identifier's length is incorrect, it should be in range of [2, 64] symbols: q (cannot be auto-corrected) (diktat-ruleset:identifier-naming) val q = Config() run { // ;warn:5: [DEBUG_PRINT] use a dedicated logging library: found println() (cannot be auto-corrected) (diktat-ruleset:debug-print) println("a") } also { // ;warn:5: [DEBUG_PRINT] use a dedicated logging library: found println() (cannot be auto-corrected) (diktat-ruleset:debug-print) println("a") } ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/main/kotlin/script/PackageInScriptTest.kts ================================================ package com.saveourtool.diktat.script run { println("hello world!") } fun foo() { println() } val q = Config() run { println("a") } also { println("a") } ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/main/kotlin/script/SimpleRunInScriptExpected.kts ================================================ // ;warn:$line:1: [HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE] files that contain multiple or no classes should contain description of what is inside of this file: there are 0 declared classes and/or objects (cannot be auto-corrected) (diktat-ruleset:header-comment) run { // ;warn:5: [DEBUG_PRINT] use a dedicated logging library: found println() (cannot be auto-corrected) (diktat-ruleset:debug-print) println("hello world!") } // ;warn:$line:1: [MISSING_KDOC_ON_FUNCTION] all public, internal and protected functions should have Kdoc with proper tags: foo (cannot be auto-corrected) (diktat-ruleset:kdoc-methods) fun foo() { // ;warn:5: [DEBUG_PRINT] use a dedicated logging library: found println() (cannot be auto-corrected) (diktat-ruleset:debug-print) println() } // ;warn:5: [IDENTIFIER_LENGTH] identifier's length is incorrect, it should be in range of [2, 64] symbols: q (cannot be auto-corrected) (diktat-ruleset:identifier-naming) val q = Config() run { // ;warn:5: [DEBUG_PRINT] use a dedicated logging library: found println() (cannot be auto-corrected) (diktat-ruleset:debug-print) println("a") } also { // ;warn:5: [DEBUG_PRINT] use a dedicated logging library: found println() (cannot be auto-corrected) (diktat-ruleset:debug-print) println("a") } ================================================ FILE: diktat-cli/src/test/resources/test/smoke/src/main/kotlin/script/SimpleRunInScriptTest.kts ================================================ println("hello world!") fun foo() { println() } val q = Config() run { println("a") } also { println("a") } ================================================ FILE: diktat-common-test/build.gradle.kts ================================================ plugins { id("com.saveourtool.diktat.buildutils.kotlin-jvm-configuration") id("com.saveourtool.diktat.buildutils.code-quality-convention") id("com.saveourtool.diktat.buildutils.publishing-default-configuration") } project.description = "Diktat common for tests" dependencies { implementation(libs.kotlin.logging) implementation(libs.junit.jupiter.api) implementation(libs.assertj.core) } ================================================ FILE: diktat-common-test/src/main/kotlin/com/saveourtool/diktat/test/framework/processing/ResourceReader.kt ================================================ package com.saveourtool.diktat.test.framework.processing import com.saveourtool.diktat.test.framework.util.readTextOrNull import io.github.oshai.kotlinlogging.KotlinLogging import java.nio.file.Path import kotlin.io.path.createDirectories import kotlin.io.path.isRegularFile import kotlin.io.path.toPath import kotlin.io.path.writeText /** * A base interface to read resources for testing purposes */ fun interface ResourceReader : Function1 { /** * @param resourceName * @return [Path] for provider [resourceName] */ override fun invoke(resourceName: String): Path? companion object { private val log = KotlinLogging.logger {} /** * Default implementation of [ResourceReader] */ val default: ResourceReader = ResourceReader { resourceName -> ResourceReader::class.java .classLoader .getResource(resourceName) ?.toURI() ?.toPath() .also { path -> if (path == null || !path.isRegularFile()) { log.error { "Not able to find file for running test: $resourceName" } } } } /** * @param tempDir the temporary directory (usually injected by _JUnit_). * @param replacements a map of replacements which will be applied to actual and expected content before comparing. * @return Instance of [ResourceReader] with replacements of content */ fun ResourceReader.withReplacements( tempDir: Path, replacements: Map, ): ResourceReader = ResourceReader { resourceName -> this@withReplacements.invoke(resourceName) ?.let { originalFile -> tempDir.resolve(resourceName) .also { resultFile -> originalFile.readTextOrNull()?.replaceAll(replacements) ?.let { resultFile.parent.createDirectories() resultFile.writeText(it) } } } } /** * @param resourceFilePath a prefix for loading resources * @return Instance of [ResourceReader] which loads resource with [resourceFilePath] as prefix */ fun ResourceReader.withPrefix( resourceFilePath: String, ): ResourceReader = ResourceReader { resourceName -> this@withPrefix.invoke("$resourceFilePath/$resourceName") } private fun String.replaceAll(replacements: Map): String = replacements.entries .fold(this) { result, replacement -> result.replace(replacement.key, replacement.value) } } } ================================================ FILE: diktat-common-test/src/main/kotlin/com/saveourtool/diktat/test/framework/processing/TestComparatorUnit.kt ================================================ package com.saveourtool.diktat.test.framework.processing import com.saveourtool.diktat.test.framework.processing.ResourceReader.Companion.withPrefix import com.saveourtool.diktat.test.framework.util.NEWLINE import com.saveourtool.diktat.test.framework.util.readTextOrNull import com.saveourtool.diktat.test.framework.util.toUnixEndLines import io.github.oshai.kotlinlogging.KotlinLogging import java.nio.file.Path import kotlin.io.path.isRegularFile /** * Class that can apply transformation to an input file and then compare with expected result and output difference. * * @param resourceReader only used when the files are loaded as resources, * via [compareFilesFromResources]. * @param function a transformation that will be applied to the file */ @Suppress("ForbiddenComment", "TYPE_ALIAS") class TestComparatorUnit( private val resourceReader: ResourceReader = ResourceReader.default, private val function: (testFile: Path) -> String, ) { constructor( resourceFilePath: String, function: (testFile: Path) -> String, ) : this( resourceReader = ResourceReader.default.withPrefix(resourceFilePath), function = function, ) /** * @param expectedResult the name of the resource which has the expected * content. The trailing newline, if any, **won't be read** as a separate * empty string. So, if the content to be read from this file is expected * to be terminated with an empty string (which is the case if * `newlineAtEnd` is `true`), then the file should end with **two** * consecutive linebreaks. * @param testFileStr the name of the resource which has the original content. * @param overrideResourceReader function to override [ResourceReader] to read resource content * @return the result of file comparison by their content. * @see compareFilesFromFileSystem */ @Suppress("FUNCTION_BOOLEAN_PREFIX") fun compareFilesFromResources( expectedResult: String, testFileStr: String, overrideResourceReader: (ResourceReader) -> ResourceReader = { it }, ): TestFileContent { val overriddenResourceReader = overrideResourceReader(resourceReader) val expectedPath = overriddenResourceReader(expectedResult) val testPath = overriddenResourceReader(testFileStr) if (testPath == null || expectedPath == null) { log.error { "Not able to find files for running test: $expectedResult and $testFileStr" } return NotFoundResourcesTestFileContent( expectedResource = expectedResult, expectedPath = expectedPath, actualResource = testFileStr, actualPath = testPath, ) } return compareFilesFromFileSystem( expectedPath, testPath, ) } /** * @param expectedFile the file which has the expected content. The trailing * newline, if any, **won't be read** as a separate empty string. So, if * the content to be read from this file is expected to be terminated with * an empty string (which is the case if `newlineAtEnd` is `true`), then * the file should end with **two** consecutive linebreaks. * @param testFile the file which has the original content. * @return the result of file comparison by their content. * @see compareFilesFromResources */ @Suppress("FUNCTION_BOOLEAN_PREFIX") fun compareFilesFromFileSystem( expectedFile: Path, testFile: Path, ): TestFileContent { if (!testFile.isRegularFile() || !expectedFile.isRegularFile()) { log.error { "Not able to find files for running test: $expectedFile and $testFile" } return NotFoundFilesTestFileContent( expectedPath = expectedFile, actualPath = testFile, ) } val actualFileContent = function(testFile).toUnixEndLines() val expectedFileContent = expectedFile.readTextOrNull().orEmpty() return DefaultTestFileContent( actualContent = actualFileContent, expectedContent = expectedFileContent.withoutWarns(), ) } private companion object { private val log = KotlinLogging.logger {} private val warnRegex = (".*// ;warn:?(.*):(\\d*): (.+)").toRegex() /** * @return Expected result without lines with warns */ private fun String.withoutWarns(): String = split(NEWLINE) .filterNot { line -> line.contains(warnRegex) } .joinToString(NEWLINE.toString()) } } ================================================ FILE: diktat-common-test/src/main/kotlin/com/saveourtool/diktat/test/framework/processing/TestFileContent.kt ================================================ /** * It's a class container for test file content. * Plus exception cases when resource or file is not found */ package com.saveourtool.diktat.test.framework.processing import com.saveourtool.diktat.test.framework.util.describe import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.SoftAssertions.assertSoftly import org.intellij.lang.annotations.Language import java.nio.file.Path import kotlin.io.path.absolutePathString /** * A base interface for content of test file */ sealed interface TestFileContent { /** * Asserts [TestFileContent] that content are equal */ fun assertSuccessful() } /** * Implementation of [TestFileContent] when resources are not found * * @param expectedResource * @param expectedPath * @param actualResource * @param actualPath */ data class NotFoundResourcesTestFileContent( private val expectedResource: String, private val expectedPath: Path?, private val actualResource: String, private val actualPath: Path?, ) : TestFileContent { override fun assertSuccessful() { assertSoftly { softly -> softly.assertThat(expectedPath) .describedAs("Expected resource <%s>", expectedResource) .isNotNull softly.assertThat(actualPath) .describedAs("Actual resource <%s>", actualResource) .isNotNull } } } /** * Implementation of [TestFileContent] when files are not found * * @param expectedPath * @param actualPath */ data class NotFoundFilesTestFileContent( private val expectedPath: Path, private val actualPath: Path, ) : TestFileContent { override fun assertSuccessful() { assertSoftly { softly -> softly.assertThat(expectedPath) .describedAs("Expected file <%s>", expectedPath.absolutePathString()) .isRegularFile softly.assertThat(actualPath) .describedAs("Actual resource <%s>", actualPath.absolutePathString()) .isRegularFile } } } /** * The result of files being compared by their content. * * @param actualContent the actual file content (possibly slightly different * from the original after `diktat:check` is run). * @param expectedContent the expected file content without warns. */ data class DefaultTestFileContent( @Language("kotlin") private val actualContent: String, @Language("kotlin") private val expectedContent: String, ) : TestFileContent { override fun assertSuccessful() { assertThat(actualContent) .describedAs("lint result for ", actualContent.describe()) .isEqualTo(expectedContent) } } ================================================ FILE: diktat-common-test/src/main/kotlin/com/saveourtool/diktat/test/framework/util/TestUtils.kt ================================================ /** * Utility classes and methods for tests */ package com.saveourtool.diktat.test.framework.util import io.github.oshai.kotlinlogging.KotlinLogging import java.io.File import java.io.IOException import java.nio.charset.StandardCharsets import java.nio.file.FileVisitResult import java.nio.file.FileVisitResult.CONTINUE import java.nio.file.Files import java.nio.file.Files.walkFileTree import java.nio.file.NoSuchFileException import java.nio.file.Path import java.nio.file.SimpleFileVisitor import java.nio.file.attribute.BasicFileAttributes import kotlin.contracts.ExperimentalContracts import kotlin.contracts.contract import kotlin.io.path.Path import kotlin.io.path.absolute import kotlin.io.path.absolutePathString import kotlin.io.path.bufferedReader import kotlin.io.path.createDirectories import kotlin.io.path.deleteExisting import kotlin.io.path.deleteIfExists import kotlin.io.path.div import kotlin.io.path.isDirectory import kotlin.io.path.isSameFileAs import kotlin.io.path.readText const val NEWLINE = '\n' private val logger = KotlinLogging.logger {} /** * Deletes the file if it exists, retrying as necessary if the file is * blocked by another process (on Windows). * * @receiver the file or empty directory. * @see Path.deleteIfExists */ @Suppress( "EMPTY_BLOCK_STRUCTURE_ERROR", "MAGIC_NUMBER", ) fun Path.deleteIfExistsSilently() { val attempts = 10 val deleted = retry(attempts, delayMillis = 100L, lazyDefault = { false }) { deleteIfExists() /* * Ignore the return code of `deleteIfExists()` (will be `false` * if the file doesn't exist). */ true } if (!deleted) { logger.warn { "File \"${absolute()}\" not deleted after $attempts attempt(s)." } } } /** * Deletes this directory recursively. * * @see Path.deleteIfExistsRecursively */ fun Path.deleteRecursively() { walkFileTree(this, object : SimpleFileVisitor() { override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult { file.deleteIfExistsSilently() return CONTINUE } override fun postVisitDirectory(dir: Path, exc: IOException?): FileVisitResult { dir.deleteExisting() return CONTINUE } }) } /** * Deletes this directory recursively if it exists. * * @return `true` if the existing directory was successfully deleted, `false` if * the directory doesn't exist. * @see Files.deleteIfExists * @see Path.deleteRecursively */ @Suppress("FUNCTION_BOOLEAN_PREFIX") fun Path.deleteIfExistsRecursively(): Boolean = try { deleteRecursively() true } catch (_: NoSuchFileException) { false } /** * @receiver the 1st operand. * @param other the 2nd operand. * @return `true` if, and only if, the two paths locate the same `JAVA_HOME`. */ fun Path.isSameJavaHomeAs(other: Path): Boolean = isDirectory() && (isSameFileAsSafe(other) || resolve("jre").isSameFileAsSafe(other) || other.resolve("jre").isSameFileAsSafe(this)) /** * The same as [Path.isSameFileAs], but doesn't throw any [NoSuchFileException] * if either of the operands doesn't exist. * * @receiver the 1st operand. * @param other the 2nd operand. * @return `true` if, and only if, the two paths locate the same file. * @see Path.isSameFileAs */ fun Path.isSameFileAsSafe(other: Path): Boolean = try { isSameFileAs(other) } catch (_: NoSuchFileException) { false } /** * Requests that this file or directory be deleted when the JVM terminates. * * Does nothing if this [Path] is not associated with the default provider. * * @receiver a regular file or a directory. * @return this [Path]. */ fun Path.tryToDeleteOnExit(): Path { try { toFile().deleteOnExit() } catch (_: UnsupportedOperationException) { /* * Ignore. */ } return this } /** * Resets any permissions which might otherwise prevent from reading or writing * this file or directory, or traversing this directory. * * @receiver a regular file or a directory. */ fun Path.resetPermissions() { toFile().apply { setReadable(true) setWritable(true) if (isDirectory) { setExecutable(true) } } } /** * Returns a sequence containing only files whose content (the first * [linesToRead] lines) matches [lineRegex]. * * The operation is _intermediate_ and _stateless_. * * @receiver a sequence of regular files. * @param linesToRead the number of lines to read (at most). * @param lineRegex the regular expression to be applied to each line until a * match is found (i.e. the line is found which _contains_ [lineRegex]). * @return the filtered sequence. */ fun Sequence.filterContentMatches(linesToRead: Int, lineRegex: Regex): Sequence = filter { file -> file.bufferedReader().useLines { lines -> lines.take(linesToRead).any { line -> line.contains(lineRegex) } } } /** * Prepends the `PATH` of this process builder with [pathEntry]. * * @param pathEntry the entry to be prepended to the `PATH`. */ fun ProcessBuilder.prependPath(pathEntry: Path) { require(pathEntry.isDirectory()) { "$pathEntry is not a directory" } val environment = environment() val defaultPathKey = "PATH" val defaultWindowsPathKey = "Path" val pathKey = when { /*- * Keys of the Windows environment are case-insensitive ("PATH" == "Path"). * Keys of the Java interface to the environment are not ("PATH" != "Path"). * This is an attempt to work around the inconsistency. */ System.getProperty("os.name").isWindows() -> environment.keys.firstOrNull { key -> key.equals(defaultPathKey, ignoreCase = true) } ?: defaultWindowsPathKey else -> defaultPathKey } val pathSeparator = File.pathSeparatorChar val oldPath = environment[pathKey] val newPath = when { oldPath.isNullOrEmpty() -> pathEntry.toString() else -> "$pathEntry$pathSeparator$oldPath" } environment[pathKey] = newPath } /** * Inherits the home of the current JVM (by setting `JAVA_HOME` and adding it to * the `PATH`) for the children of this process builder. */ fun ProcessBuilder.inheritJavaHome() { val javaHome = System.getProperty("java.home") environment()["JAVA_HOME"] = javaHome prependPath(Path(javaHome) / "bin") } /** * Changes the temporary directory for the children of this process builder. * * @param temporaryDirectory the new temporary directory (created automatically, * scheduled for removal at JVM exit). */ fun ProcessBuilder.temporaryDirectory(temporaryDirectory: Path) { temporaryDirectory.createDirectories().tryToDeleteOnExit() /* * On UNIX, TMPDIR is the canonical name */ val environmentVariables: Sequence = when { System.getProperty("os.name").isWindows() -> sequenceOf("TMP", "TEMP") else -> sequenceOf("TMPDIR") } val environment = environment() val value = temporaryDirectory.absolutePathString() environmentVariables.forEach { name -> environment[name] = value } } /** * @receiver the value of `os.name` system property. * @return `true` if the value of `os.name` system property starts with * "Windows", `false` otherwise. */ @OptIn(ExperimentalContracts::class) fun String?.isWindows(): Boolean { contract { returns(true) implies (this@isWindows != null) } return this != null && startsWith("Windows") } /** * @return original [String] with unix end lines */ fun String.toUnixEndLines(): String = replace("\r\n", "\n").replace("\r", "\n") /** * @receiver the file whose content is to be read. * @return file content as a single [String], or null if an I/O error * has occurred. */ fun Path.readTextOrNull(): String? = try { readText(StandardCharsets.UTF_8).toUnixEndLines() } catch (e: IOException) { logger.error(e) { "Not able to read file: $this" } null } /** * @return a brief description of this code fragment. */ fun String.describe(): String { val lines = splitToSequence(NEWLINE) var first: String? = null val count = lines.onEachIndexed { index, line -> if (index == 0) { first = line } }.count() return when (count) { 1 -> "\"$this\"" else -> "\"$first\u2026\" ($count line(s))" } } /** * Retries the execution of the [block]. * * @param attempts the number of attempts (must be positive). * @param delayMillis the timeout (in milliseconds) between the consecutive * attempts. The default is 0. Ignored if [attempts] is 1. * @param lazyDefault allows to override the return value if none of the * attempts succeeds. By default, the last exception is thrown. * @param block the block to execute. * @return the result of the execution of the [block], or whatever [lazyDefault] * evaluates to if none of the attempts is successful. */ fun retry( attempts: Int, delayMillis: Long = 0L, lazyDefault: (Throwable) -> T = { error -> throw error }, block: () -> T ): T { require(attempts > 0) { "The number of attempts should be positive: $attempts" } var lastError: Throwable? = null repeat(attempts) { try { return block() } catch (error: Throwable) { lastError = error } if (delayMillis > 0L) { @Suppress("SleepInsteadOfDelay") Thread.sleep(delayMillis) } } return lazyDefault(lastError ?: Exception("The block was never executed")) } /** * Checks whether the current JVM's home matches the `JAVA_HOME` environment * variable. */ @Suppress("AVOID_NULL_CHECKS") fun checkForkedJavaHome() { val forkedJavaHome = System.getenv("JAVA_HOME") if (forkedJavaHome != null) { val javaHome = System.getProperty("java.home") if (javaHome != null && !Path(javaHome).isSameJavaHomeAs(Path(forkedJavaHome))) { logger.warn { "Current JDK home is $javaHome. Forked tests may use a different JDK at $forkedJavaHome." } } logger.warn { "Make sure JAVA_HOME ($forkedJavaHome) points to a Java 8 or Java 11 home. Java 17 is not yet supported." } } } ================================================ FILE: diktat-dev-ksp/build.gradle.kts ================================================ plugins { id("com.saveourtool.diktat.buildutils.kotlin-jvm-configuration") id("com.saveourtool.diktat.buildutils.code-quality-convention") } dependencies { implementation(libs.kotlin.ksp.api) } sequenceOf("diktatFix", "diktatCheck").forEach { diktatTaskName -> tasks.findByName(diktatTaskName)?.dependsOn( tasks.named("compileKotlin"), tasks.named("processResources"), ) } ================================================ FILE: diktat-dev-ksp/src/main/kotlin/com/saveourtool/diktat/ruleset/generation/EnumNames.kt ================================================ package com.saveourtool.diktat.ruleset.generation /** * Annotation that marks to generate an object with names from Enum * * @property generatedPackageName * @property generatedClassName */ @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.SOURCE) annotation class EnumNames( val generatedPackageName: String, val generatedClassName: String, ) ================================================ FILE: diktat-dev-ksp/src/main/kotlin/com/saveourtool/diktat/ruleset/generation/EnumNamesSymbolProcessor.kt ================================================ package com.saveourtool.diktat.ruleset.generation import com.google.devtools.ksp.processing.CodeGenerator import com.google.devtools.ksp.processing.Dependencies import com.google.devtools.ksp.processing.Resolver import com.google.devtools.ksp.processing.SymbolProcessor import com.google.devtools.ksp.symbol.ClassKind import com.google.devtools.ksp.symbol.KSAnnotated import com.google.devtools.ksp.symbol.KSAnnotation import com.google.devtools.ksp.symbol.KSClassDeclaration /** * [SymbolProcessor] to generate a class with contacts for names from provided enum * * @param codeGenerator */ class EnumNamesSymbolProcessor( private val codeGenerator: CodeGenerator, ) : SymbolProcessor { override fun process(resolver: Resolver): List { resolver.getEnumDeclarations().forEach { doProcess(resolver, it) } return emptyList() } private fun doProcess(resolver: Resolver, enumDeclaration: KSClassDeclaration) { val annotation = enumDeclaration.annotations .single { it.shortName.asString() == EnumNames::class.simpleName } val targetPackageName = annotation.getArgumentValue("generatedPackageName") val targetClassName = annotation.getArgumentValue("generatedClassName") if (resolver.isAlreadyGenerated(targetPackageName, targetClassName)) { return } codeGenerator.createNewFile( dependencies = Dependencies(false), packageName = targetPackageName, fileName = targetClassName, ).bufferedWriter() .use { writer -> writer.write(autoGenerationComment) writer.newLine() writer.write("package $targetPackageName\n") writer.newLine() writer.write("import kotlin.String\n") writer.newLine() writer.write("object $targetClassName {\n") enumDeclaration.declarations .filterIsInstance() .filter { it.classKind == ClassKind.ENUM_ENTRY } .map { it.simpleName.asString() } .forEach { enumEntryName -> writer.write(" const val $enumEntryName: String = \"$enumEntryName\"\n") } writer.write("}\n") } } companion object { /** * The comment that will be added to the generated sources file. */ private val autoGenerationComment = """ |/** | * This document was auto generated, please don't modify it. | * This document contains all enum properties from Warnings.kt as Strings. | */ """.trimMargin() private val annotationName: String = requireNotNull(EnumNames::class.qualifiedName) { "Failed to retrieve a qualified name from ${EnumNames::class}" } private fun Resolver.getEnumDeclarations(): Sequence = getSymbolsWithAnnotation(annotationName) .filterIsInstance() .onEach { candidate -> require(candidate.classKind == ClassKind.ENUM_CLASS) { "Annotated class ${candidate.qualifiedName} is not enum" } } private fun KSAnnotation.getArgumentValue(argumentName: String): String = arguments .singleOrNull { it.name?.asString() == argumentName } .let { argument -> requireNotNull(argument) { "Not found $argumentName in $this" } } .value ?.let { it as? String } .let { argumentValue -> requireNotNull(argumentValue) { "Not found a value for $argumentName in $this" } } private fun Resolver.isAlreadyGenerated( packageName: String, className: String, ): Boolean = getNewFiles() .find { it.packageName.asString() == packageName && it.fileName == "$className.kt" } ?.let { true } ?: false } } ================================================ FILE: diktat-dev-ksp/src/main/kotlin/com/saveourtool/diktat/ruleset/generation/EnumNamesSymbolProcessorProvider.kt ================================================ package com.saveourtool.diktat.ruleset.generation import com.google.devtools.ksp.processing.SymbolProcessor import com.google.devtools.ksp.processing.SymbolProcessorEnvironment import com.google.devtools.ksp.processing.SymbolProcessorProvider /** * [SymbolProcessorProvider] for [EnumNamesSymbolProcessor] */ class EnumNamesSymbolProcessorProvider : SymbolProcessorProvider { override fun create( environment: SymbolProcessorEnvironment, ): SymbolProcessor = EnumNamesSymbolProcessor( codeGenerator = environment.codeGenerator, ) } ================================================ FILE: diktat-dev-ksp/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider ================================================ com.saveourtool.diktat.ruleset.generation.EnumNamesSymbolProcessorProvider ================================================ FILE: diktat-gradle-plugin/README.md ================================================ ## Local testing You can build and publish to maven local by using `gradlew :diktat-gradle-plugin:publishToMavenLocal`. Then you can use a built version in projects in examples. A calculated version will be printed in logs by `reckon` plugin. ================================================ FILE: diktat-gradle-plugin/build.gradle.kts ================================================ import com.saveourtool.diktat.buildutils.configurePom import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform.getCurrentOperatingSystem import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.KotlinVersion import org.jetbrains.kotlin.gradle.tasks.KotlinCompile @Suppress("DSL_SCOPE_VIOLATION", "RUN_IN_SCRIPT") // https://github.com/gradle/gradle/issues/22797 plugins { id("com.saveourtool.diktat.buildutils.kotlin-jvm-configuration") id("com.saveourtool.diktat.buildutils.code-quality-convention") id("com.saveourtool.diktat.buildutils.publishing-configuration") id("pl.droidsonroids.jacoco.testkit") version "1.0.12" id("org.gradle.test-retry") version "1.5.8" id("com.gradle.plugin-publish") version "1.2.1" alias(libs.plugins.shadow) } dependencies { implementation(kotlin("gradle-plugin-api")) implementation(projects.diktatRunner) // merge sarif reports implementation(libs.sarif4k.jvm) implementation(libs.kotlinx.serialization.json) testImplementation(libs.junit.jupiter.api) testRuntimeOnly(libs.junit.jupiter.engine) testImplementation(projects.diktatKtlintEngine) testImplementation(libs.ktlint.cli.reporter.core) testImplementation(libs.ktlint.cli.reporter.json) testImplementation(libs.ktlint.cli.reporter.plain) testImplementation(libs.ktlint.cli.reporter.sarif) } tasks.withType { compilerOptions { // kotlin 1.4 api is the latest support version in kotlin 1.9 // min supported Gradle is 7.0 languageVersion.set(KotlinVersion.KOTLIN_2_0) apiVersion.set(KotlinVersion.KOTLIN_2_0) jvmTarget.set(JvmTarget.JVM_1_8) } } tasks.named("shadowJar") { archiveClassifier.set("") duplicatesStrategy = DuplicatesStrategy.FAIL // all kotlin libs relocate("org.jetbrains", "shadow.org.jetbrains") } gradlePlugin { website = "https://diktat.saveourtool.com/" vcsUrl = "https://github.com/saveourtool/diktat" plugins { create("diktatPlugin") { id = "com.saveourtool.diktat" displayName = "Static code analysis for Kotlin" description = "Strict coding standard for Kotlin and a custom set of rules for detecting code smells, code style issues and bugs" tags = listOf("kotlin", "code-analysis") implementationClass = "com.saveourtool.diktat.plugin.gradle.DiktatGradlePlugin" } } } afterEvaluate { publishing { publications { withType { pom { configurePom(project) } } } } } // === testing & code coverage, jacoco is run independent from maven val functionalTestTask by tasks.register("functionalTest") tasks.withType { useJUnitPlatform() } // === integration testing // fixme: should probably use KotlinSourceSet instead val functionalTest: SourceSet = sourceSets.create("functionalTest") { compileClasspath += sourceSets.main.get().output + configurations.testRuntimeClasspath.get() runtimeClasspath += output + compileClasspath } @Suppress("GENERIC_VARIABLE_WRONG_DECLARATION", "MAGIC_NUMBER") val functionalTestProvider: TaskProvider = tasks.named("functionalTest") { shouldRunAfter("test") testClassesDirs = functionalTest.output.classesDirs classpath = functionalTest.runtimeClasspath maxParallelForks = Runtime.getRuntime().availableProcessors() maxHeapSize = "1024m" retry { failOnPassedAfterRetry.set(false) maxFailures.set(10) maxRetries.set(3) } doLast { if (getCurrentOperatingSystem().isWindows) { // workaround for https://github.com/koral--/jacoco-gradle-testkit-plugin/issues/9 logger.lifecycle("Sleeping for 5 sec after functionalTest to avoid error with file locking") Thread.sleep(5_000) } } finalizedBy(tasks.jacocoTestReport) } tasks.check { dependsOn(tasks.jacocoTestReport) } jacocoTestKit { @Suppress("UNCHECKED_CAST") applyTo("functionalTestRuntimeOnly", functionalTestProvider as TaskProvider) } tasks.jacocoTestReport { shouldRunAfter(tasks.withType()) executionData( layout.buildDirectory .dir("jacoco") .map { jacocoDir -> jacocoDir.asFileTree .matching { include("*.exec") } } ) reports { // xml report is used by codecov xml.required.set(true) } } ================================================ FILE: diktat-gradle-plugin/src/functionalTest/kotlin/com/saveourtool/diktat/plugin/gradle/DiktatGradlePluginFunctionalTest.kt ================================================ package com.saveourtool.diktat.plugin.gradle import com.saveourtool.diktat.plugin.gradle.DiktatGradlePlugin.Companion.DIKTAT_CHECK_TASK import org.gradle.buildinit.plugins.internal.modifiers.BuildInitDsl import org.gradle.internal.impldep.org.junit.rules.TemporaryFolder import org.gradle.testkit.runner.TaskOutcome import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import java.io.File class DiktatGradlePluginFunctionalTest { private val testProjectDir = TemporaryFolder() private lateinit var buildFile: File @BeforeEach fun setUp() { testProjectDir.create() val buildInitDsl = BuildInitDsl.KOTLIN createExampleProject(testProjectDir, File("../examples/gradle-kotlin-dsl"), buildInitDsl) buildFile = testProjectDir.root.resolve(buildInitDsl.fileNameFor("build")) } @AfterEach fun tearDown() { testProjectDir.delete() } @Test fun `should execute diktatCheck on default values`() { val result = runDiktat(testProjectDir, shouldSucceed = false) val diktatCheckBuildResult = result.task(":$DIKTAT_CHECK_TASK") requireNotNull(diktatCheckBuildResult) Assertions.assertEquals(TaskOutcome.FAILED, diktatCheckBuildResult.outcome) Assertions.assertTrue( result.output.contains("[FILE_NAME_MATCH_CLASS]") ) } @Test fun `should have json reporter files`() { buildFile.appendText( """${System.lineSeparator()} diktat { inputs { include("src/**/*.kt") } reporter = "json" output = "test.txt" } """.trimIndent() ) val result = runDiktat(testProjectDir, shouldSucceed = false) val diktatCheckBuildResult = result.task(":$DIKTAT_CHECK_TASK") requireNotNull(diktatCheckBuildResult) Assertions.assertEquals(TaskOutcome.FAILED, diktatCheckBuildResult.outcome) val file = testProjectDir.root.walkTopDown().filter { it.name == "test.txt" }.first() Assertions.assertNotNull(file) Assertions.assertTrue( file.readLines().any { it.contains("[FILE_NAME_MATCH_CLASS]") } ) } @Test fun `should execute diktatCheck with explicit inputs`() { buildFile.appendText( """${System.lineSeparator()} diktat { inputs { include("src/**/*.kt") } } """.trimIndent() ) val result = runDiktat(testProjectDir, shouldSucceed = false) val diktatCheckBuildResult = result.task(":$DIKTAT_CHECK_TASK") requireNotNull(diktatCheckBuildResult) Assertions.assertEquals(TaskOutcome.FAILED, diktatCheckBuildResult.outcome) Assertions.assertTrue( result.output.contains("[FILE_NAME_MATCH_CLASS]") ) } @Test fun `should execute diktatCheck with excludes`() { buildFile.appendText( """${System.lineSeparator()} diktat { inputs { include("src/**/*.kt") exclude("src/**/Test.kt") } } """.trimIndent() ) val result = runDiktat(testProjectDir, shouldSucceed = false) val diktatCheckBuildResult = result.task(":$DIKTAT_CHECK_TASK") requireNotNull(diktatCheckBuildResult) Assertions.assertEquals(TaskOutcome.FAILED, diktatCheckBuildResult.outcome) } @Test fun `should not run diktat with ktlint's default includes when no files match include patterns`() { buildFile.appendText( """${System.lineSeparator()} diktat { inputs { include ("nonexistent-directory/src/**/*.kt") } } """.trimIndent() ) val result = runDiktat(testProjectDir, arguments = listOf("--info")) val diktatCheckBuildResult = result.task(":$DIKTAT_CHECK_TASK") requireNotNull(diktatCheckBuildResult) Assertions.assertEquals(TaskOutcome.NO_SOURCE, diktatCheckBuildResult.outcome) Assertions.assertFalse( result.output.contains("Skipping diktat execution") ) } @Test fun `should execute diktatCheck with gradle older than 6_4`() { val result = runDiktat(testProjectDir, shouldSucceed = false, arguments = listOf("--info")) { withGradleVersion("5.3") } val diktatCheckBuildResult = result.task(":$DIKTAT_CHECK_TASK") requireNotNull(diktatCheckBuildResult) Assertions.assertEquals(TaskOutcome.FAILED, diktatCheckBuildResult.outcome) Assertions.assertTrue( result.output.contains("[FILE_NAME_MATCH_CLASS]") ) } @Test fun `should respect ignoreFailures setting`() { buildFile.appendText( """${System.lineSeparator()} diktat { ignoreFailures = true } """.trimIndent() ) val result = runDiktat(testProjectDir, shouldSucceed = true, arguments = listOf("--info")) val diktatCheckBuildResult = result.task(":$DIKTAT_CHECK_TASK") requireNotNull(diktatCheckBuildResult) Assertions.assertEquals(TaskOutcome.SUCCESS, diktatCheckBuildResult.outcome) Assertions.assertTrue( result.output.contains("[FILE_NAME_MATCH_CLASS]") ) } } ================================================ FILE: diktat-gradle-plugin/src/functionalTest/kotlin/com/saveourtool/diktat/plugin/gradle/DiktatGradlePluginGroovyFunctionalTest.kt ================================================ package com.saveourtool.diktat.plugin.gradle import org.gradle.buildinit.plugins.internal.modifiers.BuildInitDsl import org.gradle.internal.impldep.org.junit.rules.TemporaryFolder import org.gradle.testkit.runner.TaskOutcome import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import java.io.File class DiktatGradlePluginGroovyFunctionalTest { private val testProjectDir = TemporaryFolder() private lateinit var buildFile: File @BeforeEach fun setUp() { testProjectDir.create() val buildInitDsl = BuildInitDsl.GROOVY createExampleProject(testProjectDir, File("../examples/gradle-groovy-dsl"), buildInitDsl) buildFile = testProjectDir.root.resolve(buildInitDsl.fileNameFor("build")) } @AfterEach fun tearDown() { testProjectDir.delete() } @Test fun `should execute diktatCheck with default values`() { val result = runDiktat(testProjectDir, shouldSucceed = false) assertDiktatExecuted(result) } @Test fun `should execute diktatCheck with explicit configuration`() { buildFile.appendText( """${System.lineSeparator()} diktat { inputs { it.include("src/**/*.kt") } reporter = "plain" diktatConfigFile = file(rootDir.path + "/diktat-analysis.yml") } """.trimIndent() ) val result = runDiktat(testProjectDir, shouldSucceed = false) assertDiktatExecuted(result) } } ================================================ FILE: diktat-gradle-plugin/src/functionalTest/kotlin/com/saveourtool/diktat/plugin/gradle/DiktatGradlePluginMultiprojectFunctionalTest.kt ================================================ package com.saveourtool.diktat.plugin.gradle import org.gradle.buildinit.plugins.internal.modifiers.BuildInitDsl import org.gradle.internal.impldep.org.junit.rules.TemporaryFolder import org.gradle.testkit.runner.TaskOutcome import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import java.io.File class DiktatGradlePluginMultiprojectFunctionalTest { private val testProjectDir = TemporaryFolder() private lateinit var buildFile: File @BeforeEach fun setUp() { testProjectDir.create() val buildInitDsl = BuildInitDsl.KOTLIN File("../examples/gradle-kotlin-dsl-multiproject").copyRecursively(testProjectDir.root) buildFile = testProjectDir.root.resolve(buildInitDsl.fileNameFor("build")) } @AfterEach fun tearDown() { testProjectDir.delete() } @Test fun `should execute diktatCheck on default values in multiproject build`() { val result = runDiktat(testProjectDir, shouldSucceed = false) assertDiktatExecuted(result, TaskOutcome.NO_SOURCE) { "Task for root project with empty sources should succeed" } } } ================================================ FILE: diktat-gradle-plugin/src/functionalTest/kotlin/com/saveourtool/diktat/plugin/gradle/Utils.kt ================================================ package com.saveourtool.diktat.plugin.gradle import org.gradle.buildinit.plugins.internal.modifiers.BuildInitDsl import org.gradle.internal.impldep.org.junit.rules.TemporaryFolder import org.gradle.testkit.runner.BuildResult import org.gradle.testkit.runner.GradleRunner import org.gradle.testkit.runner.TaskOutcome import org.junit.jupiter.api.Assertions import java.io.File import java.util.concurrent.atomic.AtomicInteger internal val testsCounter = AtomicInteger(0) internal fun createExampleProject(testProjectDir: TemporaryFolder, exampleProject: File, buildInitDsl: BuildInitDsl ) { exampleProject.copyRecursively(testProjectDir.root) val buildFileName = buildInitDsl.fileNameFor("build") File(testProjectDir.root, buildFileName).delete() testProjectDir.newFile(buildFileName).writeText( """ plugins { id("com.saveourtool.diktat") } repositories { mavenLocal() mavenCentral() } """.trimIndent() ) } /** * @param arguments additional arguments to pass to [GradleRunner] */ internal fun runDiktat(testProjectDir: TemporaryFolder, shouldSucceed: Boolean = true, arguments: List = emptyList(), configureRunner: GradleRunner.() -> GradleRunner = { this } ) = GradleRunner.create() .run(configureRunner) .withProjectDir(testProjectDir.root) .withArguments(arguments + DiktatGradlePlugin.DIKTAT_CHECK_TASK) .withPluginClasspath() .withJaCoCo(testsCounter.incrementAndGet()) .forwardOutput() .runCatching { if (shouldSucceed) build() else buildAndFail() } .also { require(it.isSuccess) { val ex = it.exceptionOrNull() "Running gradle returned exception $ex, cause: ${ex?.cause}" } } .getOrNull() .let { requireNotNull(it) { "Failed to get build result from running diktat" } } /** * This is support for jacoco reports in tests run with gradle TestKit */ private fun GradleRunner.withJaCoCo(number: Int) = apply { javaClass.classLoader .getResourceAsStream("testkit-gradle.properties") .also { it ?: error("properties file for testkit is not available, check build configuration") } ?.use { propertiesFileStream -> val text = propertiesFileStream.reader().readText() File(projectDir, "gradle.properties").createNewFile() File(projectDir, "gradle.properties").writer().use { it.write(text.replace("functionalTest.exec", "functionalTest-$number.exec")) } } } fun assertDiktatExecuted( result: BuildResult, taskOutcome: TaskOutcome = TaskOutcome.FAILED, errorMessage: () -> String? = { null } ) { val diktatCheckBuildResult = result.task(":${DiktatGradlePlugin.DIKTAT_CHECK_TASK}") requireNotNull(diktatCheckBuildResult) Assertions.assertEquals(taskOutcome, diktatCheckBuildResult.outcome, errorMessage) Assertions.assertTrue( result.output.contains("[FILE_NAME_MATCH_CLASS]") ) { "Task ${DiktatGradlePlugin.DIKTAT_CHECK_TASK} wasn't run" } } ================================================ FILE: diktat-gradle-plugin/src/main/kotlin/com/saveourtool/diktat/plugin/gradle/DiktatExtension.kt ================================================ package com.saveourtool.diktat.plugin.gradle import com.saveourtool.diktat.plugin.gradle.extension.Reporters import org.gradle.api.Action import org.gradle.api.model.ObjectFactory import org.gradle.api.tasks.InputFile import org.gradle.api.tasks.Internal import org.gradle.api.tasks.Optional import org.gradle.api.tasks.PathSensitive import org.gradle.api.tasks.PathSensitivity import org.gradle.api.tasks.util.PatternFilterable import org.gradle.api.tasks.util.PatternSet import java.io.File import javax.inject.Inject /** * An extension to configure diktat in build.gradle(.kts) file * * @param objectFactory * @param patternSet */ open class DiktatExtension @Inject constructor( objectFactory: ObjectFactory, private val patternSet: PatternSet, ) { /** * All reporters */ @get:Internal val reporters: Reporters = objectFactory.newInstance(Reporters::class.java) /** * Boolean flag to support `ignoreFailures` property of [VerificationTask]. */ var ignoreFailures: Boolean = false /** * Flag that indicates whether to turn debug logging on */ var debug = false /** * Property that will be used if you need to publish the report to GitHub */ var githubActions = false /** * Baseline file, containing a list of errors that will be ignored. * If this file doesn't exist, it will be created on the first invocation. */ var baseline: String? = null /** * Path to diktat yml config file. Can be either absolute or relative to project's root directory. * Default value: `diktat-analysis.yml` in rootDir if it exists or default (empty) configuration */ @get:InputFile @get:Optional @get:PathSensitive(PathSensitivity.RELATIVE) var diktatConfigFile: File? = null /** * Configure input files for diktat task * * @param action configuration lambda for `PatternFilterable` */ fun inputs(action: PatternFilterable.() -> Unit) { action(patternSet) } /** * Configure reporters * * @param action configuration lambda for [Reporters] */ fun reporters(action: Action): Unit = action.execute(reporters) } ================================================ FILE: diktat-gradle-plugin/src/main/kotlin/com/saveourtool/diktat/plugin/gradle/DiktatGradlePlugin.kt ================================================ package com.saveourtool.diktat.plugin.gradle import com.saveourtool.diktat.plugin.gradle.tasks.DiktatCheckTask.Companion.registerDiktatCheckTask import com.saveourtool.diktat.plugin.gradle.tasks.DiktatFixTask.Companion.registerDiktatFixTask import com.saveourtool.diktat.plugin.gradle.tasks.configureMergeReportsTask import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.tasks.util.PatternSet /** * Plugin that configures diktat and registers tasks to run diktat */ class DiktatGradlePlugin : Plugin { /** * @param project a gradle [Project] that the plugin is applied to */ override fun apply(project: Project) { val patternSet = PatternSet() val diktatExtension = project.extensions.create( DIKTAT_EXTENSION, DiktatExtension::class.java, patternSet, ).apply { diktatConfigFile = project.rootProject.file("diktat-analysis.yml") } project.registerDiktatCheckTask(diktatExtension, patternSet) project.registerDiktatFixTask(diktatExtension, patternSet) project.configureMergeReportsTask() } companion object { /** * Task to check diKTat */ const val DIKTAT_CHECK_TASK = "diktatCheck" /** * DiKTat extension */ const val DIKTAT_EXTENSION = "diktat" /** * Task to run diKTat with fix */ const val DIKTAT_FIX_TASK = "diktatFix" /** * Name of the task that merges SARIF reports of diktat tasks */ internal const val MERGE_SARIF_REPORTS_TASK_NAME = "mergeDiktatReports" } } ================================================ FILE: diktat-gradle-plugin/src/main/kotlin/com/saveourtool/diktat/plugin/gradle/Utils.kt ================================================ /** * Utilities for diktat gradle plugin */ package com.saveourtool.diktat.plugin.gradle import org.gradle.api.Project import org.gradle.api.file.RegularFile import org.gradle.api.provider.Provider import org.gradle.api.reporting.ReportingExtension /** * @param fileName * @param extension * @return default location of report with provided [extension] */ internal fun Project.defaultReportLocation( extension: String, fileName: String = "diktat", ): Provider = project.layout .buildDirectory .file("${ReportingExtension.DEFAULT_REPORTS_DIR_NAME}/diktat/$fileName.$extension") ================================================ FILE: diktat-gradle-plugin/src/main/kotlin/com/saveourtool/diktat/plugin/gradle/extension/DefaultReporter.kt ================================================ /** * All default reporters */ @file:Suppress("UnnecessaryAbstractClass") package com.saveourtool.diktat.plugin.gradle.extension import com.saveourtool.diktat.api.DiktatReporterCreationArguments import com.saveourtool.diktat.api.DiktatReporterType import com.saveourtool.diktat.plugin.gradle.defaultReportLocation import org.gradle.api.Project import org.gradle.api.file.RegularFile import org.gradle.api.file.RegularFileProperty import org.gradle.api.model.ObjectFactory import org.gradle.api.provider.Provider import java.io.File import java.nio.file.Files import java.nio.file.Path import javax.inject.Inject /** * A base interface for reporter * * @param objectFactory * @param project * @property type type of reporter */ abstract class DefaultReporter @Inject constructor( val type: DiktatReporterType, objectFactory: ObjectFactory, project: Project, ) : Reporter { override val output: RegularFileProperty = objectFactory.fileProperty() .also { fileProperty -> fileProperty.convention(project.defaultReportLocation(extension = type.extension)) } override fun toCreationArguments(sourceRootDir: Path): DiktatReporterCreationArguments = DiktatReporterCreationArguments( reporterType = type, outputStream = output.map { file -> file.asFile.also { Files.createDirectories(it.parentFile.toPath()) }.outputStream() }.orNull, sourceRootDir = sourceRootDir.takeIf { type == DiktatReporterType.SARIF }, ) } /** * Plain reporter * * @param objectFactory * @param project */ abstract class PlainReporter @Inject constructor( objectFactory: ObjectFactory, project: Project, ) : DefaultReporter( type = DiktatReporterType.PLAIN, objectFactory, project, ) { /** * Remove the default value for plain to print to stdout by default */ override val output: RegularFileProperty = objectFactory.fileProperty() .also { fileProperty -> fileProperty.set(null as File?) } } /** * JSON reporter * * @param objectFactory * @param project */ abstract class JsonReporter @Inject constructor( objectFactory: ObjectFactory, project: Project, ) : DefaultReporter( type = DiktatReporterType.JSON, objectFactory, project, ) /** * SARIF reporter * * @param objectFactory * @param project */ abstract class SarifReporter @Inject constructor( objectFactory: ObjectFactory, project: Project, ) : DefaultReporter( type = DiktatReporterType.SARIF, objectFactory, project, ) /** * GitHub actions reporter * * @param objectFactory * @param project */ abstract class GitHubActionsReporter @Inject constructor( project: Project, objectFactory: ObjectFactory, ) : SarifReporter(objectFactory, project) { override val output: RegularFileProperty = objectFactory.fileProperty() .also { fileProperty -> fileProperty.convention(project.getGitHubActionReporterOutput()) .finalizeValue() } /** * Location for merged output */ val mergeOutput: RegularFileProperty = objectFactory.fileProperty() .also { fileProperty -> fileProperty.convention(project.getGitHubActionReporterMergeOutput()) .finalizeValue() } companion object { /** * @return [RegularFile] for output */ fun Project.getGitHubActionReporterOutput(): Provider = defaultReportLocation(extension = "sarif") /** * @return [RegularFile] for mergeOutput */ fun Project.getGitHubActionReporterMergeOutput(): Provider = rootProject.defaultReportLocation(fileName = "diktat-merged", extension = "sarif") } } /** * Checkstyle reporter * * @param objectFactory * @param project */ abstract class CheckstyleReporter @Inject constructor( objectFactory: ObjectFactory, project: Project, ) : DefaultReporter( type = DiktatReporterType.CHECKSTYLE, objectFactory, project, ) /** * HTML reporter * * @param objectFactory * @param project */ abstract class HtmlReporter @Inject constructor( objectFactory: ObjectFactory, project: Project, ) : DefaultReporter( type = DiktatReporterType.HTML, objectFactory, project, ) ================================================ FILE: diktat-gradle-plugin/src/main/kotlin/com/saveourtool/diktat/plugin/gradle/extension/Reporter.kt ================================================ package com.saveourtool.diktat.plugin.gradle.extension import com.saveourtool.diktat.api.DiktatReporterCreationArguments import org.gradle.api.file.RegularFileProperty import org.gradle.api.tasks.OutputFile import java.nio.file.Path /** * A base interface for reporter */ interface Reporter { /** * Location for output */ @get:OutputFile val output: RegularFileProperty /** * @param sourceRootDir * @return [DiktatReporterCreationArguments] to create this reporter */ fun toCreationArguments(sourceRootDir: Path): DiktatReporterCreationArguments } ================================================ FILE: diktat-gradle-plugin/src/main/kotlin/com/saveourtool/diktat/plugin/gradle/extension/Reporters.kt ================================================ package com.saveourtool.diktat.plugin.gradle.extension import org.gradle.api.Action import org.gradle.api.model.ObjectFactory import javax.inject.Inject /** * Configuration for reporters */ open class Reporters @Inject constructor( private val objectFactory: ObjectFactory, ) { /** * All reporters */ val all: MutableList = mutableListOf() /** * Configure *plain* reporter with [action] configuration * * @param action */ fun plain(action: Action): Unit = action.execute(newReporter()) /** * Configure *plain* reporter with default configuration */ fun plain() { plain(emptyAction()) } /** * Configure *json* reporter with [action] configuration * * @param action */ fun json(action: Action): Unit = action.execute(newReporter()) /** * Configure *json* reporter with default configuration */ fun json() { json(emptyAction()) } /** * Configure *sarif* reporter with [action] configuration * * @param action */ fun sarif(action: Action): Unit = action.execute(newReporter()) /** * Configure *sarif* reporter with default configuration */ fun sarif() { sarif(emptyAction()) } /** * Configure *sarif* reporter for GitHub actions */ fun gitHubActions() { newReporter() } /** * Configure *checkstyle* reporter with [action] configuration * * @param action */ fun checkstyle(action: Action): Unit = action.execute(newReporter()) /** * Configure *checkstyle* reporter with default configuration */ fun checkstyle() { checkstyle(emptyAction()) } /** * Configure *html* reporter with default configuration */ fun html() { html(emptyAction()) } /** * Configure *html* reporter with [action] configuration * * @param action */ fun html(action: Action): Unit = action.execute(newReporter()) private inline fun newReporter(): T = objectFactory.newInstance(T::class.java) .apply { all.add(this) } private inline fun emptyAction() = Action { } } ================================================ FILE: diktat-gradle-plugin/src/main/kotlin/com/saveourtool/diktat/plugin/gradle/tasks/DiktatCheckTask.kt ================================================ package com.saveourtool.diktat.plugin.gradle.tasks import com.saveourtool.diktat.DiktatRunner import com.saveourtool.diktat.DiktatRunnerArguments import com.saveourtool.diktat.plugin.gradle.DiktatExtension import com.saveourtool.diktat.plugin.gradle.DiktatGradlePlugin import org.gradle.api.Project import org.gradle.api.model.ObjectFactory import org.gradle.api.tasks.TaskProvider import org.gradle.api.tasks.util.PatternFilterable import org.gradle.api.tasks.util.PatternSet import javax.inject.Inject /** * A task to check source code by diktat */ abstract class DiktatCheckTask @Inject constructor( extension: DiktatExtension, inputs: PatternFilterable, objectFactory: ObjectFactory, ) : DiktatTaskBase( extension, inputs, objectFactory ) { override fun doRun( runner: DiktatRunner, args: DiktatRunnerArguments ): Int = runner.checkAll(args) companion object { /** * @param diktatExtension [DiktatExtension] with some values for task configuration * @param patternSet [PatternSet] to discover files for diktat check * @return a [TaskProvider] */ fun Project.registerDiktatCheckTask( diktatExtension: DiktatExtension, patternSet: PatternSet, ): TaskProvider = tasks.register( DiktatGradlePlugin.DIKTAT_CHECK_TASK, DiktatCheckTask::class.java, diktatExtension, patternSet, ).also { it.configure(diktatExtension) } } } ================================================ FILE: diktat-gradle-plugin/src/main/kotlin/com/saveourtool/diktat/plugin/gradle/tasks/DiktatFixTask.kt ================================================ package com.saveourtool.diktat.plugin.gradle.tasks import com.saveourtool.diktat.DiktatRunner import com.saveourtool.diktat.DiktatRunnerArguments import com.saveourtool.diktat.plugin.gradle.DiktatExtension import com.saveourtool.diktat.plugin.gradle.DiktatGradlePlugin import org.gradle.api.Project import org.gradle.api.model.ObjectFactory import org.gradle.api.tasks.TaskProvider import org.gradle.api.tasks.util.PatternFilterable import org.gradle.api.tasks.util.PatternSet import javax.inject.Inject /** * A task to check source code by diktat */ abstract class DiktatFixTask @Inject constructor( extension: DiktatExtension, inputs: PatternFilterable, objectFactory: ObjectFactory, ) : DiktatTaskBase( extension, inputs, objectFactory ) { override fun doRun( runner: DiktatRunner, args: DiktatRunnerArguments ): Int = runner.fixAll(args) { updatedFile -> project.logger.info("Original and formatted content differ, writing to ${updatedFile.fileName}...") } companion object { /** * @param diktatExtension [DiktatExtension] with some values for task configuration * @param patternSet [PatternSet] to discover files for diktat fix * @return a [TaskProvider] */ fun Project.registerDiktatFixTask( diktatExtension: DiktatExtension, patternSet: PatternSet, ): TaskProvider = tasks.register( DiktatGradlePlugin.DIKTAT_FIX_TASK, DiktatFixTask::class.java, diktatExtension, patternSet, ).also { it.configure(diktatExtension) } } } ================================================ FILE: diktat-gradle-plugin/src/main/kotlin/com/saveourtool/diktat/plugin/gradle/tasks/DiktatTaskBase.kt ================================================ package com.saveourtool.diktat.plugin.gradle.tasks import com.saveourtool.diktat.DiktatRunner import com.saveourtool.diktat.DiktatRunnerArguments import com.saveourtool.diktat.DiktatRunnerFactory import com.saveourtool.diktat.ENGINE_INFO import com.saveourtool.diktat.api.DiktatProcessorListener import com.saveourtool.diktat.api.DiktatReporterCreationArguments import com.saveourtool.diktat.api.DiktatReporterType import com.saveourtool.diktat.diktatRunnerFactory import com.saveourtool.diktat.plugin.gradle.DiktatExtension import com.saveourtool.diktat.plugin.gradle.extension.DefaultReporter import com.saveourtool.diktat.plugin.gradle.extension.PlainReporter import com.saveourtool.diktat.plugin.gradle.extension.Reporters import generated.DIKTAT_VERSION import org.gradle.api.DefaultTask import org.gradle.api.GradleException import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.file.FileCollection import org.gradle.api.file.RegularFileProperty import org.gradle.api.model.ObjectFactory import org.gradle.api.tasks.IgnoreEmptyDirectories import org.gradle.api.tasks.InputFile import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.Internal import org.gradle.api.tasks.Optional import org.gradle.api.tasks.OutputFiles import org.gradle.api.tasks.PathSensitive import org.gradle.api.tasks.PathSensitivity import org.gradle.api.tasks.SkipWhenEmpty import org.gradle.api.tasks.TaskAction import org.gradle.api.tasks.TaskProvider import org.gradle.api.tasks.VerificationTask import org.gradle.api.tasks.util.PatternFilterable import org.gradle.language.base.plugins.LifecycleBasePlugin import java.nio.file.Files import java.nio.file.Path /** * A base task to run `diktat` * * @param inputs * @property extension */ @Suppress("WRONG_NEWLINES", "Deprecation") abstract class DiktatTaskBase( @get:Internal internal val extension: DiktatExtension, private val inputs: PatternFilterable, objectFactory: ObjectFactory, ) : DefaultTask(), VerificationTask { /** * Config file */ @get:Optional @get:InputFile abstract val configFile: RegularFileProperty /** * Baseline */ @get:Optional @get:InputFile abstract val baselineFile: RegularFileProperty /** * Files that will be analyzed by diktat */ @get:IgnoreEmptyDirectories @get:SkipWhenEmpty @get:PathSensitive(PathSensitivity.RELATIVE) @get:InputFiles val actualInputs: FileCollection by lazy { if (inputs.includes.isEmpty() && inputs.excludes.isEmpty()) { inputs.include("src/**/*.kt") } project.objects.fileCollection().from( project.fileTree("${project.projectDir}").apply { exclude("${project.buildDir}") } .matching(inputs) ) } /** * All reporters */ @get:Internal val reporters: Reporters = objectFactory.newInstance(Reporters::class.java) /** * Outputs for all reporters */ @get:OutputFiles @get:Optional val reporterOutputs: ConfigurableFileCollection = objectFactory.fileCollection() .also { fileCollection -> fileCollection.setFrom(reporters.all.mapNotNull { it.output.orNull }) fileCollection.finalizeValue() } /** * Whether diktat should be executed */ @get:Internal internal val shouldRun: Boolean by lazy { !actualInputs.isEmpty } private val diktatRunnerArguments by lazy { val sourceRootDir by lazy { project.rootProject.projectDir.toPath() } val defaultPlainReporter by lazy { project.objects.newInstance(PlainReporter::class.java) } val reporterCreationArgumentsList = (reporters.all.takeUnless { it.isEmpty() } ?: listOf(defaultPlainReporter)) .filterIsInstance() .map { reporter -> DiktatReporterCreationArguments( reporterType = reporter.type, outputStream = reporter.output.map { file -> file.asFile.also { Files.createDirectories(it.parentFile.toPath()) }.outputStream() }.orNull, sourceRootDir = sourceRootDir.takeIf { reporter.type == DiktatReporterType.SARIF }, ) } val loggingListener = object : DiktatProcessorListener { override fun beforeAll(files: Collection) { project.logger.info("Analyzing {} files with diktat in project {}", files.size, project.name) project.logger.debug("Analyzing {}", files) } override fun before(file: Path) { project.logger.debug("Checking file {}", file) } } DiktatRunnerArguments( configInputStream = configFile.map { it.asFile.inputStream() }.orNull, sourceRootDir = sourceRootDir, files = actualInputs.files.map { it.toPath() }, baselineFile = baselineFile.map { it.asFile.toPath() }.orNull, reporterArgsList = reporterCreationArgumentsList, loggingListener = loggingListener, ) } /** * [DiktatRunner] created based on a default [DiktatRunnerFactory] */ @get:Internal val diktatRunner by lazy { diktatRunnerFactory(diktatRunnerArguments) } init { group = LifecycleBasePlugin.VERIFICATION_GROUP } /** * Function to execute diKTat * * @throws GradleException */ @TaskAction fun run() { if (extension.debug) { project.logger.lifecycle("Running diktat $DIKTAT_VERSION with $ENGINE_INFO") } if (!shouldRun) { /* If ktlint receives empty patterns, it implicitly uses **/*.kt, **/*.kts instead. This can lead to diktat analyzing gradle buildscripts and so on. We want to prevent it. */ project.logger.warn("Inputs for $name do not exist, will not run diktat") project.logger.info("Skipping diktat execution") } else { doRun() } } private fun doRun() { val errorCounter = doRun( runner = diktatRunner, args = diktatRunnerArguments ) if (errorCounter > 0 && !ignoreFailures) { throw GradleException("There are $errorCounter lint errors") } } /** * An abstract method which should be overridden by fix and check tasks * * @param runner instance of [DiktatRunner] used in analysis * @param args arguments for [DiktatRunner] * @return count of errors */ abstract fun doRun( runner: DiktatRunner, args: DiktatRunnerArguments ): Int companion object { /** * @param extension */ fun TaskProvider.configure(extension: DiktatExtension) { configure { task -> extension.diktatConfigFile?.let { diktatConfigFile -> task.configFile.set(task.project.file(diktatConfigFile)) } extension.baseline?.let { baseline -> task.baselineFile.set(task.project.file(baseline)) } task.ignoreFailures = extension.ignoreFailures task.reporters.all.addAll(extension.reporters.all) if (extension.githubActions) { task.reporters.gitHubActions() } } } } } ================================================ FILE: diktat-gradle-plugin/src/main/kotlin/com/saveourtool/diktat/plugin/gradle/tasks/SarifReportMergeTask.kt ================================================ package com.saveourtool.diktat.plugin.gradle.tasks import com.saveourtool.diktat.plugin.gradle.DiktatGradlePlugin.Companion.DIKTAT_CHECK_TASK import com.saveourtool.diktat.plugin.gradle.DiktatGradlePlugin.Companion.MERGE_SARIF_REPORTS_TASK_NAME import com.saveourtool.diktat.plugin.gradle.extension.GitHubActionsReporter.Companion.getGitHubActionReporterMergeOutput import com.saveourtool.diktat.plugin.gradle.extension.GitHubActionsReporter.Companion.getGitHubActionReporterOutput import io.github.detekt.sarif4k.SarifSchema210 import org.gradle.api.DefaultTask import org.gradle.api.Project import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.file.RegularFileProperty import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.Optional import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.PathSensitive import org.gradle.api.tasks.PathSensitivity import org.gradle.api.tasks.TaskAction import org.gradle.api.tasks.TaskExecutionException import org.gradle.api.tasks.VerificationTask import kotlinx.serialization.SerializationException import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json /** * A task to merge SARIF reports produced by diktat check / diktat fix tasks. */ abstract class SarifReportMergeTask : DefaultTask(), VerificationTask { /** * Source reports that should be merged */ @get:InputFiles @get:PathSensitive(PathSensitivity.RELATIVE) @get:Optional abstract val input: ConfigurableFileCollection /** * Destination for the merged report */ @get:OutputFile @get:Optional abstract val output: RegularFileProperty /** * @throws TaskExecutionException if failed to deserialize SARIF */ @TaskAction fun mergeReports() { if (!output.isPresent) { logger.debug("Skipping merging SARIF reports because output is not set") return } val sarifReports = input.files .filter { it.exists() } .also { logger.info("Merging SARIF reports from files $it") } .map { try { Json.decodeFromString(it.readText()) } catch (e: SerializationException) { logger.error("Couldn't deserialize JSON: is ${it.canonicalPath} a SARIF file?") throw TaskExecutionException(this, e) } } if (sarifReports.isEmpty()) { logger.warn("Cannot perform merging of SARIF reports because no matching files were found; " + "is SARIF reporter active?" ) return } // All reports should contain identical metadata, so we are using the first one as a base. val templateReport = sarifReports.first() val allResults = sarifReports.flatMap { sarifSchema -> sarifSchema.runs .flatMap { it.results.orEmpty() } } val mergedSarif = templateReport.copy( runs = listOf(templateReport.runs.first().copy(results = allResults)) ) output.get().asFile.writeText(Json.encodeToString(mergedSarif)) } } /** * Configure [MERGE_SARIF_REPORTS_TASK_NAME] */ internal fun Project.configureMergeReportsTask() { val diktatCheckTask = tasks.named(DIKTAT_CHECK_TASK, DiktatCheckTask::class.java) val rootMergeSarifReportsTask = if (path == rootProject.path) { tasks.register(MERGE_SARIF_REPORTS_TASK_NAME, SarifReportMergeTask::class.java) { reportMergeTask -> reportMergeTask.output.set(getGitHubActionReporterMergeOutput()) } } else { rootProject.tasks.named(MERGE_SARIF_REPORTS_TASK_NAME, SarifReportMergeTask::class.java) } rootMergeSarifReportsTask.configure { reportMergeTask -> reportMergeTask.input.from(getGitHubActionReporterOutput()) reportMergeTask.mustRunAfter(diktatCheckTask) } diktatCheckTask.configure { it.finalizedBy(rootMergeSarifReportsTask) } } ================================================ FILE: diktat-gradle-plugin/src/test/kotlin/com/saveourtool/diktat/plugin/gradle/DiktatGradlePluginTest.kt ================================================ package com.saveourtool.diktat.plugin.gradle import com.saveourtool.diktat.plugin.gradle.tasks.DiktatCheckTask import org.gradle.api.Project import org.gradle.testfixtures.ProjectBuilder import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test class DiktatGradlePluginTest { private val projectBuilder = ProjectBuilder.builder() private lateinit var project: Project @BeforeEach fun setUp() { project = projectBuilder.build() // mock kotlin sources project.mkdir("src/main/kotlin") project.file("src/main/kotlin/Test.kt").createNewFile() project.pluginManager.apply(DiktatGradlePlugin::class.java) } @Test fun `check that tasks are registered`() { Assertions.assertTrue(project.tasks.findByName("diktatCheck") != null) Assertions.assertTrue(project.tasks.findByName("diktatFix") != null) } @Test fun `check default extension properties`() { val diktatExtension = project.extensions.getByName("diktat") as DiktatExtension val actualInputs = project.tasks .named("diktatCheck", DiktatCheckTask::class.java) .get() .actualInputs Assertions.assertFalse(diktatExtension.debug) Assertions.assertIterableEquals(project.fileTree("src").files, actualInputs.files) Assertions.assertTrue(actualInputs.files.isNotEmpty()) } } ================================================ FILE: diktat-gradle-plugin/src/test/kotlin/com/saveourtool/diktat/plugin/gradle/DiktatJavaExecTaskTest.kt ================================================ package com.saveourtool.diktat.plugin.gradle import com.saveourtool.diktat.api.DiktatReporter import com.saveourtool.diktat.ktlint.DiktatReporterImpl.Companion.unwrapToKtlint import com.saveourtool.diktat.plugin.gradle.tasks.DiktatCheckTask import com.saveourtool.diktat.util.DiktatProcessorListenerWrapper.Companion.unwrap import com.pinterest.ktlint.cli.reporter.json.JsonReporter import com.pinterest.ktlint.cli.reporter.plain.PlainReporter import com.pinterest.ktlint.cli.reporter.sarif.SarifReporter import org.gradle.api.Project import org.gradle.testfixtures.ProjectBuilder import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import java.io.File import java.nio.file.Files class DiktatJavaExecTaskTest { private val projectBuilder = ProjectBuilder.builder() private lateinit var project: Project @BeforeEach fun setUp() { project = projectBuilder .withName("testProject") .build() // mock kotlin sources project.mkdir("src/main/kotlin") project.file("src/main/kotlin/Test.kt").createNewFile() sequenceOf( project.file("diktat-analysis.yml"), project.file("../diktat-analysis.yml") ).forEach { it.writeText(""" # Common configuration - name: DIKTAT_COMMON enabled: true """.trimIndent()) } } @AfterEach fun tearDown() { project.layout.buildDirectory.get().asFile.deleteRecursively() } @Test fun `check command line for various inputs`() { assertFiles( listOf( combinePathParts("src", "main", "kotlin", "Test.kt") ) ) { inputs { include("src/**/*.kt") } } val task = project.tasks.getByName(DIKTAT_CHECK_TASK) as DiktatCheckTask assert(task.diktatRunner.diktatReporter.unwrapFirst() is PlainReporter) } @Test fun `check command line in debug mode`() { assertFiles( listOf(combinePathParts("src", "main", "kotlin", "Test.kt")) ) { inputs { include("src/**/*.kt") } debug = true } } @Test fun `check command line with excludes`() { project.file("src/main/kotlin/generated").mkdirs() project.file("src/main/kotlin/generated/Generated.kt").createNewFile() assertFiles( listOf(combinePathParts("src", "main", "kotlin", "Test.kt")) ) { inputs { include("src/**/*.kt") exclude("src/main/kotlin/generated") } } val task = project.tasks.getByName(DIKTAT_CHECK_TASK) as DiktatCheckTask assert(task.diktatRunner.diktatReporter.unwrapFirst() is PlainReporter) } @Test fun `check command line with non-existent inputs`() { val task = project.registerDiktatTask { inputs { exclude("*") } } Assertions.assertFalse(task.shouldRun) } @Test fun `check system property with default config`() { val task = project.registerDiktatTask { inputs { exclude("*") } } Assertions.assertEquals(File(project.projectDir, "diktat-analysis.yml"), task.extension.diktatConfigFile) } @Test fun `check system property with custom config`() { val task = project.registerDiktatTask { inputs { exclude("*") } diktatConfigFile = project.file("../diktat-analysis.yml") } Assertions.assertEquals(File(project.projectDir.parentFile, "diktat-analysis.yml"), task.extension.diktatConfigFile) } @Test fun `check command line has reporter type and output`() { assertFiles(emptyList()) { inputs { exclude("*") } diktatConfigFile = project.file("../diktat-analysis.yml") reporters { reportersDsl -> reportersDsl.json { jsonDsl -> jsonDsl.output.set(project.layout.buildDirectory.file("some.txt")) } } } val task = project.tasks.getByName(DIKTAT_CHECK_TASK) as DiktatCheckTask assert(task.diktatRunner.diktatReporter.unwrapFirst() is JsonReporter) } @Test fun `check command line has reporter type without output`() { assertFiles(emptyList()) { inputs { exclude("*") } diktatConfigFile = project.file("../diktat-analysis.yml") reporters { reportersDsl -> reportersDsl.json() } } val task = project.tasks.getByName(DIKTAT_CHECK_TASK) as DiktatCheckTask assert(task.diktatRunner.diktatReporter.unwrapFirst() is JsonReporter) } @Test fun `check command line in githubActions mode`() { assertFiles(emptyList()) { inputs { exclude("*") } diktatConfigFile = project.file("../diktat-analysis.yml") githubActions = true } val task = project.tasks.getByName(DIKTAT_CHECK_TASK) as DiktatCheckTask assert(task.diktatRunner.diktatReporter.unwrapFirst() is SarifReporter) Assertions.assertEquals( project.rootDir.toString(), System.getProperty("user.home") ) } @Test fun `githubActions mode should have higher precedence over explicit reporter`() { assertFiles(emptyList()) { inputs { exclude("*") } diktatConfigFile = project.file("../diktat-analysis.yml") githubActions = true reporters { reportersDsl -> reportersDsl.json { jsonDsl -> jsonDsl.output.set(project.layout.buildDirectory.file("report.json")) } } } val task = project.tasks.getByName(DIKTAT_CHECK_TASK) as DiktatCheckTask val (first, second) = task.diktatRunner.diktatReporter .unwrap>() .first() .unwrap() .unwrap>() .toList() assert(first.unwrapToKtlint() is JsonReporter) assert(second.unwrapToKtlint() is SarifReporter) Assertions.assertEquals( project.rootDir.toString(), System.getProperty("user.home") ) } @Test fun `should set system property with SARIF reporter`() { assertFiles(emptyList()) { inputs { exclude("*") } diktatConfigFile = project.file("../diktat-analysis.yml") reporters { reportersDsl -> reportersDsl.sarif() } } val task = project.tasks.getByName(DIKTAT_CHECK_TASK) as DiktatCheckTask assert(task.diktatRunner.diktatReporter.unwrapFirst() is SarifReporter) Assertions.assertEquals( project.rootProject.projectDir.toPath().toString(), System.getProperty("user.home") ) } @Test fun `check system property with multiproject build with default config`() { setupMultiProject() val subproject = project.subprojects.first() project.allprojects { it.registerDiktatTask { inputs { exclude("*") } } } val task = project.tasks.getByName(DIKTAT_CHECK_TASK) as DiktatCheckTask val subprojectTask = subproject.tasks.getByName(DIKTAT_CHECK_TASK) as DiktatCheckTask Assertions.assertEquals(File(project.projectDir, "diktat-analysis.yml"), task.extension.diktatConfigFile) Assertions.assertEquals(File(project.projectDir, "diktat-analysis.yml"), subprojectTask.extension.diktatConfigFile) } @Test fun `check system property with multiproject build with custom config`() { setupMultiProject() val subproject = project.subprojects.first() project.allprojects { it.registerDiktatTask { inputs { exclude("*") } diktatConfigFile = it.rootProject.file("diktat-analysis.yml") } } val task = project.tasks.getByName(DIKTAT_CHECK_TASK) as DiktatCheckTask val subprojectTask = subproject.tasks.getByName(DIKTAT_CHECK_TASK) as DiktatCheckTask Assertions.assertEquals(File(project.projectDir, "diktat-analysis.yml"), task.extension.diktatConfigFile) Assertions.assertEquals(File(project.projectDir, "diktat-analysis.yml"), subprojectTask.extension.diktatConfigFile) } @Test fun `check system property with multiproject build with custom config and two config files`() { setupMultiProject() val subproject = project.subprojects.single() subproject.file("diktat-analysis.yml").createNewFile() project.allprojects { it.registerDiktatTask { inputs { exclude("*") } diktatConfigFile = it.file("diktat-analysis.yml") } } val task = project.tasks.getByName(DIKTAT_CHECK_TASK) as DiktatCheckTask val subprojectTask = subproject.tasks.getByName(DIKTAT_CHECK_TASK) as DiktatCheckTask Assertions.assertEquals(File(project.projectDir, "diktat-analysis.yml"), task.extension.diktatConfigFile) Assertions.assertEquals(File(subproject.projectDir, "diktat-analysis.yml"), subprojectTask.extension.diktatConfigFile) } private fun Project.registerDiktatTask(extensionConfiguration: DiktatExtension.() -> Unit): DiktatCheckTask { pluginManager.apply(DiktatGradlePlugin::class.java) extensions.configure("diktat", extensionConfiguration) return tasks.getByName(DIKTAT_CHECK_TASK) as DiktatCheckTask } private fun assertFiles(expected: List, extensionConfiguration: DiktatExtension.() -> Unit) { val task = project.registerDiktatTask(extensionConfiguration) val files = task.actualInputs.files Assertions.assertEquals(expected.size, files.size) Assertions.assertTrue { expected.zip(files) .map { (expectedStr, file) -> file.path.endsWith(expectedStr) } .all { it } } } private fun setupMultiProject() { ProjectBuilder.builder() .withParent(project) .withName("testSubproject") .withProjectDir(File(project.projectDir, "testSubproject").also { Files.createDirectory(it.toPath()) }) .build() project.file("diktat-analysis.yml").createNewFile() } private fun combinePathParts(vararg parts: String) = parts.joinToString(File.separator) companion object { private const val DIKTAT_CHECK_TASK = "diktatCheck" private fun DiktatReporter.unwrapFirst() = this .unwrap>().first() .unwrap() .unwrap>().first() .unwrapToKtlint() } } ================================================ FILE: diktat-ktlint-engine/build.gradle.kts ================================================ import org.gradle.accessors.dm.LibrariesForLibs plugins { id("com.saveourtool.diktat.buildutils.kotlin-jvm-configuration") id("com.saveourtool.diktat.buildutils.code-quality-convention") id("com.saveourtool.diktat.buildutils.publishing-default-configuration") } project.description = "This module builds diktat-api implementation using ktlint" dependencies { api(projects.diktatApi) implementation(libs.ktlint.cli.reporter.core) implementation(libs.ktlint.rule.engine) implementation(libs.ktlint.rule.engine.core) implementation(libs.ktlint.cli.reporter.baseline) implementation(libs.ktlint.cli.reporter.checkstyle) implementation(libs.ktlint.cli.reporter.html) implementation(libs.ktlint.cli.reporter.json) implementation(libs.ktlint.cli.reporter.plain) implementation(libs.ktlint.cli.reporter.sarif) testImplementation(libs.log4j2.slf4j2) testImplementation(libs.junit.jupiter) testImplementation(libs.junit.platform.suite) testImplementation(libs.assertj.core) } val ktlintVersion: String = the() .versions .ktlint .get() val generateKtlintVersionFile by tasks.registering { val outputDir = layout.buildDirectory.dir("generated/src").get().asFile val versionsFile = outputDir.resolve("generated/KtLintVersion.kt") inputs.property("ktlint version", ktlintVersion) outputs.dir(outputDir) doFirst { versionsFile.parentFile.mkdirs() versionsFile.writeText( """ package generated const val KTLINT_VERSION = "$ktlintVersion" """.trimIndent() ) } } kotlin.sourceSets.getByName("main") { kotlin.srcDir( generateKtlintVersionFile.map { it.outputs.files.singleFile } ) } ================================================ FILE: diktat-ktlint-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/api/Code.kt ================================================ /** * Copied from KtLint and open the constructor */ @file:Suppress( "TOO_LONG_FUNCTION", "PACKAGE_NAME_INCORRECT_PREFIX", "PACKAGE_NAME_INCORRECT_PATH", "KDOC_NO_CONSTRUCTOR_PROPERTY", "MISSING_KDOC_CLASS_ELEMENTS", "MISSING_KDOC_ON_FUNCTION", "KDOC_WITHOUT_PARAM_TAG", "KDOC_WITHOUT_RETURN_TAG", ) package com.pinterest.ktlint.rule.engine.api import com.pinterest.ktlint.rule.engine.api.KtLintRuleEngine.Companion.STDIN_FILE import org.jetbrains.kotlin.konan.file.file import java.io.File import java.nio.file.Path import kotlin.io.path.pathString /** * A representation of a block of code. Use one of the factory methods [fromFile], [fromPath], [fromSnippet] or [fromStdin] to instantiate. */ public class Code( public val content: String, public val fileName: String?, public val filePath: Path?, public val script: Boolean, public val isStdIn: Boolean, ) { public fun fileNameOrStdin(): String = if (isStdIn) { STDIN_FILE } else { fileName.orEmpty() } public fun filePathOrStdin(): String = if (isStdIn) { STDIN_FILE } else { filePath?.pathString.orEmpty() } public companion object { /** * Create [Code] from a [file] containing valid Kotlin code or script. The '.editorconfig' files on the path to [file] are taken * into account. */ public fun fromFile(file: File): Code = Code( content = file.readText(), fileName = file.name, filePath = file.toPath(), script = file.name.endsWith(".kts", ignoreCase = true), isStdIn = false, ) /** * Create [Code] from a [path] to a file containing valid Kotlin code or script. The '.editorconfig' files on the path to [file] are * taken into account. This method is intended to be used in unit tests. In order to work with the Ktlint test file system it needs * to make additional call to get the file system which makes it slower compared to [fromFile]. Prefer to use [fromFile]. */ public fun fromPath(path: Path): Code { // Resolve the file based on the file system of the original path given. val file = path .fileSystem .file(path.pathString) return Code( content = file.readStrings().joinToString(separator = "\n"), fileName = file.name, filePath = path, script = file.name.endsWith(".kts", ignoreCase = true), isStdIn = false, ) } /** * The [content] represent a valid piece of Kotlin code or Kotlin script. The '.editorconfig' files on the filesystem are ignored as * the snippet is not associated with a file path. Use [Code.fromFile] for scanning a file while at the same time respecting the * '.editorconfig' files on the path to the file. */ public fun fromSnippet( content: String, script: Boolean = false, ): Code = Code( content = content, filePath = null, fileName = null, script = script, isStdIn = true, ) /** * Create [Code] by reading the snippet from 'stdin'. No '.editorconfig' are taken into account. The '.editorconfig' files on the * filesystem are ignored as the snippet is not associated with a file path. Use [Code.fromFile] for scanning a file while at the * same time respecting the '.editorconfig' files on the path to the file. */ public fun fromStdin(): Code = fromSnippet(String(System.`in`.readBytes())) } } ================================================ FILE: diktat-ktlint-engine/src/main/kotlin/com/saveourtool/diktat/ktlint/DiktatBaselineFactoryImpl.kt ================================================ package com.saveourtool.diktat.ktlint import com.saveourtool.diktat.api.DiktatBaseline import com.saveourtool.diktat.api.DiktatBaselineFactory import com.saveourtool.diktat.api.DiktatProcessorListener import com.saveourtool.diktat.ktlint.DiktatReporterImpl.Companion.wrap import com.pinterest.ktlint.cli.reporter.baseline.Baseline import com.pinterest.ktlint.cli.reporter.baseline.BaselineErrorHandling import com.pinterest.ktlint.cli.reporter.baseline.BaselineReporterProvider import com.pinterest.ktlint.cli.reporter.baseline.loadBaseline import java.nio.file.Path import kotlin.io.path.absolutePathString import kotlin.io.path.outputStream /** * A factory to create or generate [DiktatBaseline] using `KtLint` */ class DiktatBaselineFactoryImpl : DiktatBaselineFactory { private val baselineReporterProvider = BaselineReporterProvider() override fun tryToLoad( baselineFile: Path, sourceRootDir: Path?, ): DiktatBaseline? = loadBaseline(baselineFile.absolutePathString(), BaselineErrorHandling.LOG) .takeIf { it.status == Baseline.Status.VALID } ?.let { ktLintBaseline -> DiktatBaseline { file -> ktLintBaseline.lintErrorsPerFile[file.relativePathStringTo(sourceRootDir)] .orEmpty() .map { it.wrap() } .toSet() } } override fun generator( baselineFile: Path, sourceRootDir: Path?, ): DiktatProcessorListener = baselineReporterProvider.get( baselineFile.outputStream(), closeOutAfterAll = true, emptyMap(), ).wrap(sourceRootDir) } ================================================ FILE: diktat-ktlint-engine/src/main/kotlin/com/saveourtool/diktat/ktlint/DiktatProcessorFactoryImpl.kt ================================================ package com.saveourtool.diktat.ktlint import com.saveourtool.diktat.DiktatProcessor import com.saveourtool.diktat.DiktatProcessorFactory import com.saveourtool.diktat.api.DiktatCallback import com.saveourtool.diktat.api.DiktatRuleSet import com.saveourtool.diktat.ktlint.KtLintRuleWrapper.Companion.toKtLint import com.pinterest.ktlint.rule.engine.api.Code import com.pinterest.ktlint.rule.engine.api.EditorConfigDefaults import com.pinterest.ktlint.rule.engine.api.KtLintRuleEngine import com.pinterest.ktlint.rule.engine.api.LintError import org.ec4j.core.model.EditorConfig import org.ec4j.core.model.Glob import org.ec4j.core.model.Property import org.ec4j.core.model.PropertyType import java.nio.file.Path import kotlin.io.path.name private typealias LintCallback = (LintError) -> Unit /** * A factory to create [DiktatProcessor] using [DiktatProcessorFactory] and `KtLint` as engine */ class DiktatProcessorFactoryImpl : DiktatProcessorFactory { override fun invoke(diktatRuleSet: DiktatRuleSet): DiktatProcessor { val ktLintRuleEngine = diktatRuleSet.toKtLintEngine() return object : DiktatProcessor { override fun fix( file: Path, callback: DiktatCallback, ): String = ktLintRuleEngine.formatSilentlyThenLint(file.toKtLint(), callback.toKtLintForLint()) override fun fix( code: String, virtualPath: Path?, callback: DiktatCallback ): String = ktLintRuleEngine.formatSilentlyThenLint(code.toKtLint(virtualPath), callback.toKtLintForLint()) override fun check( file: Path, callback: DiktatCallback, ) = ktLintRuleEngine.lint(file.toKtLint(), callback.toKtLintForLint()) override fun check( code: String, virtualPath: Path?, callback: DiktatCallback ) = ktLintRuleEngine.lint(code.toKtLint(virtualPath), callback.toKtLintForLint()) } } companion object { private fun DiktatRuleSet.toKtLintEngine(): KtLintRuleEngine = KtLintRuleEngine( ruleProviders = toKtLint(), // use platform dependent endlines in process of editing editorConfigDefaults = EditorConfigDefaults( EditorConfig.builder() .openSection() .glob(Glob("**")) .property( Property.builder() .name(PropertyType.end_of_line.name) .type(PropertyType.end_of_line) .value(PropertyType.EndOfLineValue.ofEndOfLineString(System.lineSeparator())?.name) ) .closeSection() .build() ) ) private fun Path.toKtLint(): Code = Code.fromFile(this.toFile()) private fun String.toKtLint(virtualPath: Path?): Code = Code( content = this, fileName = virtualPath?.name, filePath = virtualPath, script = virtualPath?.name?.endsWith(".kts", ignoreCase = true) ?: false, isStdIn = virtualPath == null, ) private fun DiktatCallback.toKtLintForLint(): LintCallback = { error -> this(error.wrap(), false) } private fun KtLintRuleEngine.formatSilentlyThenLint( code: Code, callback: LintCallback, ): String { // this API method is significantly changed in Ktlint, so -werror was disabled due to it @Suppress("Deprecation") val formatResult = format(code) lint( code = Code( content = formatResult, fileName = code.fileName, filePath = code.filePath, script = code.script, isStdIn = code.isStdIn, ), callback = callback, ) return formatResult } } } ================================================ FILE: diktat-ktlint-engine/src/main/kotlin/com/saveourtool/diktat/ktlint/DiktatReporterFactoryImpl.kt ================================================ package com.saveourtool.diktat.ktlint import com.saveourtool.diktat.api.DiktatReporter import com.saveourtool.diktat.api.DiktatReporterCreationArguments import com.saveourtool.diktat.api.DiktatReporterFactory import com.saveourtool.diktat.api.DiktatReporterType import com.saveourtool.diktat.api.PlainDiktatReporterCreationArguments import com.saveourtool.diktat.ktlint.DiktatReporterImpl.Companion.wrap import com.pinterest.ktlint.cli.reporter.checkstyle.CheckStyleReporterProvider import com.pinterest.ktlint.cli.reporter.html.HtmlReporterProvider import com.pinterest.ktlint.cli.reporter.json.JsonReporterProvider import com.pinterest.ktlint.cli.reporter.plain.Color import com.pinterest.ktlint.cli.reporter.plain.PlainReporterProvider import com.pinterest.ktlint.cli.reporter.sarif.SarifReporterProvider import kotlin.io.path.pathString /** * A factory to create [DiktatReporter] using `KtLint` */ class DiktatReporterFactoryImpl : DiktatReporterFactory { private val plainReporterProvider = PlainReporterProvider() /** * All reporters which __KtLint__ provides */ private val reporterProviders = mapOf( DiktatReporterType.JSON to JsonReporterProvider(), DiktatReporterType.SARIF to SarifReporterProvider(), DiktatReporterType.CHECKSTYLE to CheckStyleReporterProvider(), DiktatReporterType.HTML to HtmlReporterProvider(), DiktatReporterType.PLAIN to plainReporterProvider, ) override val colorNamesInPlain: Set get() = Color.entries.map { it.name }.toSet() override fun invoke( args: DiktatReporterCreationArguments, ): DiktatReporter { val opts = when { args is PlainDiktatReporterCreationArguments -> buildMap { put("color", args.colorName?.let { true } ?: false) put("color_name", args.colorName ?: Color.DARK_GRAY) args.groupByFile?.let { put("group_by_file", it) } }.mapValues { it.value.toString() } args.reporterType == DiktatReporterType.PLAIN -> mapOf( "color_name" to Color.DARK_GRAY.name, "group_by_file" to false.toString(), ) args.reporterType == DiktatReporterType.PLAIN_GROUP_BY_FILE -> mapOf( "color_name" to Color.DARK_GRAY.name, "group_by_file" to true.toString(), ) else -> emptyMap() } val reporterProvider = reporterProviders[args.reporterType] ?: throw IllegalArgumentException("Not supported reporter id by ${DiktatBaselineFactoryImpl::class.simpleName}") if (reporterProvider is SarifReporterProvider) { args.sourceRootDir?.let { System.setProperty("user.home", it.pathString) } } return reporterProvider.get(args.outputStream, args.closeOutputStreamAfterAll, opts).wrap(args.sourceRootDir) } } ================================================ FILE: diktat-ktlint-engine/src/main/kotlin/com/saveourtool/diktat/ktlint/DiktatReporterImpl.kt ================================================ package com.saveourtool.diktat.ktlint import com.saveourtool.diktat.api.DiktatError import com.saveourtool.diktat.api.DiktatReporter import com.saveourtool.diktat.ktlint.ReporterV2Wrapper.Companion.unwrapIfNeeded import com.saveourtool.diktat.util.DiktatProcessorListenerWrapper import com.pinterest.ktlint.cli.reporter.core.api.ReporterV2 import java.nio.file.Path /** * [DiktatReporter] using __KtLint__ * * @param ktLintReporter * @param sourceRootDir */ class DiktatReporterImpl( ktLintReporter: ReporterV2, private val sourceRootDir: Path?, ) : DiktatProcessorListenerWrapper(ktLintReporter) { override fun doBeforeAll(wrappedValue: ReporterV2, files: Collection): Unit = wrappedValue.beforeAll() override fun doBefore(wrappedValue: ReporterV2, file: Path): Unit = wrappedValue.before(file.relativePathStringTo(sourceRootDir)) override fun doOnError( wrappedValue: ReporterV2, file: Path, error: DiktatError, isCorrected: Boolean, ): Unit = wrappedValue.onLintError(file.relativePathStringTo(sourceRootDir), error.toKtLintForCli()) override fun doAfter(wrappedValue: ReporterV2, file: Path): Unit = wrappedValue.after(file.relativePathStringTo(sourceRootDir)) override fun doAfterAll(wrappedValue: ReporterV2): Unit = wrappedValue.afterAll() companion object { /** * @param sourceRootDir * @return [DiktatReporter] which wraps __KtLint__'s [ReporterV2] */ fun ReporterV2.wrap(sourceRootDir: Path?): DiktatReporter = DiktatReporterImpl(this, sourceRootDir) /** * @return __KtLint__'s [ReporterV2] */ fun DiktatReporter.unwrapToKtlint(): ReporterV2 = unwrap().unwrapIfNeeded() } } ================================================ FILE: diktat-ktlint-engine/src/main/kotlin/com/saveourtool/diktat/ktlint/KtLintRuleWrapper.kt ================================================ package com.saveourtool.diktat.ktlint import com.saveourtool.diktat.api.DiktatRule import com.saveourtool.diktat.api.DiktatRuleSet import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.pinterest.ktlint.rule.engine.core.api.Rule import com.pinterest.ktlint.rule.engine.core.api.Rule.VisitorModifier.RunAfterRule.Mode import com.pinterest.ktlint.rule.engine.core.api.RuleId import com.pinterest.ktlint.rule.engine.core.api.RuleProvider import org.jetbrains.kotlin.com.intellij.lang.ASTNode private typealias EmitType = (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit /** * This is a wrapper around __KtLint__'s [Rule] which adjusts visitorModifiers to keep order with prevRule. * @property rule */ class KtLintRuleWrapper( val rule: DiktatRule, prevRuleId: RuleId? = null, ) : Rule( ruleId = rule.id.toRuleId(DIKTAT_RULE_SET_ID), about = about, visitorModifiers = createVisitorModifiers(rule, prevRuleId), ) { @Deprecated("Marked for removal in Ktlint 2.0. Please implement interface RuleAutocorrectApproveHandler.") override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, emit: EmitType, ) = rule.invoke(node, autoCorrect) { offset, errorMessage, canBeAutoCorrected -> emit.invoke(offset, errorMessage.correctErrorDetail(canBeAutoCorrected), canBeAutoCorrected) } companion object { private val about: About = About( maintainer = "Diktat", repositoryUrl = "https://github.com/saveourtool/diktat", issueTrackerUrl = "https://github.com/saveourtool/diktat/issues", ) private fun Sequence.wrapRulesToProviders(): Sequence = runningFold(null as RuleProvider?) { prevRuleProvider, diktatRule -> val prevRuleId = prevRuleProvider?.ruleId?.value?.toRuleId(DIKTAT_RULE_SET_ID) RuleProvider( provider = { KtLintRuleWrapper(diktatRule, prevRuleId) }, ) }.filterNotNull() /** * @return [Set] of __KtLint__'s [RuleProvider]s created from [DiktatRuleSet] */ fun DiktatRuleSet.toKtLint(): Set = rules .asSequence() .wrapRulesToProviders() .toSet() private fun createVisitorModifiers( rule: DiktatRule, prevRuleId: RuleId?, ): Set = prevRuleId?.run { val ruleId = rule.id.toRuleId(DIKTAT_RULE_SET_ID) require(ruleId != prevRuleId) { "PrevRule has same ID as rule: $ruleId" } setOf( VisitorModifier.RunAfterRule( ruleId = prevRuleId, mode = Mode.REGARDLESS_WHETHER_RUN_AFTER_RULE_IS_LOADED_OR_DISABLED ) ) } ?: emptySet() /** * @return a rule to which a logic is delegated */ internal fun Rule.unwrap(): DiktatRule = (this as? KtLintRuleWrapper)?.rule ?: error("Provided rule ${javaClass.simpleName} is not wrapped by diktat") } } ================================================ FILE: diktat-ktlint-engine/src/main/kotlin/com/saveourtool/diktat/ktlint/KtLintUtils.kt ================================================ /** * This file contains util methods for __KtLint__ */ package com.saveourtool.diktat.ktlint import com.saveourtool.diktat.api.DiktatCallback import com.saveourtool.diktat.api.DiktatError import com.saveourtool.diktat.api.DiktatRuleSet import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.pinterest.ktlint.cli.reporter.core.api.KtlintCliError import com.pinterest.ktlint.cli.reporter.core.api.ReporterProviderV2 import com.pinterest.ktlint.cli.reporter.core.api.ReporterV2 import com.pinterest.ktlint.rule.engine.api.LintError import com.pinterest.ktlint.rule.engine.core.api.RuleId import org.intellij.lang.annotations.Language import org.jetbrains.kotlin.utils.addToStdlib.applyIf import java.io.OutputStream import java.io.PrintStream import java.nio.file.Path import kotlin.io.path.invariantSeparatorsPathString import kotlin.io.path.relativeToOrSelf private const val CANNOT_BE_AUTOCORRECTED_SUFFIX = " (cannot be auto-corrected)" /** * Makes sure this _rule id_ is qualified with a _rule set id_. * * @param ruleSetId the _rule set id_; defaults to [DIKTAT_RULE_SET_ID]. * @return the fully-qualified _rule id_ in the form of `ruleSetId:ruleId` as __KtLint__'s [RuleId]. * @see DIKTAT_RULE_SET_ID * @since 1.2.4 */ fun String.toRuleId(ruleSetId: String = DIKTAT_RULE_SET_ID): RuleId = when { this.contains(':') -> RuleId(this) else -> RuleId("$ruleSetId:$this") } /** * @return [DiktatError] from KtLint's [LintError] */ fun LintError.wrap(): DiktatError = DiktatError( line = this@wrap.line, col = this@wrap.col, ruleId = this@wrap.ruleId.value, detail = this@wrap.detail.removeSuffix(CANNOT_BE_AUTOCORRECTED_SUFFIX), canBeAutoCorrected = this@wrap.canBeAutoCorrected, ) /** * @return [DiktatError] from KtLint's [KtlintCliError] */ fun KtlintCliError.wrap(): DiktatError = DiktatError( line = this@wrap.line, col = this@wrap.col, ruleId = this@wrap.ruleId, detail = this@wrap.detail.removeSuffix(CANNOT_BE_AUTOCORRECTED_SUFFIX), canBeAutoCorrected = this@wrap.status == KtlintCliError.Status.LINT_CAN_BE_AUTOCORRECTED, ) /** * @return KtLint [LintError] from [DiktatError] or exception */ fun DiktatError.toKtLintForCli(): KtlintCliError = KtlintCliError( line = this@toKtLintForCli.line, col = this@toKtLintForCli.col, ruleId = this@toKtLintForCli.ruleId, detail = this@toKtLintForCli.detail.correctErrorDetail(this@toKtLintForCli.canBeAutoCorrected), status = if (this@toKtLintForCli.canBeAutoCorrected) { KtlintCliError.Status.LINT_CAN_BE_AUTOCORRECTED } else { KtlintCliError.Status.LINT_CAN_NOT_BE_AUTOCORRECTED } ) /** * @receiver [DiktatError.detail] * @param canBeAutoCorrected [DiktatError.canBeAutoCorrected] * @return input string with [CANNOT_BE_AUTOCORRECTED_SUFFIX] if it's required */ fun String.correctErrorDetail(canBeAutoCorrected: Boolean): String = if (canBeAutoCorrected) { this } else { "$this$CANNOT_BE_AUTOCORRECTED_SUFFIX" } /** * @param sourceRootDir * @return relative path to [sourceRootDir] as [String] */ fun Path.relativePathStringTo(sourceRootDir: Path?): String = (sourceRootDir?.let { relativeToOrSelf(it) } ?: this).invariantSeparatorsPathString /** * @param out [OutputStream] for [ReporterV2] * @param closeOutAfterAll close [OutputStream] in [ReporterV2.afterAll] * @param opt configuration for [ReporterV2] * @return created [ReporterV2] which closes [out] in [ReporterV2.afterAll] if it's required */ fun ReporterProviderV2.get( out: OutputStream, closeOutAfterAll: Boolean, opt: Map, ): ReporterV2 = get(out.printStream(), opt).applyIf(closeOutAfterAll) { closeAfterAll(out) } /** * Enables ignoring autocorrected errors when in "fix" mode (i.e. when * [com.pinterest.ktlint.core.KtLint.format] is invoked). * * Before version 0.47, _Ktlint_ only reported non-corrected errors in "fix" * mode. * Now, this has changed. * * @receiver the instance of _Ktlint_ parameters. * @return the instance [DiktatCallback] that ignores corrected errors. * @see com.pinterest.ktlint.core.KtLint.format * @since 1.2.4 */ private fun DiktatCallback.ignoreCorrectedErrors(): DiktatCallback = DiktatCallback { error, isCorrected -> if (!isCorrected) { this@ignoreCorrectedErrors(error, false) } } private fun OutputStream.printStream(): PrintStream = (this as? PrintStream) ?: PrintStream(this) private fun ReporterV2.closeAfterAll(outputStream: OutputStream): ReporterV2 = object : ReporterV2Wrapper(this@closeAfterAll) { override fun afterAll() { super.afterAll() outputStream.flush() outputStream.close() } } /** * @param ruleSetSupplier * @param file * @param cb callback to be called on unhandled [LintError]s * @return formatted code */ @Suppress("LAMBDA_IS_NOT_LAST_PARAMETER") fun format( ruleSetSupplier: () -> DiktatRuleSet, file: Path, cb: DiktatCallback, ): String = DiktatProcessorFactoryImpl().invoke(ruleSetSupplier()) .fix( file = file, callback = cb.ignoreCorrectedErrors(), ) /** * @param ruleSetSupplier * @param file * @param cb callback to be called on unhandled [LintError]s * @return [Unit] */ @Suppress("LAMBDA_IS_NOT_LAST_PARAMETER") fun check( ruleSetSupplier: () -> DiktatRuleSet, file: Path, cb: DiktatCallback = DiktatCallback.empty ) = DiktatProcessorFactoryImpl().invoke(ruleSetSupplier()) .check( file = file, callback = cb.ignoreCorrectedErrors(), ) /** * @param ruleSetSupplier * @param text * @param cb callback to be called on unhandled [LintError]s * @return [Unit] */ @Suppress("LAMBDA_IS_NOT_LAST_PARAMETER") fun check( ruleSetSupplier: () -> DiktatRuleSet, @Language("kotlin") text: String, cb: DiktatCallback = DiktatCallback.empty ) = DiktatProcessorFactoryImpl().invoke(ruleSetSupplier()) .check( code = text, virtualPath = null, callback = cb.ignoreCorrectedErrors(), ) ================================================ FILE: diktat-ktlint-engine/src/main/kotlin/com/saveourtool/diktat/ktlint/ReporterV2Wrapper.kt ================================================ package com.saveourtool.diktat.ktlint import com.pinterest.ktlint.cli.reporter.core.api.KtlintCliError import com.pinterest.ktlint.cli.reporter.core.api.ReporterV2 /** * Wrapper for [ReporterV2] * * @param reporterV2 */ open class ReporterV2Wrapper(private val reporterV2: ReporterV2) : ReporterV2 { override fun beforeAll() = reporterV2.beforeAll() override fun before(file: String) = reporterV2.before(file) override fun onLintError(file: String, ktlintCliError: KtlintCliError) = reporterV2.onLintError(file, ktlintCliError) override fun after(file: String) = reporterV2.after(file) override fun afterAll() = reporterV2.afterAll() companion object { /** * @return unwrapped [ReporterV2Wrapper] if it's required */ fun ReporterV2.unwrapIfNeeded(): ReporterV2 = if (this is ReporterV2Wrapper) { this.reporterV2 } else { this } } } ================================================ FILE: diktat-ktlint-engine/src/test/kotlin/com/saveourtool/diktat/ktlint/KtLintRuleWrapperTest.kt ================================================ package com.saveourtool.diktat.ktlint import com.saveourtool.diktat.api.DiktatErrorEmitter import com.saveourtool.diktat.api.DiktatRule import com.saveourtool.diktat.api.DiktatRuleSet import com.saveourtool.diktat.ktlint.KtLintRuleWrapper.Companion.toKtLint import com.saveourtool.diktat.ktlint.KtLintRuleWrapper.Companion.unwrap import com.pinterest.ktlint.rule.engine.api.Code import com.pinterest.ktlint.rule.engine.api.KtLintRuleEngine import com.pinterest.ktlint.rule.engine.core.api.Rule import com.pinterest.ktlint.rule.engine.core.api.RuleProvider import org.assertj.core.api.Assertions.assertThat import org.intellij.lang.annotations.Language import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test class KtLintRuleWrapperTest { @Test fun `check KtLintRuleSetWrapper with duplicate`() { val rule = mockRule("rule") Assertions.assertThrows(IllegalArgumentException::class.java) { DiktatRuleSet(listOf(rule, rule)).toKtLint() } } @Test fun `check OrderedRule`() { val rule1 = mockRule(id = "rule-first") val rule2 = mockRule(id = "rule-second") val orderedRuleProviders = DiktatRuleSet(listOf(rule1, rule2)).toKtLint() val orderedRuleProviderIterator = orderedRuleProviders.iterator() val orderedRule1 = orderedRuleProviderIterator.next().createNewRuleInstance() val orderedRule2 = orderedRuleProviderIterator.next().createNewRuleInstance() Assertions.assertFalse(orderedRuleProviderIterator.hasNext(), "Extra elements after ordering") Assertions.assertEquals(rule1, orderedRule1.unwrap(), "First rule is modified") orderedRule2.visitorModifiers .filterIsInstance() .also { Assertions.assertEquals(1, it.size, "Found invalid count of Rule.VisitorModifier.RunAfterRule") } .first() .let { Assertions.assertEquals(rule1.id.toRuleId(), it.ruleId, "Invalid ruleId in Rule.VisitorModifier.RunAfterRule") } } @Test @Suppress("TOO_LONG_FUNCTION") fun `KtLint keeps order with RuleVisitorModifierRunAfterRule`() { val actualRuleInvocationOrder: MutableList = mutableListOf() val onVisit: (DiktatRule) -> Unit = { rule -> actualRuleInvocationOrder += rule.id } val rules: List = sequenceOf("ccc", "bbb", "aaa").map { ruleId -> mockRule( id = ruleId, onVisit = onVisit ) }.toList() assertThat(rules).isNotEmpty /* * Make sure the rules are not sorted by id. */ val rulesOrderedById: List = rules.sortedBy(DiktatRule::id) assertThat(rules).containsExactlyInAnyOrder(*rulesOrderedById.toTypedArray()) assertThat(rules).isNotEqualTo(rulesOrderedById) /* * Make sure OrderedRuleSet preserves the order. */ val ruleProviders = DiktatRuleSet(rules).toKtLint() assertThat(ruleProviders.map(RuleProvider::createNewRuleInstance).map(Rule::ruleId)) .containsExactlyElementsOf(rules.map(DiktatRule::id).map { it.toRuleId() }) @Language("kotlin") val code = "fun foo() { }" KtLintRuleEngine( ruleProviders = ruleProviders ).lint( code = Code.fromSnippet( content = code ) ) val ruleCount = rules.size assertThat(actualRuleInvocationOrder) .describedAs("The ordered list of rule invocations") .matches({ order -> order.size % ruleCount == 0 }, "has a size which is multiple of $ruleCount") /* * This is the count of AST nodes in `code` above. */ val astNodeCount = actualRuleInvocationOrder.size / ruleCount /*- * This is new in ktlint 0.47. * Previously, rules were applied in this sequence: * * A -> B -> C (File) * | * V * A -> B -> C (Node) * | * V * A -> B -> C (Leaf) * * Now, each rule is recursively applied to all AST nodes, and then the * control is passed to the next rule: * * A(File) -> A(Node) -> A(Leaf) * | * V * B(File) -> B(Node) -> B(Leaf) * | * V * C(File) -> C(Node) -> C(Leaf) */ val expectedRuleInvocationOrder = rules.asSequence() .map(DiktatRule::id) .flatMap { ruleId -> generateSequence { ruleId }.take(astNodeCount) } .toList() assertThat(actualRuleInvocationOrder) .containsExactlyElementsOf(expectedRuleInvocationOrder) } companion object { private fun mockRule( id: String, onVisit: (DiktatRule) -> Unit = { } ): DiktatRule = object : DiktatRule { override val id: String get() = id override fun invoke(node: ASTNode, autoCorrect: Boolean, emitter: DiktatErrorEmitter) { onVisit(this) } } } } ================================================ FILE: diktat-maven-plugin/README.md ================================================ ## Local testing You can build and publish to maven local by using `:diktat-api:publishToMavenLocal :diktat-rules:publishToMavenLocal :diktat-runner:publishToMavenLocal :diktat-common:publishToMavenLocal :diktat-ktlint-engine:publishToMavenLocal :diktat-maven-plugin:publishToMavenLocal`. Then you can use a built version in projects in examples. A calculated version will be printed in logs by `reckon` plugin. ================================================ FILE: diktat-maven-plugin/build.gradle.kts ================================================ import com.saveourtool.diktat.buildutils.configurePublications import de.benediktritter.maven.plugin.development.task.GenerateHelpMojoSourcesTask import de.benediktritter.maven.plugin.development.task.GenerateMavenPluginDescriptorTask plugins { id("com.saveourtool.diktat.buildutils.kotlin-jvm-configuration") id("com.saveourtool.diktat.buildutils.code-quality-convention") id("com.saveourtool.diktat.buildutils.publishing-configuration") id("de.benediktritter.maven-plugin-development") version "0.4.3" `maven-publish` } dependencies { compileOnly(libs.maven.plugin.annotations) compileOnly(libs.maven.core) implementation(libs.kotlin.stdlib.jdk8) implementation(projects.diktatRunner) testImplementation(libs.junit.jupiter.api) testImplementation(libs.junit.vintage.engine) testImplementation(libs.junit.jupiter.extension.itf) testImplementation(libs.maven.plugin.testing.harness) // to use org.apache.maven.repository.RepositorySystem in newer maven versions and maybe other classes testImplementation(libs.maven.compat) testImplementation(libs.assertj.core) testImplementation(libs.plexus.cipher) } tasks.withType { notCompatibleWithConfigurationCache("https://github.com/britter/maven-plugin-development/issues/8") } tasks.withType { notCompatibleWithConfigurationCache("https://github.com/britter/maven-plugin-development/issues/8") } mavenPlugin { goalPrefix.set("diktat") } publishing { publications { create("mavenPlugin") { from(components["java"]) } } } configurePublications() ================================================ FILE: diktat-maven-plugin/src/main/kotlin/com/saveourtool/diktat/plugin/maven/DiktatBaseMojo.kt ================================================ package com.saveourtool.diktat.plugin.maven import com.saveourtool.diktat.DiktatRunner import com.saveourtool.diktat.DiktatRunnerArguments import com.saveourtool.diktat.diktatRunnerFactory import com.saveourtool.diktat.plugin.maven.reporters.GitHubActionsReporter import com.saveourtool.diktat.plugin.maven.reporters.PlainReporter import com.saveourtool.diktat.plugin.maven.reporters.Reporter import com.saveourtool.diktat.plugin.maven.reporters.Reporters import com.saveourtool.diktat.util.isKotlinCodeOrScript import org.apache.maven.plugin.AbstractMojo import org.apache.maven.plugin.Mojo import org.apache.maven.plugin.MojoExecutionException import org.apache.maven.plugin.MojoFailureException import org.apache.maven.plugins.annotations.Parameter import org.apache.maven.project.MavenProject import java.io.File import java.nio.file.Path import java.nio.file.Paths import kotlin.io.path.inputStream import kotlin.io.path.isRegularFile /** * Base [Mojo] for checking and fixing code using diktat */ abstract class DiktatBaseMojo : AbstractMojo() { /** * Property that will be used if you need to publish the report to GitHub */ @Parameter(property = "diktat.githubActions") var githubActions = false /** * The reporters to use */ @Parameter var reporters: Reporters? = null /** * Baseline file, containing a list of errors that will be ignored. * If this file doesn't exist, it will be created on the first invocation. * Default: no baseline. */ @Parameter(property = "diktat.baseline") var baseline: File? = null /** * Path to diktat yml config file. Can be either absolute or relative to project's root directory. */ @Parameter(property = "diktat.config", defaultValue = "diktat-analysis.yml") lateinit var diktatConfigFile: String /** * Property that can be used to access various maven settings */ @Parameter(defaultValue = "\${project}", readonly = true) private lateinit var mavenProject: MavenProject /** * Paths that will be scanned for .kt(s) files */ @Parameter(property = "diktat.inputs", defaultValue = "\${project.basedir}/src") lateinit var inputs: List /** * Paths that will be excluded if encountered during diktat run */ @Parameter(property = "diktat.excludes", defaultValue = "") lateinit var excludes: List /** * @param runner instance of [DiktatRunner] used in analysis * @param args arguments for [DiktatRunner] * @return count of errors */ abstract fun runAction( runner: DiktatRunner, args: DiktatRunnerArguments, ): Int /** * Perform code check using diktat ruleset * * @throws MojoFailureException if code style check was not passed * @throws MojoExecutionException if an exception in __KtLint__ has been thrown */ override fun execute() { val configFile = resolveConfig() log.info("Running diKTat plugin with ${configFile?.let { "configuration file $it" } ?: "default configuration" } and inputs $inputs" + if (excludes.isNotEmpty()) " and excluding $excludes" else "" ) val sourceRootDir = getSourceRootDirTransitive() val reporters: List = (reporters?.getAll() ?: listOf(PlainReporter())) .let { all -> if (githubActions && all.filterIsInstance().isEmpty()) { all + GitHubActionsReporter() } else { all } } val reporterArgsList = reporters.map { it.toCreationArguments(mavenProject, sourceRootDir) } val args = DiktatRunnerArguments( configInputStream = configFile?.inputStream(), sourceRootDir = sourceRootDir, files = files(), baselineFile = baseline?.toPath(), reporterArgsList = reporterArgsList, ) val diktatRunner = diktatRunnerFactory(args) val errorCounter = runAction( runner = diktatRunner, args = args, ) if (errorCounter > 0) { throw MojoFailureException("There are $errorCounter lint errors") } } /** * Function that searches diktat config file in maven project hierarchy. * If [diktatConfigFile] is absolute, it's path is used. If [diktatConfigFile] is relative, this method looks for it in all maven parent projects. * This way config file can be placed in parent module directory and used in all child modules too. * * @return a configuration file. File by this path exists. */ private fun resolveConfig(): Path? { val file = Paths.get(diktatConfigFile) if (file.isAbsolute) { return file } return generateSequence(mavenProject) { it.parent } .map { it.basedir.toPath().resolve(diktatConfigFile) } .firstOrNull { it.isRegularFile() } } private fun getSourceRootDirTransitive(): Path = generateSequence(mavenProject) { project -> val parent = project.parent parent?.basedir?.let { parent } }.last().basedir.toPath() private fun files(): List { val (excludedDirs, excludedFiles) = excludes.map(::File).partition { it.isDirectory } return inputs .asSequence() .map(::File) .flatMap { it.files(excludedDirs, excludedFiles) } .map { it.toPath() } .toList() } @Suppress("TYPE_ALIAS") private fun File.files( excludedDirs: List, excludedFiles: List, ): Sequence = walk() .filter { file -> file.isFile && file.toPath().isKotlinCodeOrScript() } .filterNot { file -> file in excludedFiles || excludedDirs.any { file.startsWith(it) } } } ================================================ FILE: diktat-maven-plugin/src/main/kotlin/com/saveourtool/diktat/plugin/maven/DiktatMojo.kt ================================================ /** * MOJOs for goals of diktat plugin */ package com.saveourtool.diktat.plugin.maven import com.saveourtool.diktat.DiktatRunner import com.saveourtool.diktat.DiktatRunnerArguments import org.apache.maven.plugins.annotations.Mojo /** * Main [Mojo] that call diktat's rules on [inputs] files */ @Mojo(name = "check") @Suppress("unused") class DiktatCheckMojo : DiktatBaseMojo() { override fun runAction( runner: DiktatRunner, args: DiktatRunnerArguments, ): Int = runner.checkAll(args) } /** * Main [Mojo] that call diktat's rules on [inputs] files * and fixes discovered errors */ @Mojo(name = "fix") @Suppress("unused") class DiktatFixMojo : DiktatBaseMojo() { override fun runAction( runner: DiktatRunner, args: DiktatRunnerArguments, ): Int = runner.fixAll(args) { updatedFile -> log.info("Original and formatted content differ, writing to ${updatedFile.fileName}...") } } ================================================ FILE: diktat-maven-plugin/src/main/kotlin/com/saveourtool/diktat/plugin/maven/Utils.kt ================================================ /** * Utilities for diktat maven plugin */ package com.saveourtool.diktat.plugin.maven import com.saveourtool.diktat.api.DiktatReporterType import org.apache.maven.project.MavenProject import java.io.File /** * @param reporterType * @return default location of report with provided [reporterType] */ internal fun MavenProject.defaultReportLocation( reporterType: DiktatReporterType, ): File = basedir .resolve(build.directory) .resolve("reports") .resolve("diktat") .resolve("diktat.${reporterType.extension}") ================================================ FILE: diktat-maven-plugin/src/main/kotlin/com/saveourtool/diktat/plugin/maven/reporters/DefaultReporter.kt ================================================ /** * All default reporters */ package com.saveourtool.diktat.plugin.maven.reporters import com.saveourtool.diktat.api.DiktatReporterCreationArguments import com.saveourtool.diktat.api.DiktatReporterType import com.saveourtool.diktat.plugin.maven.defaultReportLocation import org.apache.maven.plugins.annotations.Parameter import org.apache.maven.project.MavenProject import java.io.File import java.nio.file.Path /** * A base interface for a default reporter * * @param type type of reporter */ open class DefaultReporter( private val type: DiktatReporterType, ) : Reporter { /** * Location for output */ @Parameter var output: File? = null override fun getOutput(project: MavenProject): File? = output ?: project.defaultReportLocation(type) override fun toCreationArguments( project: MavenProject, sourceRootDir: Path, ): DiktatReporterCreationArguments = DiktatReporterCreationArguments( reporterType = type, outputStream = getOutputStream(project), sourceRootDir = sourceRootDir.takeIf { type == DiktatReporterType.SARIF }, ) } /** * Plain reporter */ class PlainReporter : DefaultReporter( type = DiktatReporterType.PLAIN, ) { /** * Plain reporter prints to stdout by default */ override fun getOutput(project: MavenProject): File? = output } /** * JSON reporter */ class JsonReporter : DefaultReporter( type = DiktatReporterType.JSON, ) /** * SARIF reporter */ class SarifReporter : DefaultReporter( type = DiktatReporterType.SARIF, ) /** * Checkstyle reporter */ class CheckstyleReporter : DefaultReporter( type = DiktatReporterType.CHECKSTYLE, ) /** * HTML reporter */ class HtmlReporter : DefaultReporter( type = DiktatReporterType.HTML, ) ================================================ FILE: diktat-maven-plugin/src/main/kotlin/com/saveourtool/diktat/plugin/maven/reporters/GitHubActionsReporter.kt ================================================ package com.saveourtool.diktat.plugin.maven.reporters import com.saveourtool.diktat.api.DiktatReporterCreationArguments import com.saveourtool.diktat.api.DiktatReporterType import com.saveourtool.diktat.plugin.maven.defaultReportLocation import org.apache.maven.project.MavenProject import java.io.File import java.nio.file.Path /** * GitHub actions reporter */ class GitHubActionsReporter : Reporter { override fun getOutput(project: MavenProject): File = project.defaultReportLocation(DiktatReporterType.SARIF) override fun toCreationArguments(project: MavenProject, sourceRootDir: Path): DiktatReporterCreationArguments = DiktatReporterCreationArguments( reporterType = DiktatReporterType.SARIF, outputStream = getOutputStream(project), sourceRootDir = sourceRootDir, ) } ================================================ FILE: diktat-maven-plugin/src/main/kotlin/com/saveourtool/diktat/plugin/maven/reporters/Reporter.kt ================================================ package com.saveourtool.diktat.plugin.maven.reporters import com.saveourtool.diktat.api.DiktatReporterCreationArguments import org.apache.maven.project.MavenProject import java.io.File import java.io.OutputStream import java.nio.file.Files import java.nio.file.Path /** * A base interface for reporter */ interface Reporter { /** * @param project * @return location as a [File] for output or default value resolved by [project] */ fun getOutput(project: MavenProject): File? /** * @param project * @return location as an [OutputStream] for output or default value resolved by [project] */ fun getOutputStream(project: MavenProject): OutputStream? = getOutput(project)?.also { Files.createDirectories(it.parentFile.toPath()) }?.outputStream() /** * @param project * @param sourceRootDir * @return [DiktatReporterCreationArguments] to create this reporter */ fun toCreationArguments(project: MavenProject, sourceRootDir: Path): DiktatReporterCreationArguments } ================================================ FILE: diktat-maven-plugin/src/main/kotlin/com/saveourtool/diktat/plugin/maven/reporters/Reporters.kt ================================================ package com.saveourtool.diktat.plugin.maven.reporters import org.apache.maven.plugins.annotations.Parameter /** * Configuration for reporters */ class Reporters { /** * Configure *plain* reporter */ @Parameter var plain: PlainReporter? = null /** * Configure *json* reporter */ @Parameter var json: JsonReporter? = null /** * Configure *sarif* reporter */ @Parameter var sarif: SarifReporter? = null /** * Configure *sarif* reporter for GitHub actions */ @Parameter var gitHubActions: GitHubActionsReporter? = null /** * Configure *checkstyle* reporter */ @Parameter var checkstyle: CheckstyleReporter? = null /** * Configure *html* reporter */ @Parameter var html: HtmlReporter? = null /** * @return all configured reporters */ fun getAll(): List = listOfNotNull( plain, json, sarif, gitHubActions, checkstyle, html, ) } ================================================ FILE: diktat-maven-plugin/src/test/kotlin/com/saveourtool/diktat/plugin/maven/DiktatBaseMojoTest.kt ================================================ package com.saveourtool.diktat.plugin.maven import org.apache.maven.execution.DefaultMavenExecutionRequest import org.apache.maven.plugin.MojoExecutionException import org.apache.maven.plugin.testing.MojoRule import org.apache.maven.project.ProjectBuilder import org.apache.maven.project.ProjectBuildingRequest import org.eclipse.aether.DefaultRepositorySystemSession import org.junit.Before import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.jupiter.api.Assertions import kotlin.io.path.Path import kotlin.io.path.createTempFile import kotlin.io.path.div /** * Tests for mojo configuration. NB: this tests are using Junit4, because maven-plugin-testing-harness doesn't support 5. */ @Suppress("LongMethod", "TOO_LONG_FUNCTION") @Ignore class DiktatBaseMojoTest { @get:Rule val mojoRule = MojoRule() private lateinit var buildingRequest: ProjectBuildingRequest private lateinit var projectBuilder: ProjectBuilder /** * Initialize properties needed to create mavenProject stub able to resolve maven parameters. */ @Before fun setUp() { val executionRequest = DefaultMavenExecutionRequest() buildingRequest = executionRequest.projectBuildingRequest.apply { repositorySession = DefaultRepositorySystemSession() } projectBuilder = mojoRule.lookup(ProjectBuilder::class.java) } @Test fun `test default plugin configuration`() { val pom = createTempFile().toFile() pom.writeText( """ 4.0.0 com.saveourtool.diktat diktat-test 1.0.0-SNAPSHOT com.saveourtool.diktat diktat-maven-plugin check """.trimIndent() ) val mavenProject = projectBuilder.build(pom, buildingRequest).project val diktatCheckMojo = mojoRule.lookupConfiguredMojo(mavenProject, "check") as DiktatCheckMojo Assertions.assertEquals("diktat-analysis.yml", diktatCheckMojo.diktatConfigFile) Assertions.assertIterableEquals(listOf(pom.parentFile.toPath() / "src"), diktatCheckMojo.inputs.map { Path(it) }) Assertions.assertTrue(diktatCheckMojo.excludes.isEmpty()) } @Test fun `test plugin custom configuration`() { val pom = createTempFile().toFile() pom.writeText( """ 4.0.0 com.saveourtool.diktat diktat-test 1.0.0-SNAPSHOT com.saveourtool.diktat diktat-maven-plugin my-diktat-config.yml ${'$'}{project.basedir}/src/main/kotlin ${'$'}{project.basedir}/src/test/kotlin ${'$'}{project.basedir}/src/main/kotlin/exclusion check """.trimIndent() ) val mavenProject = projectBuilder.build(pom, buildingRequest).project val diktatCheckMojo = mojoRule.lookupConfiguredMojo(mavenProject, "check") as DiktatCheckMojo Assertions.assertEquals("my-diktat-config.yml", diktatCheckMojo.diktatConfigFile) Assertions.assertIterableEquals( listOf(pom.parentFile.toPath() / "src/main/kotlin", pom.parentFile.toPath() / "src/test/kotlin"), diktatCheckMojo.inputs.map { Path(it) } ) Assertions.assertIterableEquals( listOf(pom.parentFile.toPath() / "src/main/kotlin/exclusion"), diktatCheckMojo.excludes.map { Path(it) } ) val mojoExecutionException = Assertions.assertThrows(MojoExecutionException::class.java) { diktatCheckMojo.execute() } Assertions.assertEquals("Configuration file my-diktat-config.yml doesn't exist", mojoExecutionException.message) } } ================================================ FILE: diktat-maven-plugin/src/test/kotlin/com/saveourtool/diktat/plugin/maven/DiktatMavenPluginIntegrationTest.kt ================================================ package com.saveourtool.diktat.plugin.maven import com.soebes.itf.jupiter.extension.MavenGoal import com.soebes.itf.jupiter.extension.MavenJupiterExtension import com.soebes.itf.jupiter.extension.MavenTest import com.soebes.itf.jupiter.maven.MavenExecutionResult import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.SoftAssertions import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.TestInfo import java.io.File import kotlin.io.path.Path import kotlin.io.path.deleteIfExists import kotlin.io.path.div import kotlin.io.path.readText /** * Integration tests for diktat-maven-plugin. Run against the project from diktat-examples. * The whole pipeline is as follows: * * For each test case, test data is copied from examples with respect to maven-itf requirements, .mvn/jvm.config and .mvn/maven.config are copied, too. * **Note**: for maven-itf-plugin, test name should equal example project's directory name, which we have in `pom.xml`. * * maven-failsafe-plugin launches tests; for each test case a separate maven process is spawned. `diktat.version` is taken from `.mvn/maven.config` * and the exact value is written when maven copies resources. * * maven execution results are analyzed here; `.mvn/jvm.config` is used to attach jacoco java agent to every maven process and generate individual execution reports. * * When within an IDE (e.g.: _IDEA_), don't run this test directly: it's * expected to be executed from a forked JVM. Instead, run a `verify` _lifecycle * phase_ for the `diktat-maven-plugin` submodule, as if you were running * * ```console * $ mvn -pl diktat-maven-plugin verify * ``` * * from your terminal. If multiple JDKs are installed, be sure to pass * `JAVA_HOME` to the _run configuration_, so that the parent and the forked * JVMs have the same version. */ @MavenJupiterExtension class DiktatMavenPluginIntegrationTest { @BeforeEach fun beforeEach(testInfo: TestInfo) { val method = testInfo.testMethod.orElse(null) ?: return (Path("target") / "jacoco-it-${method.name}.exec").deleteIfExists() } @MavenTest @MavenGoal("diktat:check@diktat") fun diktatCheck(testInfo: TestInfo, result: MavenExecutionResult) { Assertions.assertEquals(1, result.returnCode) Assertions.assertFalse(result.isSuccessful) Assertions.assertTrue(result.isFailure) val mavenLog = result.mavenLog.stdout.readText() assertThat(mavenLog).contains("[FILE_NAME_MATCH_CLASS]") val method = testInfo.testMethod.get() File(result.mavenProjectResult.targetProjectDirectory.toFile(), "target/jacoco-it.exec").copyTo( File("target/jacoco-it-${method.name}.exec") ) } @MavenTest @MavenGoal("diktat:fix@diktat") fun diktatFix(testInfo: TestInfo, result: MavenExecutionResult) { Assertions.assertEquals(1, result.returnCode) Assertions.assertFalse(result.isSuccessful) Assertions.assertTrue(result.isFailure) val mavenLog = result.mavenLog.stdout.readText() with(SoftAssertions()) { try { assertThat(mavenLog).containsPattern("""Original and formatted content differ, writing to [:\w/\\-]+Test\.kt\.\.\.""") assertThat(mavenLog).containsPattern("There are \\d+ lint errors") assertThat(mavenLog).contains("[MISSING_KDOC_TOP_LEVEL]") } finally { assertAll() } } val method = testInfo.testMethod.get() File(result.mavenProjectResult.targetProjectDirectory.toFile(), "target/jacoco-it.exec").copyTo( File("target/jacoco-it-${method.name}.exec") ) } } ================================================ FILE: diktat-maven-plugin/src/test/resources/.mvn/jvm.config ================================================ -javaagent:${settings.localRepository}/org/jacoco/org.jacoco.agent/${jacoco.version}/org.jacoco.agent-${jacoco.version}-runtime.jar=destfile=target/jacoco-it.exec,includes=com.saveourtool.diktat.plugin.maven.* ================================================ FILE: diktat-maven-plugin/src/test/resources/.mvn/maven.config ================================================ -Ddiktat.version=${project.version} ================================================ FILE: diktat-rules/build.gradle.kts ================================================ @Suppress("DSL_SCOPE_VIOLATION", "RUN_IN_SCRIPT") // https://github.com/gradle/gradle/issues/22797 plugins { id("com.saveourtool.diktat.buildutils.kotlin-jvm-configuration") id("com.saveourtool.diktat.buildutils.code-quality-convention") id("com.saveourtool.diktat.buildutils.publishing-default-configuration") alias(libs.plugins.kotlin.ksp) idea } project.description = "The main diktat ruleset" dependencies { api(projects.diktatApi) implementation(libs.kotlin.stdlib.jdk8) implementation(libs.kotlin.compiler.embeddable) // kaml is used to read configs from YAML file implementation(libs.kaml) // guava is used for string case utils implementation(libs.guava) implementation(libs.kotlin.logging) testImplementation(projects.diktatCommonTest) testImplementation(projects.diktatKtlintEngine) testImplementation(libs.log4j2.slf4j2) testImplementation(libs.junit.jupiter) testImplementation(libs.junit.platform.suite) testImplementation(libs.assertj.core) // is used for simplifying boolean expressions implementation(libs.jbool.expressions) // generating compileOnly(projects.diktatDevKsp) ksp(projects.diktatDevKsp) testImplementation(libs.kotlin.reflect) } project.afterEvaluate { tasks.named("kspKotlin") { // not clear issue that :kspKotlin is up-to-date, but generated files are missed outputs.upToDateWhen { false } } tasks.named("test") { dependsOn(tasks.named("kspKotlin")) } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/common/config/rules/LegacyUtils.kt ================================================ /** * This file contains aliases to support old names and util methods */ package com.saveourtool.diktat.common.config.rules import com.saveourtool.diktat.api.DiktatRuleConfig import com.saveourtool.diktat.api.DiktatRuleNameAware import com.saveourtool.diktat.api.findByRuleName /** * Name of common configuration */ const val DIKTAT_COMMON = "DIKTAT_COMMON" typealias RuleConfiguration = com.saveourtool.diktat.ruleset.config.RuleConfiguration typealias CommonConfiguration = com.saveourtool.diktat.ruleset.config.CommonConfiguration /** * Get [DiktatRuleConfig] for particular [DiktatRuleNameAware] object. * * @param rule a [DiktatRuleNameAware] which configuration will be returned * @return [DiktatRuleConfig] for a particular rule if it is found, else null */ fun List.getRuleConfig(rule: DiktatRuleNameAware): DiktatRuleConfig? = this.findByRuleName(rule) /** * @return common configuration from list of all rules configuration */ fun List.getCommonConfiguration(): CommonConfiguration = CommonConfiguration(getCommonConfig()?.configuration) /** * Get [DiktatRuleConfig] representing common configuration part that can be used in any rule */ private fun List.getCommonConfig() = find { it.name == DIKTAT_COMMON } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/config/AbstractDiktatRuleConfigReader.kt ================================================ package com.saveourtool.diktat.ruleset.config import com.saveourtool.diktat.api.DiktatRuleConfig import com.saveourtool.diktat.api.DiktatRuleConfigReader import com.saveourtool.diktat.common.config.rules.DIKTAT_COMMON import com.saveourtool.diktat.ruleset.constants.Warnings import io.github.oshai.kotlinlogging.KotlinLogging import org.jetbrains.kotlin.org.jline.utils.Levenshtein import java.io.IOException import java.io.InputStream import kotlin.jvm.Throws /** * This class is used to read input stream in any format that you will specify. * Usage: * 1) implement this class with implementing the method: * a. parse - implement parser for your file format (for example parse it to a proper json) * 2) Use your new class MyReader().read(someInputStream) */ abstract class AbstractDiktatRuleConfigReader : DiktatRuleConfigReader { /** * @param inputStream - input stream * @return list of [DiktatRuleConfig] if resource has been parsed successfully */ override fun invoke(inputStream: InputStream): List = read(inputStream)?.onEach(::validate).orEmpty() private fun read(inputStream: InputStream): List? = try { parse(inputStream) } catch (e: IOException) { log.error(e) { "Cannot read config from input stream due to: " } null } /** * you can specify your own parser, in example for parsing stream as a json * * @param inputStream a [InputStream] representing loaded content * @return resource parsed as list of [DiktatRuleConfig] * @throws IOException */ @Throws(IOException::class) protected abstract fun parse(inputStream: InputStream): List private fun validate(config: DiktatRuleConfig) = require(config.name == DIKTAT_COMMON || config.name in Warnings.names) { val closestMatch = Warnings.names.minByOrNull { Levenshtein.distance(it, config.name) } "Warning name <${config.name}> in configuration file is invalid, did you mean <$closestMatch>?" } companion object { private val log = KotlinLogging.logger {} } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/config/CommonConfiguration.kt ================================================ package com.saveourtool.diktat.ruleset.config import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KotlinLogging import java.util.Locale import java.util.concurrent.atomic.AtomicInteger /** * class returns the list of common configurations that we have read from a configuration map * * @param configuration map of common configuration */ data class CommonConfiguration(private val configuration: Map?) { /** * List of directory names which will be used to detect test sources */ val testAnchors: List by lazy { val testDirs = (configuration ?: emptyMap()).getOrDefault("testDirs", "test").split(',').map { it.trim() } if (testDirs.any { !it.lowercase(Locale.getDefault()).endsWith("test") }) { log.error { "test directory names should end with `test`" } } testDirs } /** * Start of package name, which shoould be common, e.g. org.example.myproject */ val domainName: String? by lazy { configuration?.get("domainName") } /** * Get disable chapters from configuration */ val disabledChapters: String? by lazy { configuration?.get("disabledChapters") } /** * Get version of kotlin from configuration */ val kotlinVersion: KotlinVersion by lazy { configuration?.get("kotlinVersion")?.kotlinVersion() ?: run { if (visitorCounter.incrementAndGet() == 1) { log.error { "Kotlin version not specified in the configuration file. Will be using ${KotlinVersion.CURRENT} version" } } KotlinVersion.CURRENT } } /** * Get source directories from configuration */ val srcDirectories: List by lazy { configuration?.get("srcDirectories")?.split(",")?.map { it.trim() } ?: listOf("main") } companion object { internal val log: KLogger = KotlinLogging.logger {} /** * Counter that helps not to raise multiple warnings about kotlin version */ var visitorCounter = AtomicInteger(0) } } /** * Parse string into KotlinVersion * * @return KotlinVersion from configuration */ internal fun String.kotlinVersion(): KotlinVersion { require(this.contains("^(\\d+\\.)(\\d+)\\.?(\\d+)?$".toRegex())) { "Kotlin version format is incorrect" } val versions = this.split(".").map { it.toInt() } return if (versions.size == 2) { KotlinVersion(versions[0], versions[1]) } else { KotlinVersion(versions[0], versions[1], versions[2]) } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/config/DiktatRuleConfigYamlReader.kt ================================================ package com.saveourtool.diktat.ruleset.config import com.saveourtool.diktat.common.config.rules.RulesConfig import com.charleskorn.kaml.Yaml import com.charleskorn.kaml.YamlConfiguration import com.charleskorn.kaml.decodeFromStream import java.io.InputStream /** * class returns the list of configurations that we have read from a yml: diktat-analysis.yml */ class DiktatRuleConfigYamlReader : AbstractDiktatRuleConfigReader() { private val yamlSerializer by lazy { Yaml(configuration = YamlConfiguration(strictMode = true)) } /** * Parse resource file into list of [RulesConfig] * * @param inputStream a [InputStream] representing loaded rules config file * @return list of [RulesConfig] */ override fun parse(inputStream: InputStream): List = yamlSerializer.decodeFromStream>(inputStream) } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/config/RuleConfiguration.kt ================================================ package com.saveourtool.diktat.ruleset.config /** * Configuration that allows customizing additional options of particular rules. * @property config a map of strings with configuration options for a particular rule */ open class RuleConfiguration(protected val config: Map) ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/constants/Chapters.kt ================================================ package com.saveourtool.diktat.ruleset.constants import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.common.config.rules.getCommonConfiguration import com.saveourtool.diktat.ruleset.utils.isDigits import org.jetbrains.kotlin.org.jline.utils.Levenshtein /** * This class represents the chapters that are in our code style. * * @property number - number of chapter * @property title name of chapter */ @Suppress("WRONG_DECLARATIONS_ORDER") enum class Chapters(val number: String, val title: String) { DUMMY("0", "Dummy"), NAMING("1", "Naming"), COMMENTS("2", "Comments"), TYPESETTING("3", "General"), VARIABLES("4", "Variables"), FUNCTIONS("5", "Functions"), CLASSES("6", "Classes"), ; } /** * Function checks if warning from enable chapter * * @param configRules list of rules configuration * @return is warning from enable chapter */ fun Warnings.isRuleFromActiveChapter(configRules: List): Boolean { val chapterFromRule = getChapterByWarning() val configuration = configRules.getCommonConfiguration() val disabledChapters = configuration.disabledChapters ?.takeIf { it.isNotBlank() } ?.split(",") ?.map { it.trim() } ?.mapNotNull { chap -> if (chap.isDigits()) { Chapters.values().find { chap == it.number } } else { validate(chap) Chapters.values().find { it.title == chap } } } return disabledChapters?.let { return chapterFromRule !in it } ?: true } /** * Function get chapter by warning * * @return chapter to which warning refers */ @Suppress("UnsafeCallOnNullableType") fun Warnings.getChapterByWarning() = Chapters.values().find { it.number == this.ruleId.first().toString() }!! private fun validate(chapter: String) = require(chapter in Chapters.values().map { it.title }) { val closestMatch = Chapters.values().minByOrNull { Levenshtein.distance(it.title, chapter) } "Chapter name <$chapter> in configuration file is invalid, did you mean <$closestMatch>?" } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/constants/Warnings.kt ================================================ package com.saveourtool.diktat.ruleset.constants import com.saveourtool.diktat.api.DiktatErrorEmitter import com.saveourtool.diktat.api.isRuleEnabled import com.saveourtool.diktat.common.config.rules.Rule import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.generation.EnumNames import com.saveourtool.diktat.ruleset.utils.isSuppressed import org.jetbrains.kotlin.com.intellij.lang.ASTNode /** * This class represent individual inspections of diktat code style. * A [Warnings] entry contains rule name, warning message and is used in code check. * * @param warn description of the inspection * @property canBeAutoCorrected whether this inspection can automatically fix the code. Should be public to be able to use it in docs generator. * @property ruleId number of the inspection according to [diktat code style](https://github.com/saveourtool/diktat/blob/master/info/guide/diktat-coding-convention.md) */ @Suppress( "ForbiddenComment", "MagicNumber", "WRONG_DECLARATIONS_ORDER", "MaxLineLength", "WRONG_NEWLINES" ) @EnumNames( generatedPackageName = "generated", generatedClassName = "WarningNames", ) enum class Warnings( @Suppress("PRIVATE_MEMBER") val canBeAutoCorrected: Boolean, val ruleId: String, private val warn: String) : Rule { // ======== dummy test warning ====== DUMMY_TEST_WARNING(true, "0.0.0", "this is a dummy warning that can be used for manual testing of fixer/checker"), // ======== chapter 1 ======== PACKAGE_NAME_MISSING(true, "1.2.1", "no package name declared in a file"), PACKAGE_NAME_INCORRECT_CASE(true, "1.2.1", "package name should be completely in a lower case"), PACKAGE_NAME_INCORRECT_PREFIX(true, "1.2.1", "package name should start from company's domain"), // FixMe: should add autofix PACKAGE_NAME_INCORRECT_SYMBOLS(false, "1.2.1", "package name should contain only latin (ASCII) letters or numbers. For separation of words use dot"), PACKAGE_NAME_INCORRECT_PATH(true, "1.2.1", "package name does not match the directory hierarchy for this file, the real package name should be"), INCORRECT_PACKAGE_SEPARATOR(true, "1.2.1", "package name parts should be separated only by dots - there should be no other symbols like underscores (_)"), CLASS_NAME_INCORRECT(true, "1.3.1", "class/enum/interface name should be in PascalCase and should contain only latin (ASCII) letters or numbers"), OBJECT_NAME_INCORRECT(true, "1.3.1", "object structure name should be in PascalCase and should contain only latin (ASCII) letters or numbers"), VARIABLE_NAME_INCORRECT_FORMAT(true, "1.6.1", "variable name should be in lowerCamelCase and should contain only latin (ASCII) letters or numbers and should start from lower letter"), VARIABLE_NAME_INCORRECT(false, "1.1.1", "variable name should contain more than one letter"), CONSTANT_UPPERCASE(true, "1.5.1", " properties from companion object or on file level mostly in all cases are constants - please use upper snake case for them"), VARIABLE_HAS_PREFIX(true, "1.1.1", "variable has prefix (like mVariable or M_VARIABLE), generally it is a bad code style (Android - is the only exception)"), IDENTIFIER_LENGTH(false, "1.1.1", "identifier's length is incorrect, it should be in range of [2, 64] symbols"), ENUM_VALUE(true, "1.3.1", "enum values should be in a proper format/case"), GENERIC_NAME(true, "1.1.1", "generic name should contain only one single capital letter, it can be followed by a number"), BACKTICKS_PROHIBITED(false, "1.1.1", "backticks should not be used in identifier's naming. The only exception test methods marked with @Test annotation"), FUNCTION_NAME_INCORRECT_CASE(true, "1.4.1", "function/method name should be in lowerCamelCase"), TYPEALIAS_NAME_INCORRECT_CASE(true, "1.3.1", "typealias name should be in pascalCase"), FUNCTION_BOOLEAN_PREFIX(true, "1.6.2", "functions that return the value of Boolean type should have or prefix"), FILE_NAME_INCORRECT(true, "1.1.1", "file name is incorrect - it should end with .kt extension and be in PascalCase"), EXCEPTION_SUFFIX(true, "1.1.1", "all exception classes should have \"Exception\" suffix"), CONFUSING_IDENTIFIER_NAMING(false, "1.1.1", "it's a bad name for identifier"), // ======== chapter 2 ======== MISSING_KDOC_TOP_LEVEL(false, "2.1.1", "all public and internal top-level classes and functions should have Kdoc"), MISSING_KDOC_CLASS_ELEMENTS(false, "2.1.1", "all public, internal and protected classes, functions and variables inside the class should have Kdoc"), MISSING_KDOC_ON_FUNCTION(true, "2.1.1", "all public, internal and protected functions should have Kdoc with proper tags"), KDOC_TRIVIAL_KDOC_ON_FUNCTION(false, "2.3.1", "KDocs should not be trivial (e.g. method getX should not de documented as 'returns X')"), KDOC_WITHOUT_PARAM_TAG(true, "2.1.2", "all methods which take arguments should have @param tags in KDoc"), KDOC_WITHOUT_RETURN_TAG(true, "2.1.2", "all methods which return values should have @return tag in KDoc"), KDOC_WITHOUT_THROWS_TAG(true, "2.1.2", "all methods which throw exceptions should have @throws tag in KDoc"), KDOC_EMPTY_KDOC(false, "2.1.3", "KDoc should never be empty"), KDOC_WRONG_SPACES_AFTER_TAG(true, "2.1.3", "there should be exactly one white space after tag name in KDoc"), KDOC_WRONG_TAGS_ORDER(true, "2.1.3", "in KDoc standard tags are arranged in this order: @receiver, @param, @property, @return, @throws or @exception, @constructor, but are"), KDOC_NEWLINES_BEFORE_BASIC_TAGS(true, "2.1.3", "in KDoc block of standard tags @param, @return, @throws should contain newline before only if there is other content before it"), KDOC_NO_NEWLINES_BETWEEN_BASIC_TAGS(true, "2.1.3", "in KDoc standard tags @param, @return, @throws should not containt newline between them, but these tags do"), KDOC_NO_NEWLINE_AFTER_SPECIAL_TAGS(true, "2.1.3", "in KDoc there should be exactly one empty line after special tags"), KDOC_NO_EMPTY_TAGS(false, "2.2.1", "no empty descriptions in tag blocks are allowed"), KDOC_NO_DEPRECATED_TAG(true, "2.1.3", "KDoc doesn't support @deprecated tag, use @Deprecated annotation instead"), KDOC_NO_CONSTRUCTOR_PROPERTY(true, "2.1.1", "all properties from the primary constructor should be documented in a @property tag in KDoc"), KDOC_NO_CLASS_BODY_PROPERTIES_IN_HEADER(true, "2.1.1", "only properties from the primary constructor should be documented in a @property tag in class KDoc"), KDOC_EXTRA_PROPERTY(false, "2.1.1", "There is property in KDoc which is not present in the class"), KDOC_DUPLICATE_PROPERTY(false, "2.1.1", "There's a property in KDoc which is already present"), KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT(true, "2.1.1", "replace comment before property with @property tag in class KDoc"), KDOC_CONTAINS_DATE_OR_AUTHOR(false, "2.1.3", "KDoc should not contain creation date and author name"), HEADER_WRONG_FORMAT(true, "2.2.1", "file header comments should be properly formatted"), HEADER_MISSING_OR_WRONG_COPYRIGHT(true, "2.2.1", "file header comment must include copyright information inside a block comment"), WRONG_COPYRIGHT_YEAR(true, "2.2.1", "year defined in copyright and current year are different"), HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE(false, "2.2.1", "files that contain multiple or no classes should contain description of what is inside of this file"), HEADER_NOT_BEFORE_PACKAGE(true, "2.2.1", "header KDoc should be placed before package and imports"), COMMENTED_OUT_CODE(false, "2.4.2", "you should not comment out code, use VCS to save it in history and delete this block"), COMMENTED_BY_KDOC(true, "2.1.1", "you should not comment inside code blocks using kdoc syntax"), WRONG_NEWLINES_AROUND_KDOC(true, "2.4.1", "there should be a blank line above the kDoc and there should not be no blank lines after kDoc"), FIRST_COMMENT_NO_BLANK_LINE(true, "2.4.1", "there should not be any blank lines before first comment"), COMMENT_WHITE_SPACE(true, "2.4.1", "there should be a white space between code and comment also between code start token and comment text"), IF_ELSE_COMMENTS(true, "2.4.1", "invalid comments structure. Comment should be inside the block"), // ======== chapter 3 ======== FILE_IS_TOO_LONG(false, "3.1.1", "file has more number of lines than expected"), FILE_CONTAINS_ONLY_COMMENTS(false, "3.1.2", "empty files or files that contain only comments should be avoided"), FILE_INCORRECT_BLOCKS_ORDER(true, "3.1.2", "general structure of kotlin source file is wrong, parts are in incorrect order"), FILE_NO_BLANK_LINE_BETWEEN_BLOCKS(true, "3.1.2", "general structure of kotlin source file is wrong, general code blocks should be separated by empty lines"), FILE_UNORDERED_IMPORTS(true, "3.1.2", "imports should be ordered alphabetically and shouldn't be separated by newlines"), FILE_WILDCARD_IMPORTS(false, "3.1.2", "wildcard imports should not be used"), UNUSED_IMPORT(true, "3.1.2", "unused imports should be removed"), NO_BRACES_IN_CONDITIONALS_AND_LOOPS(true, "3.2.1", "in if, else, when, for, do, and while statements braces should be used. Exception: single line ternary operator statement"), WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES(true, "3.1.4", "the declaration part of a class-like code structures (class/interface/etc.) should be in the proper order"), BLANK_LINE_BETWEEN_PROPERTIES(true, "3.1.4", "there should be no blank lines between properties without comments; comment, KDoc or annotation on property should have blank" + " line before"), TOP_LEVEL_ORDER(true, "3.1.5", "the declaration part of a top level elements should be in the proper order"), BRACES_BLOCK_STRUCTURE_ERROR(true, "3.2.2", "braces should follow 1TBS style"), WRONG_INDENTATION(true, "3.3.1", "only spaces are allowed for indentation and each indentation should equal to 4 spaces (tabs are not allowed)"), EMPTY_BLOCK_STRUCTURE_ERROR(true, "3.4.1", "incorrect format of empty block"), MORE_THAN_ONE_STATEMENT_PER_LINE(true, "3.6.1", "there should not be more than one code statement in one line"), LONG_LINE(true, "3.5.1", "this line is longer than allowed"), REDUNDANT_SEMICOLON(true, "3.6.2", "there should be no redundant semicolon at the end of lines"), WRONG_NEWLINES(true, "3.6.2", "incorrect line breaking"), TRAILING_COMMA(true, "3.6.2", "use trailing comma"), COMPLEX_EXPRESSION(false, "3.6.3", "complex dot qualified expression should be replaced with variable"), COMPLEX_BOOLEAN_EXPRESSION(true, "3.6.4", "simplification could be produced for the too complex boolean expression"), // FixMe: autofixing will be added for this rule STRING_CONCATENATION(true, "3.15.1", "strings should not be concatenated using plus operator - use string templates instead if the statement fits one line"), TOO_MANY_BLANK_LINES(true, "3.7.1", "too many consecutive blank lines"), WRONG_WHITESPACE(true, "3.8.1", "incorrect usage of whitespaces for code separation"), TOO_MANY_CONSECUTIVE_SPACES(true, "3.8.1", "too many consecutive spaces"), ANNOTATION_NEW_LINE(true, "3.12.1", "annotations must be on new line"), PREVIEW_ANNOTATION(true, "3.12.2", "method, annotated with `@Preview` annotation should be private and has `Preview` suffix"), ENUMS_SEPARATED(true, "3.9.1", "enum is incorrectly formatted"), WHEN_WITHOUT_ELSE(true, "3.11.1", "each 'when' statement must have else at the end"), LONG_NUMERICAL_VALUES_SEPARATED(true, "3.14.2", "long numerical values should be separated with underscore"), MAGIC_NUMBER(false, "3.14.3", "avoid using magic numbers, instead define constants with clear names describing what the magic number means"), WRONG_DECLARATIONS_ORDER(true, "3.1.4", "declarations of constants and enum members should be sorted alphabetically"), WRONG_MULTIPLE_MODIFIERS_ORDER(true, "3.14.1", "sequence of modifier-keywords is incorrect"), LOCAL_VARIABLE_EARLY_DECLARATION(false, "3.10.2", "local variables should be declared close to the line where they are first used"), STRING_TEMPLATE_CURLY_BRACES(true, "3.15.2", "string template has redundant curly braces"), STRING_TEMPLATE_QUOTES(true, "3.15.2", "string template has redundant quotes"), FILE_NAME_MATCH_CLASS(true, "3.1.2", "file name is incorrect - it should match with the class described in it if there is the only one class declared"), COLLAPSE_IF_STATEMENTS(true, "3.16.1", "avoid using redundant nested if-statements, which could be collapsed into a single one"), CONVENTIONAL_RANGE(true, "3.17.1", "use conventional rule for range case"), DEBUG_PRINT(false, "3.18.1", "use a dedicated logging library"), // ======== chapter 4 ======== NULLABLE_PROPERTY_TYPE(true, "4.3.1", "try to avoid use of nullable types"), TYPE_ALIAS(false, "4.2.2", "variable's type is too complex and should be replaced with typealias"), SMART_CAST_NEEDED(true, "4.2.1", "you can omit explicit casting"), SAY_NO_TO_VAR(false, "4.1.3", "Usage of a mutable variables with [var] modifier - is a bad style, use [val] instead"), GENERIC_VARIABLE_WRONG_DECLARATION(true, "4.3.2", "variable should have explicit type declaration"), // FixMe: change float literal to BigDecimal? Or kotlin equivalent? FLOAT_IN_ACCURATE_CALCULATIONS(false, "4.1.1", "floating-point values shouldn't be used in accurate calculations"), AVOID_NULL_CHECKS(true, "4.3.3", "Try to avoid explicit null-checks"), // ======== chapter 5 ======== TOO_LONG_FUNCTION(false, "5.1.1", "function is too long: split it or make more primitive"), AVOID_NESTED_FUNCTIONS(true, "5.1.3", "try to avoid using nested functions"), LAMBDA_IS_NOT_LAST_PARAMETER(false, "5.2.1", "lambda inside function parameters should be in the end"), TOO_MANY_PARAMETERS(false, "5.2.2", "function has too many parameters"), NESTED_BLOCK(false, "5.1.2", "function has too many nested blocks and should be simplified"), WRONG_OVERLOADING_FUNCTION_ARGUMENTS(false, "5.2.3", "use default argument instead of function overloading"), RUN_BLOCKING_INSIDE_ASYNC(false, "5.2.4", "avoid using runBlocking in asynchronous code"), TOO_MANY_LINES_IN_LAMBDA(false, "5.2.5", "long lambdas should have a parameter name instead of it"), CUSTOM_LABEL(false, "5.2.6", "avoid using expression with custom label"), PARAMETER_NAME_IN_OUTER_LAMBDA(false, "5.2.7", "outer lambdas should have a parameter name instead of `it`"), INVERSE_FUNCTION_PREFERRED(true, "5.1.4", "it is better to use inverse function"), // ======== chapter 6 ======== SINGLE_CONSTRUCTOR_SHOULD_BE_PRIMARY(true, "6.1.1", "if a class has single constructor, it should be converted to a primary constructor"), USE_DATA_CLASS(false, "6.1.2", "this class can be converted to a data class"), WRONG_NAME_OF_VARIABLE_INSIDE_ACCESSOR(false, "6.1.9", "use `field` keyword instead of property name inside property accessors"), MULTIPLE_INIT_BLOCKS(true, "6.1.4", "avoid using multiple `init` blocks, this logic can be moved to constructors or properties declarations"), CLASS_SHOULD_NOT_BE_ABSTRACT(true, "6.1.6", "class should not be abstract, because it has no abstract functions"), CUSTOM_GETTERS_SETTERS(false, "6.1.8", "custom getters and setters are not recommended, use class methods instead"), COMPACT_OBJECT_INITIALIZATION(true, "6.1.11", "class instance can be initialized in `apply` block"), USELESS_SUPERTYPE(true, "6.1.5", "unnecessary supertype specification"), TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED(true, "6.1.10", "trivial property accessors are not recommended"), EXTENSION_FUNCTION_SAME_SIGNATURE(false, "6.2.2", "extension functions should not have same signature if their receiver classes are related"), EMPTY_PRIMARY_CONSTRUCTOR(true, "6.1.3", "avoid empty primary constructor"), NO_CORRESPONDING_PROPERTY(false, "6.1.7", "backing property should have the same name, but there is no corresponding property"), AVOID_USING_UTILITY_CLASS(false, "6.4.1", "avoid using utility classes/objects, use extension functions instead"), OBJECT_IS_PREFERRED(true, "6.4.2", "it is better to use object for stateless classes"), INLINE_CLASS_CAN_BE_USED(true, "6.1.12", "inline class can be used"), EXTENSION_FUNCTION_WITH_CLASS(false, "6.2.3", "do not use extension functions for the class defined in the same file"), RUN_IN_SCRIPT(true, "6.5.1", "wrap blocks of code in top-level scope functions like `run`"), USE_LAST_INDEX(true, "6.2.4", "Instead of \"length - 1\" need to use built-in \"lastIndex\" operation"), ; /** * Name of the inspection, it is used in configuration and in output. */ override fun ruleName() = this.name /** * Warning message that will be logged to analysis report */ fun warnText() = "[${ruleName()}] ${this.warn}:" /** * @param configRules list of [RulesConfig] * @param emit function that will be called on warning * @param freeText text that will be added to the warning message * @param offset offset from the beginning of the file * @param node the [ASTNode] on which the warning was triggered * @param shouldBeAutoCorrected should be auto corrected or not * @param isFixMode whether autocorrect mode is on * @param autoFix function that will be called to autocorrect the warning */ @Suppress("LongParameterList", "TOO_MANY_PARAMETERS") fun warnOnlyOrWarnAndFix( configRules: List, emit: DiktatErrorEmitter, freeText: String, offset: Int, node: ASTNode, shouldBeAutoCorrected: Boolean, isFixMode: Boolean, autoFix: () -> Unit, ) { if (shouldBeAutoCorrected) { warnAndFix(configRules, emit, isFixMode, freeText, offset, node, autoFix) } else { warn(configRules, emit, freeText, offset, node) } } /** * @param configRules list of [RulesConfig] * @param emit function that will be called on warning * @param isFixMode whether autocorrect mode is on * @param freeText text that will be added to the warning message * @param offset offset from the beginning of the file * @param node the [ASTNode] on which the warning was triggered * @param autoFix function that will be called to autocorrect the warning */ @Suppress("LongParameterList", "TOO_MANY_PARAMETERS") fun warnAndFix( configRules: List, emit: DiktatErrorEmitter, isFixMode: Boolean, freeText: String, offset: Int, node: ASTNode, autoFix: () -> Unit, ) { require(canBeAutoCorrected) { "warnAndFix is called, but canBeAutoCorrected is false" } doWarn(configRules, emit, freeText, offset, node, true) fix(configRules, isFixMode, node, autoFix) } /** * @param configs list of [RulesConfig] * @param emit function that will be called on warning * @param freeText text that will be added to the warning message * @param offset offset from the beginning of the file * @param node the [ASTNode] on which the warning was triggered */ @Suppress("LongParameterList", "TOO_MANY_PARAMETERS") fun warn( configs: List, emit: DiktatErrorEmitter, freeText: String, offset: Int, node: ASTNode, ) { doWarn(configs, emit, freeText, offset, node, false) } @Suppress("LongParameterList", "TOO_MANY_PARAMETERS") private fun doWarn( configs: List, emit: DiktatErrorEmitter, freeText: String, offset: Int, node: ASTNode, canBeAutoCorrected: Boolean, ) { if (isRuleFromActiveChapter(configs) && configs.isRuleEnabled(this) && !node.isSuppressed(name, this, configs)) { val trimmedFreeText = freeText .lines() .run { if (size > 1) "${first()}..." else first() } emit( offset, errorMessage = "${this.warnText()} $trimmedFreeText", canBeAutoCorrected = canBeAutoCorrected, ) } } private inline fun fix( configs: List, isFix: Boolean, node: ASTNode, autoFix: () -> Unit) { if (isRuleFromActiveChapter(configs) && configs.isRuleEnabled(this) && isFix && !node.isSuppressed(name, this, configs)) { autoFix() } } companion object { val names by lazy { entries.map { it.name } } } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/DiktatRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules import com.saveourtool.diktat.api.DiktatErrorEmitter import com.saveourtool.diktat.api.DiktatRuleNameAware import com.saveourtool.diktat.api.isRuleEnabled import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.utils.getFilePathSafely import io.github.oshai.kotlinlogging.KotlinLogging import org.jetbrains.kotlin.com.intellij.lang.ASTNode private typealias DiktatRuleApi = com.saveourtool.diktat.api.DiktatRule /** * This is a wrapper around _KtLint_ `com.pinterest.ktlint.core.Rule`. * * @param inspections warnings that are used in the rule's code * @property id id of the rule * @property configRules all rules from configuration */ @Suppress("TooGenericExceptionCaught") abstract class DiktatRule( override val id: String, val configRules: List, private val inspections: List, ) : DiktatRuleApi { /** * Default value is false */ var isFixMode: Boolean = false /** * The **file-specific** error emitter, initialized in * [invoke] and used in [logic] implementations. * * Since the file is indirectly a part of the state of a `Rule`, the same * `Rule` instance should **never be re-used** to check more than a single * file, or confusing effects (incl. race conditions) will occur. * See the documentation of the [com.pinterest.ktlint.core.Rule] class for more details. * * @see com.pinterest.ktlint.core.Rule * @see invoke * @see logic */ lateinit var emitWarn: DiktatErrorEmitter /** * @param node * @param autoCorrect * @param emitter * @throws Error */ @Suppress("TooGenericExceptionThrown") override fun invoke( node: ASTNode, autoCorrect: Boolean, emitter: DiktatErrorEmitter ) { emitWarn = emitter isFixMode = autoCorrect if (areInspectionsDisabled()) { return } else { try { logic(node) } catch (internalError: Throwable) { log.error( internalError ) { """Internal error has occurred in rule [$id]. Please make an issue on this bug at https://github.com/saveourtool/diKTat/. As a workaround you can disable these inspections in yml config: <$inspections>. Root cause of the problem is in [${node.getFilePathSafely()}] file. """.trimIndent() } // we are very sorry for throwing common Error here, but unfortunately we are not able to throw // any existing Exception, as they will be caught in ktlint framework and the logging will be confusing: // in this case it will incorrectly ask you to report issues in diktat to ktlint repository throw Error("Internal error in diktat application", internalError) } } } private fun areInspectionsDisabled(): Boolean = inspections.none { configRules.isRuleEnabled(it) } /** * Logic of the rule * * @param node node that are coming from visit */ abstract fun logic(node: ASTNode) companion object { private val log = KotlinLogging.logger {} } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/DiktatRuleSetFactoryImpl.kt ================================================ package com.saveourtool.diktat.ruleset.rules import com.saveourtool.diktat.api.DiktatRuleConfig import com.saveourtool.diktat.api.DiktatRuleSet import com.saveourtool.diktat.api.DiktatRuleSetFactory import com.saveourtool.diktat.ruleset.rules.chapter1.FileNaming import com.saveourtool.diktat.ruleset.rules.chapter1.IdentifierNaming import com.saveourtool.diktat.ruleset.rules.chapter1.PackageNaming import com.saveourtool.diktat.ruleset.rules.chapter2.comments.CommentsRule import com.saveourtool.diktat.ruleset.rules.chapter2.comments.HeaderCommentRule import com.saveourtool.diktat.ruleset.rules.chapter2.kdoc.CommentsFormatting import com.saveourtool.diktat.ruleset.rules.chapter2.kdoc.KdocComments import com.saveourtool.diktat.ruleset.rules.chapter2.kdoc.KdocFormatting import com.saveourtool.diktat.ruleset.rules.chapter2.kdoc.KdocMethods import com.saveourtool.diktat.ruleset.rules.chapter3.AnnotationNewLineRule import com.saveourtool.diktat.ruleset.rules.chapter3.BlockStructureBraces import com.saveourtool.diktat.ruleset.rules.chapter3.BooleanExpressionsRule import com.saveourtool.diktat.ruleset.rules.chapter3.BracesInConditionalsAndLoopsRule import com.saveourtool.diktat.ruleset.rules.chapter3.ClassLikeStructuresOrderRule import com.saveourtool.diktat.ruleset.rules.chapter3.CollapseIfStatementsRule import com.saveourtool.diktat.ruleset.rules.chapter3.ConsecutiveSpacesRule import com.saveourtool.diktat.ruleset.rules.chapter3.DebugPrintRule import com.saveourtool.diktat.ruleset.rules.chapter3.EmptyBlock import com.saveourtool.diktat.ruleset.rules.chapter3.EnumsSeparated import com.saveourtool.diktat.ruleset.rules.chapter3.LineLength import com.saveourtool.diktat.ruleset.rules.chapter3.LongNumericalValuesSeparatedRule import com.saveourtool.diktat.ruleset.rules.chapter3.MagicNumberRule import com.saveourtool.diktat.ruleset.rules.chapter3.MultipleModifiersSequence import com.saveourtool.diktat.ruleset.rules.chapter3.NullableTypeRule import com.saveourtool.diktat.ruleset.rules.chapter3.PreviewAnnotationRule import com.saveourtool.diktat.ruleset.rules.chapter3.RangeConventionalRule import com.saveourtool.diktat.ruleset.rules.chapter3.SingleLineStatementsRule import com.saveourtool.diktat.ruleset.rules.chapter3.SortRule import com.saveourtool.diktat.ruleset.rules.chapter3.StringConcatenationRule import com.saveourtool.diktat.ruleset.rules.chapter3.StringTemplateFormatRule import com.saveourtool.diktat.ruleset.rules.chapter3.TrailingCommaRule import com.saveourtool.diktat.ruleset.rules.chapter3.WhenMustHaveElseRule import com.saveourtool.diktat.ruleset.rules.chapter3.files.BlankLinesRule import com.saveourtool.diktat.ruleset.rules.chapter3.files.FileSize import com.saveourtool.diktat.ruleset.rules.chapter3.files.FileStructureRule import com.saveourtool.diktat.ruleset.rules.chapter3.files.IndentationRule import com.saveourtool.diktat.ruleset.rules.chapter3.files.NewlinesRule import com.saveourtool.diktat.ruleset.rules.chapter3.files.SemicolonsRule import com.saveourtool.diktat.ruleset.rules.chapter3.files.TopLevelOrderRule import com.saveourtool.diktat.ruleset.rules.chapter3.files.WhiteSpaceRule import com.saveourtool.diktat.ruleset.rules.chapter3.identifiers.LocalVariablesRule import com.saveourtool.diktat.ruleset.rules.chapter4.ImmutableValNoVarRule import com.saveourtool.diktat.ruleset.rules.chapter4.NullChecksRule import com.saveourtool.diktat.ruleset.rules.chapter4.SmartCastRule import com.saveourtool.diktat.ruleset.rules.chapter4.TypeAliasRule import com.saveourtool.diktat.ruleset.rules.chapter4.VariableGenericTypeDeclarationRule import com.saveourtool.diktat.ruleset.rules.chapter4.calculations.AccurateCalculationsRule import com.saveourtool.diktat.ruleset.rules.chapter5.AsyncAndSyncRule import com.saveourtool.diktat.ruleset.rules.chapter5.AvoidNestedFunctionsRule import com.saveourtool.diktat.ruleset.rules.chapter5.CheckInverseMethodRule import com.saveourtool.diktat.ruleset.rules.chapter5.CustomLabel import com.saveourtool.diktat.ruleset.rules.chapter5.FunctionArgumentsSize import com.saveourtool.diktat.ruleset.rules.chapter5.FunctionLength import com.saveourtool.diktat.ruleset.rules.chapter5.LambdaLengthRule import com.saveourtool.diktat.ruleset.rules.chapter5.LambdaParameterOrder import com.saveourtool.diktat.ruleset.rules.chapter5.NestedFunctionBlock import com.saveourtool.diktat.ruleset.rules.chapter5.OverloadingArgumentsFunction import com.saveourtool.diktat.ruleset.rules.chapter5.ParameterNameInOuterLambdaRule import com.saveourtool.diktat.ruleset.rules.chapter6.AvoidEmptyPrimaryConstructor import com.saveourtool.diktat.ruleset.rules.chapter6.AvoidUtilityClass import com.saveourtool.diktat.ruleset.rules.chapter6.CustomGetterSetterRule import com.saveourtool.diktat.ruleset.rules.chapter6.ExtensionFunctionsInFileRule import com.saveourtool.diktat.ruleset.rules.chapter6.ExtensionFunctionsSameNameRule import com.saveourtool.diktat.ruleset.rules.chapter6.ImplicitBackingPropertyRule import com.saveourtool.diktat.ruleset.rules.chapter6.PropertyAccessorFields import com.saveourtool.diktat.ruleset.rules.chapter6.RunInScript import com.saveourtool.diktat.ruleset.rules.chapter6.TrivialPropertyAccessors import com.saveourtool.diktat.ruleset.rules.chapter6.UseLastIndex import com.saveourtool.diktat.ruleset.rules.chapter6.UselessSupertype import com.saveourtool.diktat.ruleset.rules.chapter6.classes.AbstractClassesRule import com.saveourtool.diktat.ruleset.rules.chapter6.classes.CompactInitialization import com.saveourtool.diktat.ruleset.rules.chapter6.classes.DataClassesRule import com.saveourtool.diktat.ruleset.rules.chapter6.classes.InlineClassesRule import com.saveourtool.diktat.ruleset.rules.chapter6.classes.SingleConstructorRule import com.saveourtool.diktat.ruleset.rules.chapter6.classes.SingleInitRule import com.saveourtool.diktat.ruleset.rules.chapter6.classes.StatelessClassesRule /** * _KtLint_-agnostic factory which creates a [DiktatRuleSet]. * * By default, it is expected to have `diktat-analysis.yml` configuration in the root folder where 'ktlint' is run * otherwise it will use default configuration where some rules are disabled. */ class DiktatRuleSetFactoryImpl : DiktatRuleSetFactory { /** * This method is going to be called once for each file (which means if any * of the rules have state or are not thread-safe - a new [DiktatRuleSet] must * be created). * * For each invocation of [com.pinterest.ktlint.core.KtLintRuleEngine.lint] and [com.pinterest.ktlint.core.KtLintRuleEngine.format] the [DiktatRuleSet] * is retrieved. * This results in new instances of each [com.pinterest.ktlint.core.Rule] for each file being * processed. * As of that a [com.pinterest.ktlint.core.Rule] does not need to be thread-safe. * * However, [com.pinterest.ktlint.core.KtLintRuleEngine.format] requires the [com.pinterest.ktlint.core.Rule] to be executed twice on a * file in case at least one violation has been autocorrected. * As the same [Rule] instance is reused for the second execution of the * [Rule], the state of the [Rule] is shared. * As of this [Rule] have to clear their internal state. * * @return a default [DiktatRuleSet] */ @Suppress( "LongMethod", "TOO_LONG_FUNCTION", ) override fun invoke(rulesConfig: List): DiktatRuleSet { // Note: the order of rules is important in autocorrect mode. For example, all rules that add new code should be invoked before rules that fix formatting. // We don't have a way to enforce a specific order, so we should just be careful when adding new rules to this list and, when possible, // cover new rules in smoke test as well. If a rule needs to be at a specific position in a list, please add comment explaining it (like for NewlinesRule). val rules = sequenceOf( // semicolons, comments, documentation ::SemicolonsRule, ::CommentsRule, ::SingleConstructorRule, // this rule can add properties to a primary constructor, so should be before KdocComments ::KdocComments, ::KdocMethods, ::KdocFormatting, ::CommentsFormatting, // naming ::FileNaming, ::PackageNaming, ::IdentifierNaming, // code structure ::UselessSupertype, ::ClassLikeStructuresOrderRule, ::WhenMustHaveElseRule, ::BracesInConditionalsAndLoopsRule, ::EmptyBlock, ::AvoidEmptyPrimaryConstructor, ::TopLevelOrderRule, ::SingleLineStatementsRule, ::MultipleModifiersSequence, ::TrivialPropertyAccessors, ::CustomGetterSetterRule, ::CompactInitialization, // other rules ::UseLastIndex, ::InlineClassesRule, ::ExtensionFunctionsInFileRule, ::CheckInverseMethodRule, ::StatelessClassesRule, ::ImplicitBackingPropertyRule, ::DataClassesRule, ::LocalVariablesRule, ::SmartCastRule, ::AvoidUtilityClass, ::PropertyAccessorFields, ::AbstractClassesRule, ::TrailingCommaRule, ::SingleInitRule, ::RangeConventionalRule, ::DebugPrintRule, ::CustomLabel, ::VariableGenericTypeDeclarationRule, ::LongNumericalValuesSeparatedRule, ::NestedFunctionBlock, ::AnnotationNewLineRule, ::PreviewAnnotationRule, ::SortRule, ::EnumsSeparated, ::StringConcatenationRule, ::StringTemplateFormatRule, ::AccurateCalculationsRule, ::CollapseIfStatementsRule, ::LineLength, ::RunInScript, ::TypeAliasRule, ::OverloadingArgumentsFunction, ::FunctionLength, ::MagicNumberRule, ::LambdaParameterOrder, ::FunctionArgumentsSize, ::BlankLinesRule, ::FileSize, ::AsyncAndSyncRule, ::NullableTypeRule, ::NullChecksRule, ::ImmutableValNoVarRule, ::AvoidNestedFunctionsRule, ::ExtensionFunctionsSameNameRule, ::LambdaLengthRule, ::BooleanExpressionsRule, ::ParameterNameInOuterLambdaRule, // formatting: moving blocks, adding line breaks, indentations etc. ::BlockStructureBraces, ::ConsecutiveSpacesRule, ::HeaderCommentRule, ::FileStructureRule, // this rule should be right before indentation because it should operate on already valid code ::NewlinesRule, // newlines need to be inserted right before fixing indentation ::WhiteSpaceRule, // this rule should be after other rules that can cause wrong spacing ::IndentationRule, // indentation rule should be the last because it fixes formatting after all the changes done by previous rules ) .map { it(rulesConfig) } .toList() return DiktatRuleSet(rules) } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter1/FileNaming.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter1 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.FILE_NAME_INCORRECT import com.saveourtool.diktat.ruleset.constants.Warnings.FILE_NAME_MATCH_CLASS import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.getAllChildrenWithType import com.saveourtool.diktat.ruleset.utils.getFilePath import com.saveourtool.diktat.ruleset.utils.getFirstChildWithType import com.saveourtool.diktat.ruleset.utils.isPascalCase import com.saveourtool.diktat.util.isKotlinScript import org.jetbrains.kotlin.KtNodeTypes.CLASS import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.lexer.KtTokens.IDENTIFIER import org.jetbrains.kotlin.psi.stubs.elements.KtFileElementType import java.io.File /** * This visitor covers rule 1.2 of Huawei code style. It covers following rules related to a file naming: * 1) File must have ".kt" extension * 2) File must be in camel case * 3) File name must start with capital letter (PascalCase) * * Aggressive: In case file contains only one class on upper level - it should be named with the same name */ @Suppress("ForbiddenComment") class FileNaming(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(FILE_NAME_INCORRECT, FILE_NAME_MATCH_CLASS) ) { private lateinit var filePath: String override fun logic(node: ASTNode) { if (node.elementType == KtFileElementType.INSTANCE) { filePath = node.getFilePath() if (!filePath.isKotlinScript()) { checkFileNaming(node) checkClassNameMatchesWithFile(node) } } } private fun checkFileNaming(node: ASTNode) { val (name, extension) = getFileParts(filePath) if (!name.isPascalCase() || !validExtensions.contains(extension)) { // FixMe: we can add an autocorrect here in future, but is there any purpose to change file or class name? FILE_NAME_INCORRECT.warn(configRules, emitWarn, "$name$extension", 0, node) } } @Suppress("UnsafeCallOnNullableType", "ControlFlowWithEmptyBody") private fun checkClassNameMatchesWithFile(fileLevelNode: ASTNode) { val (fileNameWithoutSuffix, fileNameSuffix) = getFileParts(filePath) val classes = fileLevelNode.getAllChildrenWithType(CLASS) if (classes.size == 1) { val className = classes[0].getFirstChildWithType(IDENTIFIER)!!.text if (className != fileNameWithoutSuffix) { // FixMe: we can add an autocorrect here in future, but is there any purpose to change file name? FILE_NAME_MATCH_CLASS.warn(configRules, emitWarn, "$fileNameWithoutSuffix$fileNameSuffix vs $className", 0, fileLevelNode) } } else { // FixMe: need to check that if there are several classes - at least one of them should match } } private fun getFileParts(fileName: String): Pair { val file = File(fileName) val fileNameWithoutSuffix = file.name.replace(Regex("\\..*"), "") val fileNameSuffix = file.name.replace(fileNameWithoutSuffix, "") return Pair(fileNameWithoutSuffix, fileNameSuffix) } companion object { // FixMe: should be moved to properties const val NAME_ID = "file-naming" val validExtensions = listOf(".kt", ".kts") } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter1/IdentifierNaming.kt ================================================ @file:Suppress("FILE_WILDCARD_IMPORTS") package com.saveourtool.diktat.ruleset.rules.chapter1 import com.saveourtool.diktat.common.config.rules.RuleConfiguration import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.common.config.rules.getRuleConfig import com.saveourtool.diktat.ruleset.constants.Warnings.BACKTICKS_PROHIBITED import com.saveourtool.diktat.ruleset.constants.Warnings.CLASS_NAME_INCORRECT import com.saveourtool.diktat.ruleset.constants.Warnings.CONFUSING_IDENTIFIER_NAMING import com.saveourtool.diktat.ruleset.constants.Warnings.CONSTANT_UPPERCASE import com.saveourtool.diktat.ruleset.constants.Warnings.ENUM_VALUE import com.saveourtool.diktat.ruleset.constants.Warnings.EXCEPTION_SUFFIX import com.saveourtool.diktat.ruleset.constants.Warnings.FUNCTION_BOOLEAN_PREFIX import com.saveourtool.diktat.ruleset.constants.Warnings.FUNCTION_NAME_INCORRECT_CASE import com.saveourtool.diktat.ruleset.constants.Warnings.GENERIC_NAME import com.saveourtool.diktat.ruleset.constants.Warnings.IDENTIFIER_LENGTH import com.saveourtool.diktat.ruleset.constants.Warnings.OBJECT_NAME_INCORRECT import com.saveourtool.diktat.ruleset.constants.Warnings.TYPEALIAS_NAME_INCORRECT_CASE import com.saveourtool.diktat.ruleset.constants.Warnings.VARIABLE_HAS_PREFIX import com.saveourtool.diktat.ruleset.constants.Warnings.VARIABLE_NAME_INCORRECT import com.saveourtool.diktat.ruleset.constants.Warnings.VARIABLE_NAME_INCORRECT_FORMAT import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.* import com.saveourtool.diktat.ruleset.utils.search.findAllVariablesWithUsages import org.jetbrains.kotlin.KtNodeTypes import org.jetbrains.kotlin.KtNodeTypes.CATCH import org.jetbrains.kotlin.KtNodeTypes.CLASS import org.jetbrains.kotlin.KtNodeTypes.DESTRUCTURING_DECLARATION import org.jetbrains.kotlin.KtNodeTypes.DESTRUCTURING_DECLARATION_ENTRY import org.jetbrains.kotlin.KtNodeTypes.FUNCTION_TYPE import org.jetbrains.kotlin.KtNodeTypes.OBJECT_DECLARATION import org.jetbrains.kotlin.KtNodeTypes.PROPERTY import org.jetbrains.kotlin.KtNodeTypes.REFERENCE_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.TYPE_PARAMETER import org.jetbrains.kotlin.KtNodeTypes.TYPE_REFERENCE import org.jetbrains.kotlin.KtNodeTypes.VALUE_PARAMETER_LIST import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement import org.jetbrains.kotlin.com.intellij.psi.tree.TokenSet import org.jetbrains.kotlin.kdoc.lexer.KDocTokens.KDOC import org.jetbrains.kotlin.kdoc.parser.KDocKnownTag import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.lexer.KtTokens.CATCH_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.IDENTIFIER import org.jetbrains.kotlin.psi.KtParameter import org.jetbrains.kotlin.psi.KtPrimaryConstructor import org.jetbrains.kotlin.psi.KtProperty import org.jetbrains.kotlin.psi.psiUtil.getParentOfType import org.jetbrains.kotlin.psi.psiUtil.isPrivate import org.jetbrains.kotlin.psi.psiUtil.parents import org.jetbrains.kotlin.psi.stubs.elements.KtFileElementType import java.util.Locale /** * This visitor covers rules: 1.2, 1.3, 1.4, 1.5 of Huawei code style. It covers following rules: * 1) All identifiers should use only ASCII letters or digits, and the names should match regular expressions \w{2,64} * exceptions: variables like i,j,k * 2) constants from companion object should have UPPER_SNAKE_CASE * 3) fields/variables should have lowerCamelCase and should not contain prefixes * 4) interfaces/classes/annotations/enums/object names should be in PascalCase * 5) methods: function names should be in camel case, methods that return boolean value should have "is"/"has" prefix * 6) custom exceptions: PascalCase and Exception suffix * 7) FixMe: should prohibit identifiers with free format with `` (except test functions) * * // FixMe: very important, that current implementation cannot fix identifier naming properly, * // FixMe: because it fixes only declaration without the usages */ @Suppress("ForbiddenComment", "MISSING_KDOC_CLASS_ELEMENTS") class IdentifierNaming(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(BACKTICKS_PROHIBITED, VARIABLE_NAME_INCORRECT, VARIABLE_NAME_INCORRECT_FORMAT, CONSTANT_UPPERCASE, VARIABLE_HAS_PREFIX, CONFUSING_IDENTIFIER_NAMING, GENERIC_NAME, CLASS_NAME_INCORRECT, ENUM_VALUE, EXCEPTION_SUFFIX, FUNCTION_BOOLEAN_PREFIX, FUNCTION_NAME_INCORRECT_CASE, IDENTIFIER_LENGTH, OBJECT_NAME_INCORRECT, TYPEALIAS_NAME_INCORRECT_CASE) ) { private val allMethodPrefixes by lazy { if (configuration.allowedBooleanPrefixes.isEmpty()) { booleanMethodPrefixes } else { booleanMethodPrefixes + configuration.allowedBooleanPrefixes.filter { it.isNotEmpty() } } } val configuration by lazy { BooleanFunctionsConfiguration( this.configRules.getRuleConfig(FUNCTION_BOOLEAN_PREFIX)?.configuration ?: emptyMap() ) } override fun logic( node: ASTNode ) { // backticks are prohibited for identifier declarations everywhere except test methods that are marked with @Test annotation if (isIdentifierWithBackticks(node)) { return } // isVariable is used as a workaround to check corner case with variables that have length == 1 val (identifierNodes, isVariable) = when (node.elementType) { // covers interface, class, enum class and annotation class names KtNodeTypes.CLASS -> Pair(checkClassNamings(node), false) // covers "object" code blocks KtNodeTypes.OBJECT_DECLARATION -> Pair(checkObjectNaming(node), false) // covers variables (val/var), constants (const val) and parameters for lambdas KtNodeTypes.PROPERTY, KtNodeTypes.VALUE_PARAMETER -> Pair(checkVariableName(node), true) // covers case of enum values KtNodeTypes.ENUM_ENTRY -> Pair(checkEnumValues(node), false) // covers global functions, extensions and class methods KtNodeTypes.FUN -> Pair(checkFunctionName(node), false) // covers case of typeAlias values KtNodeTypes.TYPEALIAS -> Pair(checkTypeAliases(node), false) else -> Pair(null, false) } identifierNodes?.let { checkIdentifierLength(it, isVariable) } } /** * method checks that identifier is wrapped over with backticks (``) */ private fun isIdentifierWithBackticks(node: ASTNode): Boolean { val identifier = node.getIdentifierName() if (identifier != null && node.elementType != REFERENCE_EXPRESSION) { // node is a symbol declaration with present identifier val identifierText = identifier.text if (identifierText.startsWith('`') && identifierText.endsWith('`')) { val isTestFun = node.elementType == KtNodeTypes.FUN && node.hasTestAnnotation() if (!isTestFun) { BACKTICKS_PROHIBITED.warn(configRules, emitWarn, identifierText, identifier.startOffset, identifier) } return true } } return false } /** * all checks for case and naming for vals/vars/constants */ @Suppress( "SAY_NO_TO_VAR", "TOO_LONG_FUNCTION", "LongMethod", "ComplexMethod", "UnsafeCallOnNullableType", ) private fun checkVariableName(node: ASTNode): List { val configuration = ConstantUpperCaseConfiguration( configRules.getRuleConfig(CONSTANT_UPPERCASE)?.configuration ?: emptyMap()) val exceptionNames = configuration.exceptionConstNames // special case for Destructuring declarations that can be treated as parameters in lambda: var namesOfVariables = extractVariableIdentifiers(node) // Only local private properties will be autofix in order not to break code if there are usages in other files. // Destructuring declarations are only allowed for local variables/values, so we don't need to calculate `isFix` for every node in `namesOfVariables` val isPublicOrNonLocalProperty = if (node.elementType == KtNodeTypes.PROPERTY) (node.psi as KtProperty).run { !isLocal && !isPrivate() } else false val isNonPrivatePrimaryConstructorParameter = (node.psi as? KtParameter)?.run { hasValOrVar() && getParentOfType(true)?.valueParameters?.contains(this) == true && !isPrivate() } ?: false val shouldBeAutoCorrected = !(isPublicOrNonLocalProperty || isNonPrivatePrimaryConstructorParameter) namesOfVariables .forEach { variableName -> // variable should not contain only one letter in it's name. This is a bad example: b512 // but no need to raise a warning here if length of a variable. In this case we will raise IDENTIFIER_LENGTH if (variableName.text.containsOneLetterOrZero() && variableName.text.length > 1) { VARIABLE_NAME_INCORRECT.warn(configRules, emitWarn, variableName.text, variableName.startOffset, node) } // check if identifier of a property has a confusing name if (confusingIdentifierNames.contains(variableName.text) && !isValidCatchIdentifier(variableName) && node.elementType == KtNodeTypes.PROPERTY ) { warnConfusingName(variableName) } // check for constant variables - check for val from companion object or on global file level // it should be in UPPER_CASE, no need to raise this warning if it is one-letter variable if (node.isConstant()) { if (!exceptionNames.contains(variableName.text) && !variableName.text.isUpperSnakeCase() && variableName.text.length > 1) { CONSTANT_UPPERCASE.warnOnlyOrWarnAndFix( configRules = configRules, emit = emitWarn, freeText = variableName.text, offset = variableName.startOffset, node = node, shouldBeAutoCorrected = shouldBeAutoCorrected, isFixMode = isFixMode, ) { (variableName as LeafPsiElement).rawReplaceWithText(variableName.text.toDeterministic { toUpperSnakeCase() }) } } } else if (variableName.text != "_" && !variableName.text.isLowerCamelCase() && // variable name should be in camel case. The only exception is a list of industry standard variables like i, j, k. !isPairPropertyBackingField(null, node.psi as? KtProperty) ) { VARIABLE_NAME_INCORRECT_FORMAT.warnOnlyOrWarnAndFix( configRules = configRules, emit = emitWarn, freeText = variableName.text, offset = variableName.startOffset, node = node, shouldBeAutoCorrected = shouldBeAutoCorrected, isFixMode = isFixMode, ) { // FixMe: cover fixes with tests val correctVariableName = variableName.text.toDeterministic { toLowerCamelCase() } variableName .parent { it.elementType == KtFileElementType.INSTANCE } ?.findAllVariablesWithUsages { it.name == variableName.text } ?.flatMap { it.value.toList() } ?.forEach { (it.node.firstChildNode as LeafPsiElement).rawReplaceWithText(correctVariableName) } if (variableName.treeParent.psi.run { (this is KtProperty && isMember) || (this is KtParameter && getParentOfType(true)?.valueParameters?.contains(this) == true) }) { // For class members also check `@property` KDoc tag. // If we are here, then `variableName` is definitely a node from a class or an object. (variableName.parent(CLASS) ?: variableName.parent(OBJECT_DECLARATION))?.findChildByType(KDOC)?.kDocTags() ?.firstOrNull { it.knownTag == KDocKnownTag.PROPERTY && it.getSubjectName() == variableName.text } ?.run { (getSubjectLink()!!.node.findAllDescendantsWithSpecificType(IDENTIFIER).single() as LeafPsiElement).rawReplaceWithText(correctVariableName) } } (variableName as LeafPsiElement).rawReplaceWithText(correctVariableName) } } } // need to get new node in case we have already converted the case before (and replaced the child node) // we need to recalculate it twice, because nodes could have been changed by "rawReplaceWithText" function namesOfVariables = extractVariableIdentifiers(node) namesOfVariables .forEach { variableName -> // generally, variables with prefixes are not allowed (like mVariable, xCode, iValue) if (variableName.text.hasPrefix()) { VARIABLE_HAS_PREFIX.warnAndFix(configRules, emitWarn, isFixMode, variableName.text, variableName.startOffset, node) { (variableName as LeafPsiElement).rawReplaceWithText(variableName.text.removePrefix()) } } } return namesOfVariables } /** * Warns that variable have a confusing name */ private fun warnConfusingName(variableName: ASTNode) { val warnText = when (variableName.text) { "O", "D" -> "better name is: obj, dgt" "I", "l" -> "better name is: it, ln, line" "Z" -> "better name is: n1, n2" "S" -> "better name is: xs, str" "e" -> "better name is: ex, elm" "B" -> "better name is: bt, nxt" "h", "n" -> "better name is: nr, head, height" "m", "rn" -> "better name is: mbr, item" else -> "" } CONFUSING_IDENTIFIER_NAMING.warn(configRules, emitWarn, warnText, variableName.startOffset, variableName) } /** * Getting identifiers (aka variable names) from parent nodes like PROPERTY. * Several things to take into account here: * * need to handle DESTRUCTURING_DECLARATION correctly, as it does not have IDENTIFIER leaf. * * function type can have VALUE_PARAMETERs without name */ @Suppress("UnsafeCallOnNullableType") private fun extractVariableIdentifiers(node: ASTNode): List { val destructingDeclaration = node.getFirstChildWithType(DESTRUCTURING_DECLARATION) val result = if (destructingDeclaration != null) { destructingDeclaration.getAllChildrenWithType(DESTRUCTURING_DECLARATION_ENTRY) .map { it.getIdentifierName()!! } } else if (node.parents().count() > 1 && node.treeParent.elementType == VALUE_PARAMETER_LIST && node.treeParent.treeParent.elementType == FUNCTION_TYPE ) { listOfNotNull(node.getIdentifierName()) } else { listOf(node.getIdentifierName()!!) } // no need to do checks if variables are in a special list with exceptions return result.filterNot { oneCharIdentifiers.contains(it.text) } } /** * basic check for class naming (PascalCase) * and checks for generic type declared for this class */ private fun checkClassNamings(node: ASTNode): List { val genericType: ASTNode? = node.getTypeParameterList() if (genericType != null && !validGenericTypeName(genericType)) { // FixMe: should fix generic name here GENERIC_NAME.warn(configRules, emitWarn, genericType.text, genericType.startOffset, genericType) } val className: ASTNode = node.getIdentifierName() ?: return emptyList() if (!(className.text.isPascalCase())) { CLASS_NAME_INCORRECT.warnAndFix(configRules, emitWarn, isFixMode, className.text, className.startOffset, className) { (className as LeafPsiElement).rawReplaceWithText(className.text.toDeterministic { toPascalCase() }) } } checkExceptionSuffix(node) return listOf(className) } /** * all exceptions should have Exception suffix * */ private fun checkExceptionSuffix(node: ASTNode) { val classNameNode = node.getIdentifierName() ?: return // getting super class name val superClassName: String? = node .getFirstChildWithType(KtNodeTypes.SUPER_TYPE_LIST) ?.findLeafWithSpecificType(TYPE_REFERENCE) ?.text if (superClassName != null && hasExceptionSuffix(superClassName) && !hasExceptionSuffix(classNameNode.text)) { EXCEPTION_SUFFIX.warnAndFix(configRules, emitWarn, isFixMode, classNameNode.text, classNameNode.startOffset, classNameNode) { // FixMe: need to add tests for this (classNameNode as LeafPsiElement).rawReplaceWithText(classNameNode.text + "Exception") } } } private fun hasExceptionSuffix(text: String) = text.lowercase(Locale.getDefault()).endsWith("exception") /** * basic check for object naming of code blocks (PascalCase) * fix: fixing object name to PascalCase */ private fun checkObjectNaming(node: ASTNode): List { // if this object is companion object or anonymous object - it does not have any name val objectName: ASTNode = node.getIdentifierName() ?: return emptyList() if (!objectName.text.isPascalCase()) { OBJECT_NAME_INCORRECT.warnAndFix(configRules, emitWarn, isFixMode, objectName.text, objectName.startOffset, objectName) { (objectName as LeafPsiElement).rawReplaceWithText(objectName.text.toDeterministic { toPascalCase() }) } } return listOf(objectName) } /** * check that Enum values match correct case and style * node has ENUM_ENTRY type * to check all variables will need to check all IDENTIFIERS in ENUM_ENTRY */ private fun checkEnumValues(node: ASTNode): List { val enumValues: List = node.getChildren(null).filter { it.elementType == KtTokens.IDENTIFIER } enumValues.forEach { value -> val configuration = IdentifierNamingConfiguration( configRules.getRuleConfig(ENUM_VALUE)?.configuration ?: emptyMap() ) val validator = when (configuration.enumStyle) { Style.PASCAL_CASE -> String::isPascalCase Style.SNAKE_CASE -> String::isUpperSnakeCase } val autofix = when (configuration.enumStyle) { Style.PASCAL_CASE -> String::toPascalCase Style.SNAKE_CASE -> String::toUpperSnakeCase } if (!validator(value.text)) { ENUM_VALUE.warnAndFix( configRules, emitWarn, isFixMode, "${value.text} (should be in ${configuration.enumStyle.str})", value.startOffset, value ) { // FixMe: add tests for this (value as LeafPsiElement).rawReplaceWithText(autofix(value.text)) } } if (confusingIdentifierNames.contains(value.text)) { warnConfusingName(value) } } return enumValues } /** * Check function name: * 1) function names should be in camel case * 2) methods that return boolean value should have "is"/"has" prefix * 3) FixMe: The function name is usually a verb or verb phrase (need to add check/fix for it) * 4) backticks are prohibited in the naming of non-test methods */ @Suppress("UnsafeCallOnNullableType") private fun checkFunctionName(node: ASTNode): List? { val functionName = node.getIdentifierName() ?: return null // basic check for camel case if (!functionName.text.isLowerCamelCase()) { FUNCTION_NAME_INCORRECT_CASE.warnAndFix(configRules, emitWarn, isFixMode, functionName.text, functionName.startOffset, functionName) { // FixMe: add tests for this (functionName as LeafPsiElement).rawReplaceWithText(functionName.text.toDeterministic { toLowerCamelCase() }) } } // We don't need to ask subclasses to rename superclass methods if (!node.isOverridden()) { // check for methods that return Boolean // if function has Boolean return type in 99% of cases it is much better to name it with isXXX or hasXXX prefix @Suppress("COLLAPSE_IF_STATEMENTS") if (node.hasBooleanReturnType() && !node.isOperatorFun() && allMethodPrefixes.none { functionName.text.startsWith(it) }) { // FixMe: add agressive autofix for this FUNCTION_BOOLEAN_PREFIX.warn(configRules, emitWarn, functionName.text, functionName.startOffset, functionName) } } return listOf(functionName) } @Suppress("UnsafeCallOnNullableType") private fun checkTypeAliases(node: ASTNode): List { val aliasName = node.getIdentifierName()!! if (!aliasName.text.isPascalCase()) { TYPEALIAS_NAME_INCORRECT_CASE.warnAndFix(configRules, emitWarn, isFixMode, aliasName.text, aliasName.startOffset, aliasName) { (aliasName as LeafPsiElement).rawReplaceWithText(aliasName.text.toDeterministic { toPascalCase() }) } } return listOf(aliasName) } /** * check that generic name has single capital letter, can be followed by a number * this method will check it for both generic classes and generic methods */ private fun validGenericTypeName(generic: ASTNode) = generic.getChildren(TokenSet.create(TYPE_PARAMETER)).all { val typeText = it.getIdentifierName()?.text ?: return false // first letter should always be a capital and other letters - are digits typeText[0] in 'A'..'Z' && (typeText.length == 1 || typeText.substring(1).isDigits()) } /** * identifier name length should not be longer than 64 symbols and shorter than 2 symbols */ private fun checkIdentifierLength( nodes: List, isVariable: Boolean ) { nodes.forEach { val isValidOneCharVariable = oneCharIdentifiers.contains(it.text) && isVariable if (it.text != "_" && !it.isTextLengthInRange(MIN_IDENTIFIER_LENGTH..MAX_IDENTIFIER_LENGTH) && !isValidOneCharVariable && !isValidCatchIdentifier(it) ) { IDENTIFIER_LENGTH.warn(configRules, emitWarn, it.text, it.startOffset, it) } } } /** * exception case for identifiers used in catch block: * catch (e: Exception) {} */ private fun isValidCatchIdentifier(node: ASTNode): Boolean { val parentValueParamList = node.findParentNodeWithSpecificType(VALUE_PARAMETER_LIST) val prevCatchKeyWord = parentValueParamList?.prevCodeSibling()?.elementType == CATCH_KEYWORD return node.text == "e" && node.findParentNodeWithSpecificType(CATCH) != null && prevCatchKeyWord } /** * [RuleConfiguration] for identifier naming */ class IdentifierNamingConfiguration(config: Map) : RuleConfiguration(config) { @Suppress("CUSTOM_GETTERS_SETTERS") private val Style.isEnumStyle: Boolean get() = listOf(Style.PASCAL_CASE, Style.SNAKE_CASE).contains(this) /** * In which style enum members should be named */ val enumStyle = config["enumStyle"]?.let { styleString -> val style = Style.entries.firstOrNull { it.name == styleString.toUpperSnakeCase() } if (style == null || !style.isEnumStyle) { error("$styleString is unsupported for enum style") } style } ?: Style.SNAKE_CASE } class ConstantUpperCaseConfiguration(config: Map) : RuleConfiguration(config) { val exceptionConstNames = config["exceptionConstNames"]?.split(',') ?: emptyList() } class BooleanFunctionsConfiguration(config: Map) : RuleConfiguration(config) { /** * A list of functions that return boolean and are allowed to use. Input is in a form "foo, bar". */ val allowedBooleanPrefixes = config["allowedPrefixes"]?.split(",")?.map { it.trim() } ?: emptyList() } companion object { private const val MAX_DETERMINISTIC_RUNS = 5 const val MAX_IDENTIFIER_LENGTH = 64 const val MIN_IDENTIFIER_LENGTH = 2 const val NAME_ID = "identifier-naming" // FixMe: this should be moved to properties val oneCharIdentifiers = setOf("i", "j", "k", "x", "y", "z") val booleanMethodPrefixes = setOf("has", "is", "are", "have", "should", "can") val confusingIdentifierNames = setOf("O", "D", "I", "l", "Z", "S", "e", "B", "h", "n", "m", "rn") private fun String.toDeterministic(function: String.() -> String): String = generateSequence(function(this), function) .runningFold(this to false) { (current, result), next -> require(!result) { "Should return a value already" } next to (current == next) } .take(MAX_DETERMINISTIC_RUNS) .first { it.second } .first } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter1/PackageNaming.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter1 import com.saveourtool.diktat.common.config.rules.CommonConfiguration import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.common.config.rules.getCommonConfiguration import com.saveourtool.diktat.ruleset.constants.Warnings.INCORRECT_PACKAGE_SEPARATOR import com.saveourtool.diktat.ruleset.constants.Warnings.PACKAGE_NAME_INCORRECT_CASE import com.saveourtool.diktat.ruleset.constants.Warnings.PACKAGE_NAME_INCORRECT_PATH import com.saveourtool.diktat.ruleset.constants.Warnings.PACKAGE_NAME_INCORRECT_PREFIX import com.saveourtool.diktat.ruleset.constants.Warnings.PACKAGE_NAME_INCORRECT_SYMBOLS import com.saveourtool.diktat.ruleset.constants.Warnings.PACKAGE_NAME_MISSING import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.* import com.saveourtool.diktat.util.isKotlinScript import io.github.oshai.kotlinlogging.KotlinLogging import org.jetbrains.kotlin.KtNodeTypes.DOT_QUALIFIED_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.FILE_ANNOTATION_LIST import org.jetbrains.kotlin.KtNodeTypes.IMPORT_LIST import org.jetbrains.kotlin.KtNodeTypes.PACKAGE_DIRECTIVE import org.jetbrains.kotlin.KtNodeTypes.REFERENCE_EXPRESSION import org.jetbrains.kotlin.com.intellij.lang.ASTFactory import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl import org.jetbrains.kotlin.kdoc.lexer.KDocTokens.KDOC import org.jetbrains.kotlin.konan.file.File import org.jetbrains.kotlin.lexer.KtTokens.BLOCK_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.EOL_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.IDENTIFIER import org.jetbrains.kotlin.lexer.KtTokens.PACKAGE_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.WHITE_SPACE import org.jetbrains.kotlin.psi.psiUtil.children import java.util.concurrent.atomic.AtomicInteger /** * Rule 1.3: package name is in lower case and separated by dots, code developed internally in your company (in example Huawei) should start * with it's domain (like com.huawei), and the package name is allowed to have numbers * * Current limitations and FixMe: * need to support autofixing of directories in the same way as package is named. For example if we have package name: * package a.b.c.D -> then class D should be placed in a/b/c/ directories */ @Suppress("ForbiddenComment", "TOO_MANY_LINES_IN_LAMBDA") class PackageNaming(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(INCORRECT_PACKAGE_SEPARATOR, PACKAGE_NAME_INCORRECT_CASE, PACKAGE_NAME_MISSING, PACKAGE_NAME_INCORRECT_PATH, PACKAGE_NAME_INCORRECT_PREFIX, PACKAGE_NAME_INCORRECT_SYMBOLS), ) { private lateinit var domainName: String override fun logic(node: ASTNode) { val configuration = configRules.getCommonConfiguration() configuration.domainName?.let { domainName = it if (node.elementType == PACKAGE_DIRECTIVE) { val filePath = node.getFilePath() // getting all identifiers from existing package name into the list like [org, diktat, project] val wordsInPackageName = node.findAllDescendantsWithSpecificType(IDENTIFIER) if (wordsInPackageName.isEmpty() && filePath.isKotlinScript()) { // kotlin scripts are allowed to have empty package; in this case we don't suggest a new one and don't run checks return } // calculating package name based on the directory where the file is placed val realPackageName = calculateRealPackageName(filePath, configuration) // if node isLeaf - this means that there is no package name declared if (node.isLeaf() && !filePath.isKotlinScript()) { warnAndFixMissingPackageName(node, realPackageName, filePath) return } // no need to check that packageIdentifiers is empty, because in this case parsing will fail checkPackageName(wordsInPackageName, node) // fix in checkFilePathMatchesWithPackageName is much more aggressive than fixes in checkPackageName, they can conflict checkFilePathMatchesWithPackageName(wordsInPackageName, realPackageName, node) } } ?: if (visitorCounter.incrementAndGet() == 1) { log.error { "Not able to find an external configuration for domain" + " name in the common configuration (is it missing in yml config?)" } } else { @Suppress("RedundantUnitExpression") Unit } } /** * checking and fixing the case when package directive is missing in the file */ private fun warnAndFixMissingPackageName( initialPackageDirectiveNode: ASTNode, realPackageName: List, filePath: String ) { val fileName = filePath.substringAfterLast(File.separator) // if the file path contains "buildSrc" - don't add the package name to the file val isBuildSrcPath = "buildSrc" in filePath if (!isBuildSrcPath) { PACKAGE_NAME_MISSING.warnAndFix(configRules, emitWarn, isFixMode, fileName, initialPackageDirectiveNode.startOffset, initialPackageDirectiveNode) { if (realPackageName.isNotEmpty()) { // creating node for package directive using Kotlin parser val newPackageDirectiveName = realPackageName.joinToString(PACKAGE_SEPARATOR) insertNewPackageName(initialPackageDirectiveNode, newPackageDirectiveName) } } } } /** * calculating real package name based on the directory path where the file is stored * * @return list with words that are parts of package name like [org, diktat, name] */ private fun calculateRealPackageName(fileName: String, configuration: CommonConfiguration): List { val filePathParts = fileName .splitPathToDirs() .dropLast(1) // remove filename .flatMap { it.split(".") } return if (!filePathParts.contains(PACKAGE_PATH_ANCHOR)) { log.error { "Not able to determine a path to a scanned file or \"$PACKAGE_PATH_ANCHOR\" directory cannot be found in it's path." + " Will not be able to determine correct package name. It can happen due to missing <$PACKAGE_PATH_ANCHOR> directory in the path" } emptyList() } else { // creating a real package name: // 1) getting a path after the base project directory (after "src" directory) // 2) removing src/main/kotlin/java/e.t.c dirs // 3) adding company's domain name at the beginning val allDirs = languageDirNames + configuration.srcDirectories + configuration.testAnchors val fileSubDir = filePathParts.subList(filePathParts.lastIndexOf(PACKAGE_PATH_ANCHOR), filePathParts.size) .dropWhile { allDirs.contains(it) } // no need to add DOMAIN_NAME to the package name if it is already in path val domainPrefix = if (!fileSubDir.joinToString(PACKAGE_SEPARATOR).startsWith(domainName)) domainName.split(PACKAGE_SEPARATOR) else emptyList() domainPrefix + fileSubDir } } private fun checkPackageName(wordsInPackageName: List, packageDirectiveNode: ASTNode) { // all words should be in a lower case (lower case letters/digits/underscore) wordsInPackageName .filter { word -> word.text.hasUppercaseLetter() } .forEach { word -> PACKAGE_NAME_INCORRECT_CASE.warnAndFix(configRules, emitWarn, isFixMode, word.text, word.startOffset, word) { word.toLower() } } // package name should start from a company's domain name if (!isDomainMatches(wordsInPackageName)) { PACKAGE_NAME_INCORRECT_PREFIX.warnAndFix(configRules, emitWarn, isFixMode, domainName, wordsInPackageName[0].startOffset, wordsInPackageName[0]) { val oldPackageName = wordsInPackageName.joinToString(PACKAGE_SEPARATOR) { it.text } val newPackageName = "$domainName$PACKAGE_SEPARATOR$oldPackageName" insertNewPackageName(packageDirectiveNode, newPackageName) } } // all words should contain only ASCII letters or digits wordsInPackageName .filter { word -> !areCorrectSymbolsUsed(word.text) } .forEach { PACKAGE_NAME_INCORRECT_SYMBOLS.warn(configRules, emitWarn, it.text, it.startOffset, it) } // all words should contain only ASCII letters or digits wordsInPackageName.forEach { correctPackageWordSeparatorsUsed(it) } } /** * only letters, digits and underscore are allowed */ private fun areCorrectSymbolsUsed(word: String): Boolean { // underscores are allowed in some cases - see "exceptionForUnderscore" val wordFromPackage = word.replace("_", "") return wordFromPackage.isASCIILettersAndDigits() } /** * in package name no other separators except dot should be used, package words (parts) should be concatenated * without any symbols or should use dot symbol - this is the only way */ private fun correctPackageWordSeparatorsUsed(word: ASTNode) { if (word.text.contains("_") && !isExceptionForUnderscore(word.text)) { INCORRECT_PACKAGE_SEPARATOR.warnAndFix(configRules, emitWarn, isFixMode, word.text, word.startOffset, word) { (word as LeafPsiElement).rawReplaceWithText(word.text.replace("_", "")) } } } /** Underscores! In some cases, if the package name starts with a number or other characters, * but these characters cannot be used at the beginning of the Java/Kotlin package name, * or the package name contains reserved Java keywords, underscores are allowed. * For example: org.example.hyphenated_name,int_.example, com.example._123name */ private fun isExceptionForUnderscore(word: String): Boolean { val wordFromPackage = word.replace("_", "") return wordFromPackage[0].isDigit() || wordFromPackage.isKotlinKeyWord() || wordFromPackage.isJavaKeyWord() } /** * function simply checks that package name starts with a proper domain name */ private fun isDomainMatches(packageNameParts: List): Boolean { val packageNamePrefix = domainName.split(PACKAGE_SEPARATOR) if (packageNameParts.size < packageNamePrefix.size) { return false } for (i in packageNamePrefix.indices) { if (packageNameParts[i].text != packageNamePrefix[i]) { return false } } return true } @Suppress("UnsafeCallOnNullableType") private fun insertNewPackageName(packageDirectiveNode: ASTNode, packageName: String) { // package name can be dot qualified expression or a reference expression in case it contains only one word val packageNameNode = packageDirectiveNode.findChildByType(DOT_QUALIFIED_EXPRESSION) ?: packageDirectiveNode.findChildByType(REFERENCE_EXPRESSION) val generatedPackageDirective = KotlinParser() .createNode("$PACKAGE_KEYWORD $packageName", true) packageNameNode?.let { // simply replacing only node connected with the package name, all other nodes remain unchanged packageDirectiveNode.replaceChild(packageNameNode, generatedPackageDirective.findLeafWithSpecificType(DOT_QUALIFIED_EXPRESSION)!!) } ?: run { // there is missing package statement in a file, so it will be created and inserted val newPackageDirective = generatedPackageDirective.findLeafWithSpecificType(PACKAGE_DIRECTIVE)!! val packageDirectiveParent = packageDirectiveNode.treeParent // When package directive is missing in .kt file, // the node is still present in the AST, and not always in a convenient place. // E.g. `@file:Suppress("...") // comments` // AST will be: FILE_ANNOTATION_LIST, PACKAGE_DIRECTIVE, WHITE_SPACE, EOL_COMMENT // So, we can't just put new package directive in it's old place and rely on FileStructure rule if (packageDirectiveNode != packageDirectiveParent.firstChildNode) { // We will insert new package directive node before first node, which is not in the following list val possibleTypesBeforePackageDirective = listOf(WHITE_SPACE, EOL_COMMENT, BLOCK_COMMENT, KDOC, PACKAGE_DIRECTIVE, FILE_ANNOTATION_LIST) val addBefore = packageDirectiveParent.children().first { it.elementType !in possibleTypesBeforePackageDirective } packageDirectiveParent.removeChild(packageDirectiveNode) packageDirectiveParent.addChild(newPackageDirective, addBefore) if (newPackageDirective.treePrev.elementType != WHITE_SPACE) { packageDirectiveParent.addChild(PsiWhiteSpaceImpl("\n"), newPackageDirective) } } else { packageDirectiveParent.replaceChild(packageDirectiveNode, newPackageDirective) } addWhiteSpaceIfRequired(newPackageDirective, packageDirectiveParent) } } private fun addWhiteSpaceIfRequired(packageNode: ASTNode, packageParentNode: ASTNode) { if (packageNode.treeNext.isWhiteSpace()) { return } if (!packageNode.treeNext.isEmptyImportList()) { packageParentNode.addChild(ASTFactory.whitespace("\n"), packageNode.treeNext) } else { // IMPORT_LIST without imports is after PACKAGE_NODE // WHITE_SPACE needs to be after IMPORT_LIST only packageParentNode.addChild(ASTFactory.whitespace("\n"), packageNode.treeNext.treeNext) } } /** * checking and fixing package directive if it does not match with the directory where the file is stored */ private fun checkFilePathMatchesWithPackageName(packageNameParts: List, realNameParts: List, packageDirective: ASTNode ) { if (realNameParts.isNotEmpty() && packageNameParts.map { node -> node.text } != realNameParts) { val realPackageNameStr = realNameParts.joinToString(PACKAGE_SEPARATOR) val offset = packageNameParts[0].startOffset PACKAGE_NAME_INCORRECT_PATH.warnAndFix(configRules, emitWarn, isFixMode, realPackageNameStr, offset, packageNameParts[0]) { insertNewPackageName(packageDirective, realPackageNameStr) } } } companion object { private val log = KotlinLogging.logger {} const val NAME_ID = "package-naming" /** * Directory which is considered the start of sources file tree */ const val PACKAGE_PATH_ANCHOR = SRC_DIRECTORY_NAME /** * Symbol that is used to separate parts in package name */ const val PACKAGE_SEPARATOR = "." /** * tricky hack (counter) that helps not to raise multiple warnings about the package name if config is missing */ var visitorCounter = AtomicInteger(0) /** * Targets described in [KMM documentation](https://kotlinlang.org/docs/reference/mpp-supported-platforms.html) */ private val kmmTargets = listOf("common", "jvm", "js", "android", "ios", "androidNativeArm32", "androidNativeArm64", "iosArm32", "iosArm64", "iosX64", "watchosArm32", "watchosArm64", "watchosX86", "tvosArm64", "tvosX64", "macosX64", "linuxArm64", "linuxArm32Hfp", "linuxMips32", "linuxMipsel32", "linuxX64", "mingwX64", "mingwX86", "wasm32", "macosArm64") /** * Directories that are supposed to be first in sources file paths, relative to [PACKAGE_PATH_ANCHOR]. * For kotlin multiplatform projects directories for targets from [kmmTargets] are supported. */ val languageDirNames = listOf("src", "java", "kotlin") + kmmTargets.flatMap { listOf("${it}Main", "${it}Test") } private fun ASTNode.isEmptyImportList() = elementType == IMPORT_LIST && children().none() } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter2/comments/CommentsRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter2.comments import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.COMMENTED_OUT_CODE import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.findAllDescendantsWithSpecificType import com.saveourtool.diktat.ruleset.utils.getFilePath import com.saveourtool.diktat.ruleset.utils.prevSibling import io.github.oshai.kotlinlogging.KotlinLogging import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.TokenType import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.lexer.KtTokens.BLOCK_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.EOL_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.WHITE_SPACE import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.psi.KtPsiFactory import org.jetbrains.kotlin.psi.stubs.elements.KtFileElementType import org.jetbrains.kotlin.resolve.ImportPath private typealias ListOfPairs = MutableList> /** * This rule performs checks if there is any commented code. * No commented out code is allowed, including imports. */ @Suppress("ForbiddenComment") class CommentsRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(COMMENTED_OUT_CODE) ) { private lateinit var ktPsiFactory: KtPsiFactory override fun logic(node: ASTNode) { ktPsiFactory = KtPsiFactory(node.psi.project, false) // regarding markGenerated see KDoc in Kotlin sources if (node.elementType == KtFileElementType.INSTANCE) { checkCommentedCode(node) } } /** * This method tries to detect commented code by uncommenting and parsing it. If parser reports errors, * we assume this is not code (it is hard to try and parse further because comments can contain code snippets). * * @implNote * 1. Import and package directives should be separated from the rest of uncommented lines * 2. Import usually go on top of the file, so if comment contains not only imports, it probably contains imports only on top. (this possibly needs fixme) * 3. Code can be surrounded by actual comments. One possible heuristic here is to assume actual comments start * with '// ' with whitespace, while automatic commenting in, e.g., IDEA creates slashes in the beginning of the line * */ @Suppress( "UnsafeCallOnNullableType", "TOO_LONG_FUNCTION", "AVOID_NULL_CHECKS" ) private fun checkCommentedCode(node: ASTNode) { val errorNodesWithText: ListOfPairs = mutableListOf() val eolCommentsOffsetToText = getOffsetsToTextBlocksFromEolComments(node, errorNodesWithText) val blockCommentsOffsetToText = node .findAllDescendantsWithSpecificType(BLOCK_COMMENT) .map { errorNodesWithText.add(it to it.text.trim().removeSurrounding("/*", "*/")) it.startOffset to it.text.trim().removeSurrounding("/*", "*/") } (eolCommentsOffsetToText + blockCommentsOffsetToText) .flatMap { (offset, text) -> val (singleLines, blockLines) = text.lines().partition { it.contains(importOrPackage) } val block = if (blockLines.isNotEmpty()) listOf(blockLines.joinToString("\n")) else emptyList() (singleLines + block).map { offset to it } } .map { (offset, text) -> offset to text.trim() } .mapNotNull { (offset, text) -> when { text.isPossibleImport() -> offset to ktPsiFactory.createImportDirective(ImportPath.fromString(text.substringAfter(importKeywordWithSpace, ""))).node text.trimStart().startsWith(packageKeywordWithSpace) -> offset to ktPsiFactory.createPackageDirective(FqName(text.substringAfter(packageKeywordWithSpace, ""))).node else -> if (isContainingRequiredPartOfCode(text)) { offset to ktPsiFactory.createBlockCodeFragment(text, null).node } else { null } } } .filter { (_, parsedNode) -> parsedNode .findAllDescendantsWithSpecificType(TokenType.ERROR_ELEMENT) .isEmpty() } .forEach { (offset, parsedNode) -> val invalidNode = errorNodesWithText.find { it.second.trim().contains(parsedNode.text, false) || parsedNode.text.contains(it.second.trim(), false) }?.first if (invalidNode == null) { logger.warn { "Text [${parsedNode.text}] is a piece of code, created from comment; " + "but no matching text in comments has been found in the file ${node.getFilePath()}" } } else { COMMENTED_OUT_CODE.warn( configRules, emitWarn, parsedNode.text.substringBefore("\n").trim(), offset, invalidNode ) } } } /** * This method is used to extract text from EOL comments in a form which can be used for parsing. * Multiple consecutive EOL comments can correspond to one code block, so we try to glue them together here. * Splitting back into lines, if necessary, will be done outside of this method, for both text from EOL and block. * fixme: in this case offset is lost for lines which will be split once more */ private fun getOffsetsToTextBlocksFromEolComments(node: ASTNode, errorNodesWithText: ListOfPairs): List> { val comments = node .findAllDescendantsWithSpecificType(EOL_COMMENT) .filter { !it.text.contains(eolCommentStart) || isCodeAfterCommentStart(it.text) } return if (comments.isNotEmpty()) { val result = mutableListOf(mutableListOf(comments.first())) comments .drop(1) .fold(result) { acc, astNode -> val isImportOrPackage = astNode.text.contains(importOrPackage) val previousNonWhiteSpaceNode = astNode.prevSibling { it.elementType != WHITE_SPACE } if (!isImportOrPackage && previousNonWhiteSpaceNode in acc.last()) { acc.last().add(astNode) } else { acc.add(mutableListOf(astNode)) } acc } .map { list -> list.forEach { errorNodesWithText.add(it to it.text.removePrefix("//")) } list.first().startOffset to list.joinToString("\n") { it.text.removePrefix("//") } } } else { emptyList() } } /** * This is a very rare case. We should check this cases for 4 things: * * 1. If it is a class/object at the beginning of the line * 2. If it is a function * 3. If it is import/package implementation * 4. If it is }. This case is used when } goes after one space and it is closing class or fun */ private fun isCodeAfterCommentStart(text: String): Boolean { val textWithoutCommentStartToken = text.removePrefix("//").trim() return codeFileStartCases.any { textWithoutCommentStartToken.contains(it) } } private fun isContainingRequiredPartOfCode(text: String): Boolean = text.contains("val ", true) || text.contains("var ", true) || text.contains("=", true) || (text.contains("{", true) && text.substringAfter("{").contains("}", true)) /** * Some weak checks to see if this string can be used as a part of import statement. * Only string surrounded in backticks or a dot-qualified expression (i.e., containing words maybe separated by dots) * are considered for this case. */ private fun String.isPossibleImport(): Boolean = trimStart().startsWith(importKeywordWithSpace) && substringAfter(importKeywordWithSpace, "").run { startsWith('`') && endsWith('`') || !contains(' ') } @Suppress("MaxLineLength") companion object { const val NAME_ID = "comments" private val logger = KotlinLogging.logger {} private val importKeywordWithSpace = "${KtTokens.IMPORT_KEYWORD.value} " private val packageKeywordWithSpace = "${KtTokens.PACKAGE_KEYWORD.value} " private val importOrPackage = """($importKeywordWithSpace|$packageKeywordWithSpace)""".toRegex() private val classRegex = """^\s*(public|private|protected)*\s*(internal)*\s*(open|data|sealed)*\s*(internal)*\s*(class|object)\s+(\w+)(\(.*\))*(\s*:\s*\w+(\(.*\))*)?\s*\{*$""".toRegex() private val importOrPackageRegex = """^(import|package)?\s+([a-zA-Z.])+;*$""".toRegex() private val functionRegex = """^(public|private|protected)*\s*(override|abstract|actual|expect)*\s?fun\s+\w+(\(.*\))?(\s*:\s*\w+)?\s*[{=]${'$'}""".toRegex() private val rightBraceRegex = """^\s*}$""".toRegex() private val valOrVarRegex = """val |var """.toRegex() private val codeFileStartCases = listOf(classRegex, importOrPackageRegex, functionRegex, rightBraceRegex, valOrVarRegex) private val eolCommentStart = """// \S""".toRegex() } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter2/comments/HeaderCommentRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter2.comments import com.saveourtool.diktat.common.config.rules.RuleConfiguration import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.common.config.rules.getRuleConfig import com.saveourtool.diktat.ruleset.constants.Warnings.HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE import com.saveourtool.diktat.ruleset.constants.Warnings.HEADER_MISSING_OR_WRONG_COPYRIGHT import com.saveourtool.diktat.ruleset.constants.Warnings.HEADER_NOT_BEFORE_PACKAGE import com.saveourtool.diktat.ruleset.constants.Warnings.HEADER_WRONG_FORMAT import com.saveourtool.diktat.ruleset.constants.Warnings.WRONG_COPYRIGHT_YEAR import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.copyrightWords import com.saveourtool.diktat.ruleset.utils.findChildAfter import com.saveourtool.diktat.ruleset.utils.findChildBefore import com.saveourtool.diktat.ruleset.utils.getAllChildrenWithType import com.saveourtool.diktat.ruleset.utils.getFilePath import com.saveourtool.diktat.ruleset.utils.getFirstChildWithType import com.saveourtool.diktat.ruleset.utils.isGradleScript import com.saveourtool.diktat.ruleset.utils.isWhiteSpace import com.saveourtool.diktat.ruleset.utils.moveChildBefore import io.github.oshai.kotlinlogging.KotlinLogging import org.jetbrains.kotlin.KtNodeTypes import org.jetbrains.kotlin.KtNodeTypes.IMPORT_LIST import org.jetbrains.kotlin.KtNodeTypes.PACKAGE_DIRECTIVE import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafElement import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl import org.jetbrains.kotlin.kdoc.lexer.KDocTokens.KDOC import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.lexer.KtTokens.BLOCK_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.WHITE_SPACE import org.jetbrains.kotlin.psi.stubs.elements.KtFileElementType import java.time.LocalDate /** * Visitor for header comment in .kt file: * 1) Ensure header comment is at the very top and properly formatted (has newline after KDoc end) * 2) Ensure copyright exists and is properly formatted * 3) Ensure there are no dates or authors * 4) Ensure files with many or zero classes have proper description */ @Suppress("ForbiddenComment") class HeaderCommentRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE, HEADER_MISSING_OR_WRONG_COPYRIGHT, HEADER_NOT_BEFORE_PACKAGE, HEADER_NOT_BEFORE_PACKAGE, HEADER_WRONG_FORMAT, WRONG_COPYRIGHT_YEAR), ) { override fun logic(node: ASTNode) { if (node.elementType == KtFileElementType.INSTANCE && !node.getFilePath().isGradleScript()) { checkCopyright(node) if (checkHeaderKdocPosition(node)) { checkHeaderKdoc(node) } } } private fun checkHeaderKdoc(node: ASTNode) { node.findChildBefore(PACKAGE_DIRECTIVE, KDOC)?.let { headerKdoc -> if (headerKdoc.treeNext != null && headerKdoc.treeNext.elementType == WHITE_SPACE && headerKdoc.treeNext.text.count { it == '\n' } != 2) { HEADER_WRONG_FORMAT.warnAndFix(configRules, emitWarn, isFixMode, "header KDoc should have a new line after", headerKdoc.startOffset, headerKdoc) { node.replaceChild(headerKdoc.treeNext, PsiWhiteSpaceImpl("\n\n")) } } } ?: run { val numDeclaredClassesAndObjects = node.getAllChildrenWithType(KtNodeTypes.CLASS).size + node.getAllChildrenWithType(KtNodeTypes.OBJECT_DECLARATION).size if (numDeclaredClassesAndObjects != 1) { HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE.warn(configRules, emitWarn, "there are $numDeclaredClassesAndObjects declared classes and/or objects", node.startOffset, node) } } } /** * If corresponding rule is enabled, checks if header KDoc is positioned correctly and moves it in fix mode. * Algorithm is as follows: if there is no KDoc at the top of file (before package directive) and the one after imports * isn't bound to any identifier, than this KDoc is misplaced header KDoc. * * @return true if position check is not needed or if header KDoc is positioned correctly or it was moved by fix mode */ @Suppress("FUNCTION_BOOLEAN_PREFIX") private fun checkHeaderKdocPosition(node: ASTNode): Boolean { val firstKdoc = node.findChildAfter(IMPORT_LIST, KDOC) // if `firstKdoc.treeParent` is File then it's a KDoc not bound to any other structures if (node.findChildBefore(PACKAGE_DIRECTIVE, KDOC) == null && firstKdoc != null && firstKdoc.treeParent.elementType == KtFileElementType.INSTANCE) { HEADER_NOT_BEFORE_PACKAGE.warnAndFix(configRules, emitWarn, isFixMode, "header KDoc is located after package or imports", firstKdoc.startOffset, firstKdoc) { node.moveChildBefore(firstKdoc, node.getFirstChildWithType(PACKAGE_DIRECTIVE), true) // ensure there is no empty line between copyright and header kdoc node.findChildBefore(PACKAGE_DIRECTIVE, BLOCK_COMMENT)?.apply { if (treeNext.elementType == WHITE_SPACE) { node.replaceChild(treeNext, PsiWhiteSpaceImpl("\n")) } else { node.addChild(PsiWhiteSpaceImpl("\n"), this.treeNext) } } } if (!isFixMode) { return false } } return true } private fun makeCopyrightCorrectYear(copyrightText: String): String { val hyphenYear = hyphenRegex.find(copyrightText) hyphenYear?.let { val copyrightYears = hyphenYear.value.split("-") if (copyrightYears[1].toInt() != curYear) { val validYears = "${copyrightYears[0]}-$curYear" return copyrightText.replace(hyphenRegex, validYears) } } val afterCopyrightYear = afterCopyrightRegex.find(copyrightText) val copyrightYears = afterCopyrightYear?.value?.split("(c)", "(C)", "©") return if (copyrightYears != null && copyrightYears[1].trim().toInt() != curYear) { val validYears = "${copyrightYears[0]}-$curYear" copyrightText.replace(afterCopyrightRegex, validYears) } else { "" } } @Suppress("TOO_LONG_FUNCTION", "ComplexMethod") private fun checkCopyright(node: ASTNode) { val configuration = CopyrightConfiguration(configRules.getRuleConfig(HEADER_MISSING_OR_WRONG_COPYRIGHT)?.configuration ?: emptyMap()) if (!configuration.isCopyrightMandatory() && !configuration.hasCopyrightText()) { return } // need to make sure that copyright year is consistent with current year val copyrightText = configuration.getCopyrightText() val copyrightWithCorrectYear = makeCopyrightCorrectYear(copyrightText) if (copyrightWithCorrectYear.isNotEmpty()) { log.warn { "Copyright year in your configuration file is not up to date." } } val headerComment = node.findChildBefore(PACKAGE_DIRECTIVE, BLOCK_COMMENT) // Depends only on content and doesn't consider years val isCopyrightMatchesPatternExceptFirstYear = isCopyRightTextMatchesPattern(headerComment, copyrightText) || isCopyRightTextMatchesPattern(headerComment, copyrightWithCorrectYear) val isWrongCopyright = headerComment != null && !isCopyrightMatchesPatternExceptFirstYear && !isHeaderCommentContainText(headerComment, copyrightText) && !isHeaderCommentContainText(headerComment, copyrightWithCorrectYear) val isMissingCopyright = headerComment == null && configuration.isCopyrightMandatory() val isCopyrightInsideKdoc = (node.getAllChildrenWithType(KDOC) + node.getAllChildrenWithType(KtTokens.EOL_COMMENT)) .any { commentNode -> copyrightWords.any { commentNode.text.contains(it, ignoreCase = true) } } if (isWrongCopyright || isMissingCopyright || isCopyrightInsideKdoc) { val freeText = when { // If `isCopyrightInsideKdoc` then `isMissingCopyright` is true too, but warning text from `isCopyrightInsideKdoc` is preferable. isCopyrightInsideKdoc -> "copyright is placed inside KDoc, but should be inside a block comment" isWrongCopyright -> "copyright comment doesn't have correct copyright text" isMissingCopyright -> "copyright is mandatory, but is missing" else -> error("Should never get to this point") } HEADER_MISSING_OR_WRONG_COPYRIGHT.warnAndFix(configRules, emitWarn, isFixMode, freeText, node.startOffset, node) { headerComment?.let { copyrightNode -> // remove node clearly, with trailing whitespace if (copyrightNode.treeNext.isWhiteSpace()) { node.removeChild(copyrightNode.treeNext) } node.removeChild(copyrightNode) } // do not insert empty line before header kdoc val newLines = node.findChildBefore(PACKAGE_DIRECTIVE, KDOC)?.let { "\n" } ?: "\n\n" node.addChild(PsiWhiteSpaceImpl(newLines), node.firstChildNode) node.addChild(LeafPsiElement(BLOCK_COMMENT, """ |/* |${handleMultilineCopyright(copyrightWithCorrectYear.ifEmpty { copyrightText })} |*/ """.trimMargin()), node.firstChildNode ) } } // Triggers when there is a copyright, but its year is not updated. if (!isMissingCopyright && !isWrongCopyright && copyrightWithCorrectYear.isNotEmpty()) { WRONG_COPYRIGHT_YEAR.warnAndFix(configRules, emitWarn, isFixMode, "year should be $curYear", node.startOffset, node) { (headerComment as LeafElement).rawReplaceWithText(headerComment.text.replace(copyrightText, copyrightWithCorrectYear)) } } } private fun isHeaderCommentContainText(headerComment: ASTNode, text: String): Boolean = if (text.isNotEmpty()) headerComment.text.flatten().contains(text.flatten()) else false // Check if provided copyright node differs only in the first date from pattern private fun isCopyRightTextMatchesPattern(copyrightNode: ASTNode?, copyrightPattern: String): Boolean { val copyrightText = copyrightNode?.text ?.replace("/*", "") ?.replace("*/", "") ?.replace("*", "") val datesInPattern = hyphenRegex.find(copyrightPattern)?.value val datesInCode = copyrightText?.let { hyphenRegex.find(it)?.value } if (datesInPattern == null || datesInCode == null) { return false } val patternWithoutDates = copyrightPattern.replace(datesInPattern, "").flatten() val textWithoutDates = copyrightText.replace(datesInCode, "").flatten() // Text should be equal, first date could be different, second date should be equal to current year return (patternWithoutDates == textWithoutDates) && (datesInCode.substringAfter("-") == curYear.toString()) } /** * Deletes all spaces and newlines * Used to compare copyrights in yaml and file */ private fun String.flatten(): String = replace("\n", "") .replace(" ", "") /** * If it is multiline copyright, this method deletes spaces in empty lines. * Otherwise, if it is one line copyright, it returns it with 4 spaces at the beginning. */ private fun handleMultilineCopyright(copyrightText: String): String { if (copyrightText.startsWith(" ")) { return copyrightText .lines() .dropWhile { it.isBlank() } .reduce { acc, nextLine -> when { nextLine.isBlank() -> "$acc\n" else -> "$acc\n$nextLine" } } } return " $copyrightText" } /** * Configuration for copyright */ class CopyrightConfiguration(config: Map) : RuleConfiguration(config) { /** * @return Whether the copyright is mandatory in all files */ fun isCopyrightMandatory() = config["isCopyrightMandatory"]?.toBoolean() ?: false /** * Whether copyright text is present in the configuration * * @return true if config has "copyrightText" */ internal fun hasCopyrightText() = config.keys.contains("copyrightText") /** * @return text of copyright as configured in the configuration file */ fun getCopyrightText() = config["copyrightText"]?.replace(CURR_YEAR_PATTERN, curYear.toString()) ?: error("Copyright is not set in configuration") } companion object { private val log = KotlinLogging.logger {} const val CURR_YEAR_PATTERN = ";@currYear;" const val NAME_ID = "header-comment" val hyphenRegex = Regex("""\d+-\d+""") val afterCopyrightRegex = Regex("""((©|\([cC]\))+ *\d+)""") val curYear = LocalDate.now().year } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter2/kdoc/CommentsFormatting.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter2.kdoc import com.saveourtool.diktat.common.config.rules.RuleConfiguration import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.common.config.rules.getRuleConfig import com.saveourtool.diktat.ruleset.constants.Warnings.COMMENT_WHITE_SPACE import com.saveourtool.diktat.ruleset.constants.Warnings.FIRST_COMMENT_NO_BLANK_LINE import com.saveourtool.diktat.ruleset.constants.Warnings.IF_ELSE_COMMENTS import com.saveourtool.diktat.ruleset.constants.Warnings.WRONG_NEWLINES_AROUND_KDOC import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.commentType import com.saveourtool.diktat.ruleset.utils.findAllDescendantsWithSpecificType import com.saveourtool.diktat.ruleset.utils.findChildrenMatching import com.saveourtool.diktat.ruleset.utils.getAllChildrenWithType import com.saveourtool.diktat.ruleset.utils.getFirstChildWithType import com.saveourtool.diktat.ruleset.utils.hasChildOfType import com.saveourtool.diktat.ruleset.utils.isWhiteSpace import com.saveourtool.diktat.ruleset.utils.isWhiteSpaceWithNewline import com.saveourtool.diktat.ruleset.utils.leaveOnlyOneNewLine import com.saveourtool.diktat.ruleset.utils.numNewLines import com.saveourtool.diktat.ruleset.utils.prevNodeUntilNode import com.saveourtool.diktat.ruleset.utils.prevSibling import org.jetbrains.kotlin.KtNodeTypes.BLOCK import org.jetbrains.kotlin.KtNodeTypes.CLASS import org.jetbrains.kotlin.KtNodeTypes.CLASS_BODY import org.jetbrains.kotlin.KtNodeTypes.ELSE import org.jetbrains.kotlin.KtNodeTypes.FUN import org.jetbrains.kotlin.KtNodeTypes.IF import org.jetbrains.kotlin.KtNodeTypes.PROPERTY import org.jetbrains.kotlin.KtNodeTypes.THEN import org.jetbrains.kotlin.KtNodeTypes.VALUE_ARGUMENT_LIST import org.jetbrains.kotlin.com.intellij.lang.ASTFactory import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl import org.jetbrains.kotlin.kdoc.lexer.KDocTokens import org.jetbrains.kotlin.kdoc.lexer.KDocTokens.KDOC import org.jetbrains.kotlin.kdoc.parser.KDocElementTypes import org.jetbrains.kotlin.lexer.KtTokens.BLOCK_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.ELSE_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.EOL_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.LBRACE import org.jetbrains.kotlin.lexer.KtTokens.WHITE_SPACE import org.jetbrains.kotlin.psi.stubs.elements.KtFileElementType /** * This class handles rule 2.6 * Part 1: * * there must be 1 space between the comment character and the content of the comment; * * there must be a newline between a Kdoc and the previous code above and no blank lines after. * * No need to add a blank line before a first comment in this particular name space (code block), for example between function declaration * and first comment in a function body. * * Part 2: * * Leave one single space between the comment on the right side of the code and the code. * * Comments in if else should be inside code blocks. Exception: General if comment */ class CommentsFormatting(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(COMMENT_WHITE_SPACE, FIRST_COMMENT_NO_BLANK_LINE, IF_ELSE_COMMENTS, WRONG_NEWLINES_AROUND_KDOC), ) { /** * @param node */ override fun logic(node: ASTNode) { val configuration = CommentsFormattingConfiguration( configRules.getRuleConfig(COMMENT_WHITE_SPACE)?.configuration ?: emptyMap()) when (node.elementType) { CLASS, FUN, PROPERTY -> checkBlankLineAfterKdoc(node) IF -> handleIfElse(node) EOL_COMMENT, BLOCK_COMMENT -> handleEolAndBlockComments(node, configuration) KDOC -> handleKdocComments(node, configuration) else -> { // this is a generated else block } } } @Suppress("PARAMETER_NAME_IN_OUTER_LAMBDA") private fun checkBlankLineAfterKdoc(node: ASTNode) { commentType.forEach { val kdoc = node.getFirstChildWithType(it) kdoc?.treeNext?.let { nodeAfterKdoc -> if (nodeAfterKdoc.elementType == WHITE_SPACE && nodeAfterKdoc.numNewLines() > 1) { WRONG_NEWLINES_AROUND_KDOC.warnAndFix(configRules, emitWarn, isFixMode, "redundant blank line after ${kdoc.text}", nodeAfterKdoc.startOffset, nodeAfterKdoc) { nodeAfterKdoc.leaveOnlyOneNewLine() } } } } } private fun handleKdocComments(node: ASTNode, configuration: CommentsFormattingConfiguration) { if (node.treeParent.treeParent != null && node.treeParent.treeParent.elementType == BLOCK) { checkCommentsInCodeBlocks(node.treeParent) // node.treeParent is a node that contains a comment. } else if (node.treeParent.elementType != IF) { checkClassComment(node) } checkWhiteSpaceBeginInComment(node, configuration) } private fun handleEolAndBlockComments(node: ASTNode, configuration: CommentsFormattingConfiguration) { basicCommentsChecks(node, configuration) checkWhiteSpaceBeginInComment(node, configuration) } private fun basicCommentsChecks(node: ASTNode, configuration: CommentsFormattingConfiguration) { checkSpaceBeforeComment(node, configuration) if (node.treeParent.elementType == BLOCK && node.treeNext != null) { // Checking if comment is inside a code block like fun{} // Not checking last comment as well if (isFirstComment(node)) { if (node.isChildOfBlockOrClassBody()) { // Just check white spaces before comment checkFirstCommentSpaces(node) } return } } else if (node.treeParent.lastChildNode != node && node.treeParent.elementType != IF && node.treeParent.firstChildNode == node && node.treeParent.elementType != VALUE_ARGUMENT_LIST) { // Else it's a class comment checkClassComment(node) } } @Suppress("UnsafeCallOnNullableType") private fun handleIfElse(node: ASTNode) { if (node.hasChildOfType(ELSE)) { val elseKeyWord = node.getFirstChildWithType(ELSE_KEYWORD)!! val elseBlock = node.getFirstChildWithType(ELSE)!! val comment = when { elseKeyWord.prevNodeUntilNode(THEN, EOL_COMMENT) != null -> elseKeyWord.prevNodeUntilNode(THEN, EOL_COMMENT) elseKeyWord.prevNodeUntilNode(THEN, BLOCK_COMMENT) != null -> elseKeyWord.prevNodeUntilNode(THEN, BLOCK_COMMENT) elseKeyWord.prevNodeUntilNode(THEN, KDOC) != null -> elseKeyWord.prevNodeUntilNode(THEN, KDOC) else -> null } comment?.let { IF_ELSE_COMMENTS.warnAndFix(configRules, emitWarn, isFixMode, comment.text, node.startOffset, node) { moveCommentToElse(node, elseBlock, elseKeyWord, comment) } } } } @Suppress("UnsafeCallOnNullableType") private fun moveCommentToElse(node: ASTNode, elseBlock: ASTNode, elseKeyWord: ASTNode, comment: ASTNode, ) { if (elseBlock.hasChildOfType(BLOCK)) { val elseCodeBlock = elseBlock.getFirstChildWithType(BLOCK)!! elseCodeBlock.addChild(comment, elseCodeBlock.firstChildNode.treeNext) elseCodeBlock.addChild(ASTFactory.whitespace("\n"), elseCodeBlock.firstChildNode.treeNext) } else { elseKeyWord.treeParent.addChild(comment, elseKeyWord.treeNext) elseKeyWord.treeParent.addChild(ASTFactory.whitespace("\n"), elseKeyWord.treeNext) } val whiteSpace = elseKeyWord.prevNodeUntilNode(THEN, WHITE_SPACE) whiteSpace?.let { node.removeChild(it) } } private fun checkCommentsInCodeBlocks(node: ASTNode) { if (isFirstComment(node)) { if (node.isChildOfBlockOrClassBody()) { // Just check white spaces before comment checkFirstCommentSpaces(node) } return } if (!node.treePrev.isWhiteSpace()) { // If node treePrev is not a whiteSpace then node treeParent is a property WRONG_NEWLINES_AROUND_KDOC.warnAndFix(configRules, emitWarn, isFixMode, node.text, node.startOffset, node) { node.treeParent.addChild(PsiWhiteSpaceImpl("\n"), node.treeParent) } } else { if (node.treePrev.numNewLines() == 1 || node.treePrev.numNewLines() > 2) { WRONG_NEWLINES_AROUND_KDOC.warnAndFix(configRules, emitWarn, isFixMode, node.text, node.startOffset, node) { (node.treePrev as LeafPsiElement).rawReplaceWithText("\n\n") } } } } private fun checkSpaceBeforeComment(node: ASTNode, configuration: CommentsFormattingConfiguration) { if (node.treeParent.firstChildNode == node) { return } if (node.treeParent.elementType == KtFileElementType.INSTANCE) { // This case is for top-level comments that are located in the beginning of the line and therefore don't need any spaces before. if (!node.treePrev.isWhiteSpaceWithNewline() && node.treePrev.text.count { it == ' ' } > 0) { COMMENT_WHITE_SPACE.warnAndFix(configRules, emitWarn, isFixMode, "There should be 0 space(s) before comment text, but are ${node.treePrev.text.count { it == ' ' }} in ${node.text}", node.startOffset, node) { if (node.treePrev.elementType == WHITE_SPACE) { (node.treePrev as LeafPsiElement).rawReplaceWithText("\n") } else { node.treeParent.addChild(PsiWhiteSpaceImpl("\n"), node) } } } } else if (!node.treePrev.isWhiteSpace()) { // if comment is like this: val a = 5// Comment COMMENT_WHITE_SPACE.warnAndFix(configRules, emitWarn, isFixMode, "There should be ${configuration.maxSpacesBeforeComment} space(s) before comment text, but are none in ${node.text}", node.startOffset, node) { node.treeParent.addChild(PsiWhiteSpaceImpl(" ".repeat(configuration.maxSpacesBeforeComment)), node) } } else if (!node.treePrev.textContains('\n') && node.treePrev.text.length != configuration.maxSpacesBeforeComment) { // if there are too many or too few spaces before comment val manyOrFew = when { node.treePrev.text.length > configuration.maxSpacesBeforeComment -> "many" else -> "few" } val message = "There should be ${configuration.maxSpacesBeforeComment} space(s) before comment text, but there are too $manyOrFew in ${node.text}" COMMENT_WHITE_SPACE.warnAndFix(configRules, emitWarn, isFixMode, message, node.startOffset, node) { (node.treePrev as LeafPsiElement).rawReplaceWithText(" ".repeat(configuration.maxSpacesBeforeComment)) } } } @Suppress("ComplexMethod", "TOO_LONG_FUNCTION") private fun checkWhiteSpaceBeginInComment(node: ASTNode, configuration: CommentsFormattingConfiguration) { if (node.elementType == EOL_COMMENT && node .text .trimStart('/') .takeWhile { it == ' ' } .length == configuration.maxSpacesInComment) { return } if (node.elementType == BLOCK_COMMENT && (node.isIndentStyleComment() || node .text .trim('/', '*') .takeWhile { it == ' ' } .length == configuration.maxSpacesInComment || node .text .trim('/', '*') .takeWhile { it == '\n' } .isNotEmpty())) { return } if (node.elementType == KDOC) { val section = node.getFirstChildWithType(KDocElementTypes.KDOC_SECTION) if (section != null && section.findChildrenMatching(KDocTokens.TEXT) { (it.treePrev != null && it.treePrev.elementType == KDocTokens.LEADING_ASTERISK) || it.treePrev == null } .all { it.text.startsWith(" ".repeat(configuration.maxSpacesInComment)) }) { // it.treePrev == null if there is no \n at the beginning of KDoc return } if (section != null && section.getAllChildrenWithType(KDocTokens.CODE_BLOCK_TEXT).isNotEmpty() && section.getAllChildrenWithType(KDocTokens.CODE_BLOCK_TEXT).all { it.text.startsWith(" ".repeat(configuration.maxSpacesInComment)) }) { return } } COMMENT_WHITE_SPACE.warnAndFix(configRules, emitWarn, isFixMode, "There should be ${configuration.maxSpacesInComment} space(s) before comment token in ${node.text}", node.startOffset, node) { val commentText = node.text.drop(2).trim() when (node.elementType) { EOL_COMMENT -> (node as LeafPsiElement).rawReplaceWithText("// $commentText") BLOCK_COMMENT -> (node as LeafPsiElement).rawReplaceWithText("/* $commentText") KDOC -> { node.findAllDescendantsWithSpecificType(KDocTokens.TEXT).forEach { modifyKdocText(it, configuration) } node.findAllDescendantsWithSpecificType(KDocTokens.CODE_BLOCK_TEXT).forEach { modifyKdocText(it, configuration) } } } } } private fun modifyKdocText(node: ASTNode, configuration: CommentsFormattingConfiguration) { if (!node.text.startsWith(" ".repeat(configuration.maxSpacesInComment))) { val commentText = node.text.trim() val indent = " ".repeat(configuration.maxSpacesInComment) (node as LeafPsiElement).rawReplaceWithText("$indent$commentText") } } private fun checkClassComment(node: ASTNode) { if (isFirstComment(node)) { if (node.isChildOfBlockOrClassBody()) { checkFirstCommentSpaces(node) } else { checkFirstCommentSpaces(node.treeParent) } } else if (node.treeParent.elementType != KtFileElementType.INSTANCE && !node.treeParent.treePrev.isWhiteSpace()) { // fixme: we might face more issues because newline node is inserted in a wrong place which causes consecutive // white spaces to be split among nodes on different levels. But this requires investigation. WRONG_NEWLINES_AROUND_KDOC.warnAndFix(configRules, emitWarn, isFixMode, node.text, node.startOffset, node) { node.treeParent.treeParent.addChild(PsiWhiteSpaceImpl("\n"), node.treeParent) } } else if (node.treeParent.elementType != KtFileElementType.INSTANCE && (node.treeParent.treePrev.numNewLines() == 1 || node.treeParent.treePrev.numNewLines() > 2)) { WRONG_NEWLINES_AROUND_KDOC.warnAndFix(configRules, emitWarn, isFixMode, node.text, node.startOffset, node) { (node.treeParent.treePrev as LeafPsiElement).rawReplaceWithText("\n\n") } } } private fun checkFirstCommentSpaces(node: ASTNode) { if (node.treePrev.isWhiteSpace() && (node.treePrev.numNewLines() > 1 || node.treePrev.numNewLines() == 0)) { FIRST_COMMENT_NO_BLANK_LINE.warnAndFix(configRules, emitWarn, isFixMode, node.text, node.startOffset, node) { (node.treePrev as LeafPsiElement).rawReplaceWithText("\n") } } } private fun isFirstComment(node: ASTNode) = if (node.isChildOfBlockOrClassBody()) { // In case when comment is inside of a function or class if (node.treePrev.isWhiteSpace()) { // in some cases (e.g. kts files) BLOCK doesn't have a leading brace node.treePrev?.treePrev?.elementType == LBRACE } else { node.treePrev == null || node.treePrev.elementType == LBRACE // null is handled for functional literal } } else if (node.treeParent?.treeParent?.elementType == KtFileElementType.INSTANCE && node.treeParent.prevSibling { it.text.isNotBlank() } == null) { // `treeParent` is the first not-empty node in a file true } else if (node.treeParent.elementType != KtFileElementType.INSTANCE && node.treeParent.treePrev != null && node.treeParent.treePrev.treePrev != null) { // When comment inside of a PROPERTY node.treeParent .treePrev .treePrev .elementType == LBRACE } else { node.treeParent.getAllChildrenWithType(node.elementType).first() == node } private fun ASTNode.isChildOfBlockOrClassBody(): Boolean = treeParent.elementType == BLOCK || treeParent.elementType == CLASS_BODY /** * Returns whether this block comment is a `indent`-style comment. * * `indent(1)` is a source code formatting utility for C-like languages. * Historically, source code formatters are permitted to reformat and reflow * the content of block comments, except for those comments which start with * "/*-". * * See also: * - [5.1.1 Block Comments](https://www.oracle.com/java/technologies/javase/codeconventions-comments.html) * - [`indent(1)`](https://man.openbsd.org/indent.1) * - [`style(9)`](https://www.freebsd.org/cgi/man.cgi?query=style&sektion=9) * * @return `true` if this block comment is a `indent`-style comment, `false` * otherwise. */ private fun ASTNode.isIndentStyleComment(): Boolean { require(elementType == BLOCK_COMMENT) { "The elementType of this node is $elementType while $BLOCK_COMMENT expected" } return text.matches(indentCommentMarker) } /** * [RuleConfiguration] for [CommentsFormatting] rule */ class CommentsFormattingConfiguration(config: Map) : RuleConfiguration(config) { /** * Number of spaces before comment start */ val maxSpacesBeforeComment = config["maxSpacesBeforeComment"]?.toIntOrNull() ?: MAX_SPACES /** * Number of spaces after comment sign (`//` or other) */ val maxSpacesInComment = config["maxSpacesInComment"]?.toIntOrNull() ?: APPROPRIATE_COMMENT_SPACES } companion object { private const val APPROPRIATE_COMMENT_SPACES = 1 private const val MAX_SPACES = 1 const val NAME_ID = "kdoc-comments-codeblocks-formatting" /** * "/*-" followed by anything but `*` or `-`. */ private val indentCommentMarker = Regex("""(?s)^\Q/*-\E[^*-].*?""") } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter2/kdoc/KdocComments.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter2.kdoc import com.saveourtool.diktat.common.config.rules.RuleConfiguration import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.common.config.rules.getCommonConfiguration import com.saveourtool.diktat.common.config.rules.getRuleConfig import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.constants.Warnings.COMMENTED_BY_KDOC import com.saveourtool.diktat.ruleset.constants.Warnings.KDOC_DUPLICATE_PROPERTY import com.saveourtool.diktat.ruleset.constants.Warnings.KDOC_EXTRA_PROPERTY import com.saveourtool.diktat.ruleset.constants.Warnings.KDOC_NO_CLASS_BODY_PROPERTIES_IN_HEADER import com.saveourtool.diktat.ruleset.constants.Warnings.KDOC_NO_CONSTRUCTOR_PROPERTY import com.saveourtool.diktat.ruleset.constants.Warnings.KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT import com.saveourtool.diktat.ruleset.constants.Warnings.MISSING_KDOC_CLASS_ELEMENTS import com.saveourtool.diktat.ruleset.constants.Warnings.MISSING_KDOC_TOP_LEVEL import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.* import org.jetbrains.kotlin.KtNodeTypes.BLOCK import org.jetbrains.kotlin.KtNodeTypes.CLASS import org.jetbrains.kotlin.KtNodeTypes.CLASS_BODY import org.jetbrains.kotlin.KtNodeTypes.FUN import org.jetbrains.kotlin.KtNodeTypes.MODIFIER_LIST import org.jetbrains.kotlin.KtNodeTypes.PRIMARY_CONSTRUCTOR import org.jetbrains.kotlin.KtNodeTypes.PROPERTY import org.jetbrains.kotlin.KtNodeTypes.TYPE_PARAMETER import org.jetbrains.kotlin.KtNodeTypes.TYPE_PARAMETER_LIST import org.jetbrains.kotlin.KtNodeTypes.TYPE_REFERENCE import org.jetbrains.kotlin.KtNodeTypes.VALUE_PARAMETER import org.jetbrains.kotlin.KtNodeTypes.VALUE_PARAMETER_LIST import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiCommentImpl import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl import org.jetbrains.kotlin.com.intellij.psi.tree.TokenSet import org.jetbrains.kotlin.kdoc.lexer.KDocTokens import org.jetbrains.kotlin.kdoc.lexer.KDocTokens.KDOC import org.jetbrains.kotlin.kdoc.parser.KDocElementTypes import org.jetbrains.kotlin.kdoc.parser.KDocKnownTag import org.jetbrains.kotlin.kdoc.psi.impl.KDocTag import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.lexer.KtTokens.BLOCK_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.EOL_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.IDENTIFIER import org.jetbrains.kotlin.lexer.KtTokens.VAL_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.VAR_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.WHITE_SPACE import org.jetbrains.kotlin.psi.psiUtil.parents import org.jetbrains.kotlin.psi.psiUtil.siblings import org.jetbrains.kotlin.psi.stubs.elements.KtFileElementType /** * This rule checks the following features in KDocs: * 1) All top-level (file level) functions and classes with public or internal access should have KDoc * 2) All internal elements in class like class, property or function should be documented with KDoc * 3) All non-private properties declared in the primary constructor are documented using `@property` tag in class KDoc */ class KdocComments(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(KDOC_EXTRA_PROPERTY, KDOC_NO_CONSTRUCTOR_PROPERTY, KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT, MISSING_KDOC_CLASS_ELEMENTS, MISSING_KDOC_TOP_LEVEL, KDOC_DUPLICATE_PROPERTY, COMMENTED_BY_KDOC ) ) { private val configuration by lazy { KdocCommentsConfiguration(configRules.getRuleConfig(KDOC_NO_CONSTRUCTOR_PROPERTY)?.configuration ?: emptyMap()) } /** * @param node */ override fun logic(node: ASTNode) { val filePath = node.getFilePath() val config = configRules.getCommonConfiguration() if (!node.hasTestAnnotation() && !isLocatedInTest(filePath.splitPathToDirs(), config.testAnchors)) { when (node.elementType) { KtFileElementType.INSTANCE -> checkTopLevelDoc(node) CLASS -> checkClassElements(node) BLOCK -> checkKdocPresent(node) else -> { // this is a generated else block } } } } private fun checkKdocPresent(node: ASTNode) { node.findChildrenMatching { it.elementType == KDOC }.forEach { warnKdocInsideBlock(it) } } private fun warnKdocInsideBlock(kdocNode: ASTNode) { COMMENTED_BY_KDOC.warnAndFix( configRules, emitWarn, isFixMode, "Redundant asterisk in block comment: \\**", kdocNode.startOffset, kdocNode ) { kdocNode.treeParent.addChild(PsiCommentImpl(BLOCK_COMMENT, kdocNode.text.replace("/**", "/*")), kdocNode) kdocNode.treeParent.removeChild(kdocNode) } } private fun checkParameterList( classNode: ASTNode, typeParameterListNode: ASTNode?, valueParameterListNode: ASTNode? ) { if (typeParameterListNode == null && valueParameterListNode == null) { return } val kdocBeforeClass = classNode.findChildByType(KDOC) ?: return val parametersInKdoc = kdocBeforeClass .kDocTags() .filter { it.knownTag == KDocKnownTag.PROPERTY || it.knownTag == KDocKnownTag.PARAM } val getParametersFromListNode = { parameterListNode: ASTNode? -> parameterListNode?.let { node -> val parameterType = if (parameterListNode.elementType == TYPE_PARAMETER_LIST) TYPE_PARAMETER else VALUE_PARAMETER node.findChildrenMatching { it.elementType == parameterType } .mapNotNull { it.findChildByType(IDENTIFIER)?.text } } ?: emptyList() } val parametersInTypeParameterList = getParametersFromListNode(typeParameterListNode) val parametersInValueParameterList = getParametersFromListNode(valueParameterListNode) parametersInKdoc .filter { it.getSubjectName() != null && it.getSubjectName() !in (parametersInTypeParameterList + parametersInValueParameterList) } .forEach { KDOC_EXTRA_PROPERTY.warn(configRules, emitWarn, it.text, it.node.startOffset, classNode) } } @Suppress("UnsafeCallOnNullableType", "ComplexMethod") private fun checkValueParameter(classNode: ASTNode, valueParameterNode: ASTNode) { if (valueParameterNode.parents().any { it.elementType == TYPE_REFERENCE }) { return } val prevComment = if (valueParameterNode.siblings(forward = false) .takeWhile { it.elementType != EOL_COMMENT && it.elementType != BLOCK_COMMENT } .all { it.elementType == WHITE_SPACE } ) { // take previous comment, if it's immediately before `valueParameterNode` or separated only with white space valueParameterNode.prevSibling { it.elementType == EOL_COMMENT || it.elementType == BLOCK_COMMENT } } else if (valueParameterNode.hasChildOfType(KDOC)) { valueParameterNode.findChildByType(KDOC)!! } else { null } val kdocBeforeClass = classNode.findChildByType(KDOC) val isParamTagNeeded = (!valueParameterNode.hasChildOfType(VAL_KEYWORD) && !valueParameterNode.hasChildOfType(VAR_KEYWORD)) || !valueParameterNode.getFirstChildWithType(MODIFIER_LIST).isAccessibleOutside() prevComment?.let { kdocBeforeClass?.let { checkKdocBeforeClass(valueParameterNode, kdocBeforeClass, prevComment, isParamTagNeeded) } ?: createKdocWithProperty(valueParameterNode, prevComment, isParamTagNeeded) } ?: kdocBeforeClass?.let { checkBasicKdocBeforeClass(valueParameterNode, kdocBeforeClass, isParamTagNeeded) } ?: createKdocBasicKdoc(valueParameterNode, isParamTagNeeded) } @Suppress("UnsafeCallOnNullableType", "ComplexMethod") private fun checkTypeParameter(classNode: ASTNode, typeParameterNode: ASTNode) { val kdocBeforeClass = classNode.findChildByType(KDOC) kdocBeforeClass?.let { checkBasicKdocBeforeClass(typeParameterNode, kdocBeforeClass, true) } ?: createKdocBasicKdoc(typeParameterNode, true) } @Suppress("UnsafeCallOnNullableType", "ComplexMethod") private fun checkBasicKdocBeforeClass( node: ASTNode, kdocBeforeClass: ASTNode, isParamTagNeeded: Boolean ) { val parameterName = getParameterName(node) val parameterTagInClassKdoc = findParameterTagInClassKdoc(kdocBeforeClass, parameterName) parameterTagInClassKdoc?.let { val correctTag = if (isParamTagNeeded) KDocKnownTag.PARAM else KDocKnownTag.PROPERTY if (parameterTagInClassKdoc.knownTag != correctTag) { val warningText = if (isParamTagNeeded) { "change `@property` tag to `@param` tag for <$parameterName> to KDoc" } else { "change `@param` tag to `@property` tag for <$parameterName> to KDoc" } KDOC_NO_CONSTRUCTOR_PROPERTY.warnAndFix(configRules, emitWarn, isFixMode, warningText, node.startOffset, node) { val isFirstTagInKdoc = parameterTagInClassKdoc.node == kdocBeforeClass.kDocTags().first().node replaceWrongTagInClassKdoc(kdocBeforeClass, parameterName, isParamTagNeeded, isFirstTagInKdoc) } } } ?: run { if (isNeedToWarn(node, isParamTagNeeded)) { val warningText = if (isParamTagNeeded) "add param <$parameterName> to KDoc" else "add property <$parameterName> to KDoc" KDOC_NO_CONSTRUCTOR_PROPERTY.warnAndFix(configRules, emitWarn, isFixMode, warningText, node.startOffset, node) { val newKdocText = if (isParamTagNeeded) "* @param $parameterName\n " else "* @property $parameterName\n " insertTextInKdoc(kdocBeforeClass, checkOneNewLineAfterKdocClassDescription(kdocBeforeClass, newKdocText, false)) } } } } private fun isNeedToWarn(node: ASTNode, isParamTagNeeded: Boolean): Boolean { val isParameter = node.elementType == VALUE_PARAMETER && !node.hasChildOfType(VAL_KEYWORD) && !node.hasChildOfType(VAR_KEYWORD) val isPrivateProperty = node.elementType == VALUE_PARAMETER && !node.getFirstChildWithType(MODIFIER_LIST).isAccessibleOutside() val isTypeParameterNode = node.elementType == TYPE_PARAMETER return !isParamTagNeeded || (isParameter && configuration.isParamTagsForParameters) || (isPrivateProperty && configuration.isParamTagsForPrivateProperties) || (isTypeParameterNode && configuration.isParamTagsForGenericTypes) } private fun replaceWrongTagInClassKdoc( kdocBeforeClass: ASTNode, parameterName: String, isParamTagNeeded: Boolean, isFirstTagInKdoc: Boolean ) { val wrongTagText = if (isParamTagNeeded) "* @property $parameterName" else "* @param $parameterName" val replaceText = if (isParamTagNeeded) "* @param $parameterName" else "* @property $parameterName" changeTagInKdoc(kdocBeforeClass, wrongTagText, checkOneNewLineAfterKdocClassDescription(kdocBeforeClass, replaceText, isFirstTagInKdoc)) } @Suppress("UnsafeCallOnNullableType") private fun changeTagInKdoc( kdocBeforeClass: ASTNode, wrongTagText: String, correctTagText: String ) { val allKdocText = kdocBeforeClass.text val newKdocText = allKdocText.replaceFirst(wrongTagText, correctTagText) kdocBeforeClass.treeParent.replaceChild(kdocBeforeClass, KotlinParser().createNode(newKdocText).findChildByType(KDOC)!!) } @Suppress("UnsafeCallOnNullableType") private fun checkOneNewLineAfterKdocClassDescription( kdocBeforeClass: ASTNode, newKdocText: String, isFirstTagInKdoc: Boolean ): String { val firstDescriptionSection = kdocBeforeClass .findChildrenMatching { it.elementType == KDocElementTypes.KDOC_SECTION } .firstOrNull() val lastTextInDescription = firstDescriptionSection ?.findChildrenMatching { it.elementType == KDocTokens.TEXT || it.elementType == KDocTokens.CODE_BLOCK_TEXT || it.elementType == KDocTokens.MARKDOWN_LINK } ?.lastOrNull { it.text.trim().isNotEmpty() } val isHasDescription = lastTextInDescription != null return newKdocText.let {text -> if (isHasDescription && (kdocBeforeClass.kDocTags().isEmpty() || isFirstTagInKdoc)) { // if we have any existing tags and current is first of them, we need to save last three nodes which are KDOC_LEADING_ASTERISK and two WHITE_SPACE around it // this is necessary so that first tag is on new line immediately after description val beforeChild = if (isFirstTagInKdoc) firstDescriptionSection!!.lastChildNode.treePrev.treePrev.treePrev else firstDescriptionSection!!.lastChildNode // remove all KDOC_LEADING_ASTERISK and WHITE_SPACE between last text in description and end of description firstDescriptionSection .findChildrenMatching { firstDescriptionSection.isChildAfterAnother(it, lastTextInDescription!!) && (firstDescriptionSection.isChildBeforeAnother(it, beforeChild) || it == beforeChild) } .forEach { firstDescriptionSection.removeChild(it) } "*\n $text" } else { text } } } private fun checkKdocBeforeClass( node: ASTNode, kdocBeforeClass: ASTNode, prevComment: ASTNode, isParamTagNeeded: Boolean ) { if (prevComment.elementType == KDOC || prevComment.elementType == BLOCK_COMMENT) { // there is a documentation before property or parameter that we can extract, and there is class KDoc, where we can move it to handleKdocAndBlock(node, kdocBeforeClass, prevComment, isParamTagNeeded) } else { handleCommentBefore(node, kdocBeforeClass, prevComment, isParamTagNeeded) } } @Suppress("UnsafeCallOnNullableType") private fun createKdocBasicKdoc(node: ASTNode, isParamTagNeeded: Boolean) { if (isNeedToWarn(node, isParamTagNeeded)) { val parameterName = getParameterName(node) val warningText = if (isParamTagNeeded) "add param <$parameterName> to KDoc" else "add property <$parameterName> to KDoc" KDOC_NO_CONSTRUCTOR_PROPERTY.warnAndFix(configRules, emitWarn, isFixMode, warningText, node.startOffset, node) { val classNode = node.parent { it.elementType == CLASS }!! val newKdocText = if (isParamTagNeeded) "/**\n * @param $parameterName\n */" else "/**\n * @property $parameterName\n */" val newKdoc = KotlinParser().createNode(newKdocText) classNode.addChild(PsiWhiteSpaceImpl("\n"), classNode.firstChildNode) classNode.addChild(newKdoc.findChildByType(KDOC)!!, classNode.firstChildNode) } } } @Suppress("UnsafeCallOnNullableType") private fun createKdocWithProperty( node: ASTNode, prevComment: ASTNode, isParamTagNeeded: Boolean ) { if (isNeedToWarn(node, isParamTagNeeded)) { val parameterName = getParameterName(node) val classNode = node.parent { it.elementType == CLASS }!! // if property or parameter is documented with KDoc, which has a tag inside, then it can contain some additional more // complicated structure, that will be hard to move automatically val isHasTagsInConstructorKdoc = prevComment.elementType == KDOC && prevComment.kDocTags().isNotEmpty() val isFixable = !isHasTagsInConstructorKdoc val warningText = if (isParamTagNeeded) "add comment for param <$parameterName> to KDoc" else "add comment for property <$parameterName> to KDoc" KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT.warnOnlyOrWarnAndFix(configRules, emitWarn, warningText, prevComment.startOffset, node, isFixable, isFixMode) { val paramOrPropertyTagText = if (isParamTagNeeded) "@param" else "@property" val newKdocText = createClassKdocTextFromComment(prevComment, parameterName, paramOrPropertyTagText) val newKdoc = KotlinParser().createNode(newKdocText).findChildByType(KDOC)!! classNode.addChild(PsiWhiteSpaceImpl("\n"), classNode.firstChildNode) classNode.addChild(newKdoc, classNode.firstChildNode) node.removeWithWhiteSpace(prevComment) } } } private fun createClassKdocTextFromComment( prevComment: ASTNode, parameterName: String, paramOrPropertyTagText: String ) = when (prevComment.elementType) { KDOC -> "/**\n * $paramOrPropertyTagText $parameterName${createClassKdocTextFromKdocComment(prevComment)}\n */" EOL_COMMENT -> "/**\n * $paramOrPropertyTagText $parameterName ${createClassKdocTextFromEolComment(prevComment)}\n */" else -> "/**\n * $paramOrPropertyTagText $parameterName${createClassKdocTextFromBlockComment(prevComment)}\n */" } private fun createClassKdocTextFromKdocComment(prevComment: ASTNode) = prevComment.text .removePrefix("/**") .removeSuffix("*/") .replace("\n( )*\\*( )*".toRegex(), "\n * ") .trimStart(' ') .trimEnd(' ', '\n') .let { if (!it.startsWith("\n")) " $it" else it } private fun createClassKdocTextFromEolComment(prevComment: ASTNode) = prevComment.text .removePrefix("//") .trimStart(' ') .trimEnd(' ', '\n') private fun createClassKdocTextFromBlockComment(prevComment: ASTNode) = prevComment.text .removePrefix("/*") .removeSuffix("*/") .replace("\n( )*\\*( )*".toRegex(), "\n * ") .trimStart(' ') .trimEnd(' ', '\n') .let { if (!it.startsWith("\n")) " $it" else it } @Suppress( "UnsafeCallOnNullableType", "TOO_LONG_FUNCTION", "ComplexMethod" ) private fun handleKdocAndBlock( node: ASTNode, kdocBeforeClass: ASTNode, prevComment: ASTNode, isParamTagNeeded: Boolean ) { val parameterName = getParameterName(node) val parameterTagInClassKdoc = findParameterTagInClassKdoc(kdocBeforeClass, parameterName) val parameterInClassKdoc = parameterTagInClassKdoc?.node val commentText = if (prevComment.elementType == KDOC) { createClassKdocTextFromKdocComment(prevComment) } else { createClassKdocTextFromBlockComment(prevComment) } // if property or parameter is documented with KDoc, which has a tag inside, then it can contain some additional more // complicated structure, that will be hard to move automatically val isHasTagsInConstructorKdoc = prevComment.elementType == KDOC && prevComment.kDocTags().isNotEmpty() val isFixable = !isHasTagsInConstructorKdoc val (isHasWrongTag, warningText) = checkWrongTagAndMakeWarningText(parameterTagInClassKdoc, parameterName, isParamTagNeeded) val isFirstTagInKdoc = parameterTagInClassKdoc?.node != null && parameterTagInClassKdoc.node == kdocBeforeClass.kDocTags().first().node parameterInClassKdoc?.let { KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT.warnOnlyOrWarnAndFix(configRules, emitWarn, warningText, prevComment.startOffset, node, isFixable, isFixMode) { // local docs should be appended to docs in class appendKdocTagContent(parameterInClassKdoc, commentText) if (isHasWrongTag) { replaceWrongTagInClassKdoc(kdocBeforeClass, parameterName, isParamTagNeeded, isFirstTagInKdoc) } node.removeWithWhiteSpace(prevComment) } } ?: run { if (isNeedToWarn(node, isParamTagNeeded)) { KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT.warnOnlyOrWarnAndFix(configRules, emitWarn, warningText, prevComment.startOffset, node, isFixable, isFixMode) { val newKdocText = if (isParamTagNeeded) "* @param $parameterName$commentText\n " else "* @property $parameterName$commentText\n " insertTextInKdoc(kdocBeforeClass, checkOneNewLineAfterKdocClassDescription(kdocBeforeClass, newKdocText, isFirstTagInKdoc)) node.removeWithWhiteSpace(prevComment) } } } } @Suppress( "UnsafeCallOnNullableType", "TOO_LONG_FUNCTION", "ComplexMethod" ) private fun handleCommentBefore( node: ASTNode, kdocBeforeClass: ASTNode, prevComment: ASTNode, isParamTagNeeded: Boolean ) { val parameterName = getParameterName(node) val parameterTagInClassKdoc = findParameterTagInClassKdoc(kdocBeforeClass, parameterName) val parameterInClassKdoc = parameterTagInClassKdoc?.node val (isHasWrongTag, warningText) = checkWrongTagAndMakeWarningText(parameterTagInClassKdoc, parameterName, isParamTagNeeded) val isFirstTagInKdoc = parameterTagInClassKdoc?.node != null && parameterTagInClassKdoc.node == kdocBeforeClass.kDocTags().first().node parameterInClassKdoc?.let { KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT.warnAndFix(configRules, emitWarn, isFixMode, warningText, prevComment.startOffset, node) { if (parameterInClassKdoc.hasChildOfType(KDocTokens.TEXT)) { val newKdocText = parameterInClassKdoc .findChildrenMatching { it.elementType == KDocTokens.TEXT || it.elementType == KDocTokens.CODE_BLOCK_TEXT } .lastOrNull() (newKdocText as LeafPsiElement).rawReplaceWithText("${newKdocText.text}\n * ${createClassKdocTextFromEolComment(prevComment)}") } else { parameterInClassKdoc.addChild(LeafPsiElement(KDocTokens.TEXT, " ${createClassKdocTextFromEolComment(prevComment)}"), null) } if (isHasWrongTag) { replaceWrongTagInClassKdoc(kdocBeforeClass, parameterName, isParamTagNeeded, isFirstTagInKdoc) } node.treeParent.removeChildMergingSurroundingWhitespaces(prevComment.treePrev, prevComment, prevComment.treeNext) } } ?: run { if (isNeedToWarn(node, isParamTagNeeded)) { KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT.warnAndFix(configRules, emitWarn, isFixMode, warningText, prevComment.startOffset, node) { val newKdocText = if (isParamTagNeeded) { "* @param $parameterName ${createClassKdocTextFromEolComment(prevComment)}\n " } else { "* @property $parameterName ${createClassKdocTextFromEolComment(prevComment)}\n " } insertTextInKdoc(kdocBeforeClass, checkOneNewLineAfterKdocClassDescription(kdocBeforeClass, newKdocText, isFirstTagInKdoc)) node.treeParent.removeChildMergingSurroundingWhitespaces(prevComment.treePrev, prevComment, prevComment.treeNext) } } } } @Suppress("UnsafeCallOnNullableType") private fun getParameterName(node: ASTNode) = node.findChildByType(IDENTIFIER)!!.text.replace("`", "") private fun findParameterTagInClassKdoc(kdocBeforeClass: ASTNode, parameterName: String) = kdocBeforeClass .kDocTags() .firstOrNull { (it.knownTag == KDocKnownTag.PARAM || it.knownTag == KDocKnownTag.PROPERTY) && it.getSubjectName() == parameterName } private fun checkWrongTagAndMakeWarningText( parameterTagInClassKdoc: KDocTag?, parameterName: String, isParamTagNeeded: Boolean ): Pair { val correctTag = if (isParamTagNeeded) KDocKnownTag.PARAM else KDocKnownTag.PROPERTY val isHasWrongTag = parameterTagInClassKdoc != null && parameterTagInClassKdoc.knownTag != correctTag val warningText = if (isHasWrongTag) { if (isParamTagNeeded) { "change `@property` tag to `@param` tag for <$parameterName> and add comment to KDoc" } else { "change `@param` tag to `@property` tag for <$parameterName> and add comment to KDoc" } } else { if (isParamTagNeeded) { "add comment for param <$parameterName> to KDoc" } else { "add comment for property <$parameterName> to KDoc" } } return Pair(isHasWrongTag, warningText) } private fun checkDuplicateProperties(kdoc: ASTNode) { val propertiesAndParams = kdoc .kDocTags() .filter { it.knownTag == KDocKnownTag.PROPERTY || it.knownTag == KDocKnownTag.PARAM } val traversedNodes: MutableSet = mutableSetOf() propertiesAndParams.forEach { parameter -> if (!traversedNodes.add(parameter.getSubjectName())) { KDOC_DUPLICATE_PROPERTY.warn(configRules, emitWarn, parameter.text, parameter.node.startOffset, kdoc) } } } @Suppress("UnsafeCallOnNullableType") private fun insertTextInKdoc(kdocBeforeClass: ASTNode, insertText: String) { val allKdocText = kdocBeforeClass.text val endKdoc = kdocBeforeClass.findChildByType(KDocTokens.END)!!.text val newKdocText = StringBuilder(allKdocText).insert(allKdocText.indexOf(endKdoc), insertText).toString() kdocBeforeClass.treeParent.replaceChild(kdocBeforeClass, KotlinParser().createNode(newKdocText).findChildByType(KDOC)!!) } /** * Append [content] to [kdocTagNode], e.g. * (`@property foo bar`, "baz") -> `@property foo bar baz` */ private fun appendKdocTagContent(kdocTagNode: ASTNode, content: String) { kdocTagNode.findChildrenMatching { it.elementType == KDocTokens.TEXT || it.elementType == KDocTokens.CODE_BLOCK_TEXT } .lastOrNull() ?.let { kdocTagNode.replaceChild( it, LeafPsiElement(KDocTokens.TEXT, "${it.text}$content"), ) } ?: kdocTagNode.addChild(LeafPsiElement(KDocTokens.TEXT, content), null) } private fun checkClassElements(classNode: ASTNode) { val modifierList = classNode.getFirstChildWithType(MODIFIER_LIST) val typeParameterList = classNode.getFirstChildWithType(TYPE_PARAMETER_LIST) val valueParameterList = classNode.getFirstChildWithType(PRIMARY_CONSTRUCTOR)?.getFirstChildWithType(VALUE_PARAMETER_LIST) val classBody = classNode.getFirstChildWithType(CLASS_BODY) val classKdoc = classNode.getFirstChildWithType(KDOC) checkParameterList(classNode, typeParameterList, valueParameterList) typeParameterList ?.findChildrenMatching { it.elementType == TYPE_PARAMETER } ?.forEach { checkTypeParameter(classNode, it) } valueParameterList ?.findChildrenMatching { it.elementType == VALUE_PARAMETER } ?.forEach { checkValueParameter(classNode, it) } // if parent class is public or internal than we can check it's internal code elements if (classBody != null && modifierList.isAccessibleOutside()) { classBody .getChildren(statementsToDocument) .filterNot { (it.elementType == FUN && it.isStandardMethod()) || (it.elementType == FUN && it.isOverridden()) || (it.elementType == PROPERTY && it.isOverridden()) } .forEach { classElement -> if (classElement.elementType == PROPERTY) { // we check if property declared in class body is also documented in class header via // `@property` tag val propertyInClassKdoc = classKdoc?.kDocTags()?.find { it.knownTag == KDocKnownTag.PROPERTY && it.getSubjectName() == classElement.getIdentifierName()?.text } propertyInClassKdoc?.let { // if property is documented as `@property`, then we suggest to move docs to the // declaration inside the class body KDOC_NO_CLASS_BODY_PROPERTIES_IN_HEADER.warn(configRules, emitWarn, classElement.text, classElement.startOffset, classElement) return } } // for everything else, we raise a missing kdoc warning checkDoc(classElement, MISSING_KDOC_CLASS_ELEMENTS) } } } private fun checkTopLevelDoc(node: ASTNode) { // checking that all top level class declarations and functions have kDoc return (node.getAllChildrenWithType(CLASS) + node.getAllChildrenWithType(FUN)) .forEach { checkDoc(it, MISSING_KDOC_TOP_LEVEL) } } /** * raises warning if protected, public or internal code element does not have a Kdoc */ @Suppress("UnsafeCallOnNullableType") private fun checkDoc(node: ASTNode, warning: Warnings) { // if there is an annotation before function, AST is a bit more complex, so we need to look for Kdoc among // children of modifier list val kdoc = node.getFirstChildWithType(KDOC) ?: node.getFirstChildWithType(MODIFIER_LIST)?.getFirstChildWithType(KDOC) kdoc?.let { checkDuplicateProperties(kdoc) } val name = node.getIdentifierName() val isModifierAccessibleOutsideOrActual = node.getFirstChildWithType(MODIFIER_LIST).run { isAccessibleOutside() && this?.hasChildOfType(KtTokens.ACTUAL_KEYWORD) != true } if (isModifierAccessibleOutsideOrActual && kdoc == null && !isTopLevelFunctionStandard(node)) { warning.warn(configRules, emitWarn, name!!.text, node.startOffset, node) } } private fun isTopLevelFunctionStandard(node: ASTNode): Boolean = node.elementType == FUN && node.isStandardMethod() /** * [RuleConfiguration] for param tags creation */ private class KdocCommentsConfiguration(config: Map) : RuleConfiguration(config) { /** * Create param tags for parameters without val or var */ val isParamTagsForParameters = config["isParamTagsForParameters"]?.toBoolean() ?: true /** * Create param tags for private properties */ val isParamTagsForPrivateProperties = config["isParamTagsForPrivateProperties"]?.toBoolean() ?: true /** * Create param tags for generic types */ val isParamTagsForGenericTypes = config["isParamTagsForGenericTypes"]?.toBoolean() ?: true } companion object { const val NAME_ID = "kdoc-comments" private val statementsToDocument = TokenSet.create(CLASS, FUN, PROPERTY) } } private fun ASTNode.removeWithWhiteSpace(prevComment: ASTNode) { removeChildMergingSurroundingWhitespaces( if (prevComment.elementType == KDOC) prevComment.treeParent.treePrev else prevComment.treePrev, prevComment, prevComment.treeNext ) } /** * If [child] node is surrounded by nodes with type `WHITE_SPACE`, remove [child] and merge surrounding nodes, * preserving only a single newline if present (i.e. leaving no empty lines after merging). In any case, [child] is removed * from the tree. */ private fun ASTNode.removeChildMergingSurroundingWhitespaces( prevWhiteSpaces: ASTNode, child: ASTNode, nextWhitespaces: ASTNode ) { if (nextWhitespaces.elementType == WHITE_SPACE && prevWhiteSpaces.elementType == WHITE_SPACE) { val mergedText = (prevWhiteSpaces.text + nextWhitespaces.text) val mergedSpace = if (mergedText.contains('\n')) { '\n' + mergedText.substringAfterLast('\n') } else { mergedText } child.treeParent.removeChild(prevWhiteSpaces) child.treeParent.replaceWhiteSpaceText(nextWhitespaces, mergedSpace) } removeChild(child) } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter2/kdoc/KdocFormatting.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter2.kdoc import com.saveourtool.diktat.common.config.rules.RuleConfiguration import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.common.config.rules.getRuleConfig import com.saveourtool.diktat.ruleset.constants.Warnings.KDOC_CONTAINS_DATE_OR_AUTHOR import com.saveourtool.diktat.ruleset.constants.Warnings.KDOC_EMPTY_KDOC import com.saveourtool.diktat.ruleset.constants.Warnings.KDOC_NEWLINES_BEFORE_BASIC_TAGS import com.saveourtool.diktat.ruleset.constants.Warnings.KDOC_NO_DEPRECATED_TAG import com.saveourtool.diktat.ruleset.constants.Warnings.KDOC_NO_EMPTY_TAGS import com.saveourtool.diktat.ruleset.constants.Warnings.KDOC_NO_NEWLINES_BETWEEN_BASIC_TAGS import com.saveourtool.diktat.ruleset.constants.Warnings.KDOC_NO_NEWLINE_AFTER_SPECIAL_TAGS import com.saveourtool.diktat.ruleset.constants.Warnings.KDOC_WRONG_SPACES_AFTER_TAG import com.saveourtool.diktat.ruleset.constants.Warnings.KDOC_WRONG_TAGS_ORDER import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.allSiblings import com.saveourtool.diktat.ruleset.utils.findChildAfter import com.saveourtool.diktat.ruleset.utils.findChildBefore import com.saveourtool.diktat.ruleset.utils.getAllChildrenWithType import com.saveourtool.diktat.ruleset.utils.getFirstChildWithType import com.saveourtool.diktat.ruleset.utils.getIdentifierName import com.saveourtool.diktat.ruleset.utils.hasChildMatching import com.saveourtool.diktat.ruleset.utils.hasTrailingNewlineInTagBody import com.saveourtool.diktat.ruleset.utils.isWhiteSpaceWithNewline import com.saveourtool.diktat.ruleset.utils.kDocTags import com.saveourtool.diktat.ruleset.utils.leaveOnlyOneNewLine import com.saveourtool.diktat.ruleset.utils.nextSibling import com.saveourtool.diktat.ruleset.utils.prevSibling import com.saveourtool.diktat.ruleset.utils.reversedChildren import org.jetbrains.kotlin.KtNodeTypes import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.CompositeElement import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType import org.jetbrains.kotlin.kdoc.lexer.KDocTokens import org.jetbrains.kotlin.kdoc.lexer.KDocTokens.KDOC import org.jetbrains.kotlin.kdoc.parser.KDocElementTypes import org.jetbrains.kotlin.kdoc.parser.KDocKnownTag import org.jetbrains.kotlin.kdoc.psi.impl.KDocTag import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.lexer.KtTokens.WHITE_SPACE import org.jetbrains.kotlin.psi.psiUtil.startOffset import java.time.format.DateTimeFormatter import java.time.temporal.ChronoField /** * Formatting visitor for Kdoc: * 1) removing all blank lines between Kdoc and the code it's declaring * 2) ensuring there are no tags with empty content * 3) ensuring there is only one white space between tag and it's body * 4) ensuring tags @apiNote, @implSpec, @implNote have one empty line after their body * 5) ensuring tags @param, @return, @throws are arranged in this order * 6) ensuring @author tag is not used * 7) ensuring @since tag contains only versions and not dates */ @Suppress("ForbiddenComment") class KdocFormatting(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(KDOC_CONTAINS_DATE_OR_AUTHOR, KDOC_EMPTY_KDOC, KDOC_NEWLINES_BEFORE_BASIC_TAGS, KDOC_NO_DEPRECATED_TAG, KDOC_NO_EMPTY_TAGS, KDOC_NO_NEWLINES_BETWEEN_BASIC_TAGS, KDOC_NO_NEWLINE_AFTER_SPECIAL_TAGS, KDOC_WRONG_SPACES_AFTER_TAG, KDOC_WRONG_TAGS_ORDER), ) { /** * The reasoning behind the tag ordering: * * 1. `@receiver` documents `this` instance which is the 1st function call * parameter (ordered before `@param`). * 1. `@param` followed by `@return`, then followed by `@throws` or * `@exception` is the conventional order inherited from _JavaDoc_. * 1. `@param` tags can also be used to document generic type parameters. * 1. `@property` tags are placed after `@param` tags in the official * [example](https://kotlinlang.org/docs/kotlin-doc.html#kdoc-syntax). Looking at the * [code](https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/src/kotlin/util/Tuples.kt#L22), * this is also the style _JetBrains_ use themselves. * 1. looking at the examples, `@constructor` tags are placed last in * constructor descriptions. */ private val basicTagsList = listOf( KDocKnownTag.RECEIVER, KDocKnownTag.PARAM, KDocKnownTag.PROPERTY, KDocKnownTag.RETURN, KDocKnownTag.THROWS, KDocKnownTag.EXCEPTION, KDocKnownTag.CONSTRUCTOR, ) private val specialTagNames = setOf("implSpec", "implNote", "apiNote") private var versionRegex: Regex? = null /** * @param node */ override fun logic(node: ASTNode) { versionRegex ?: run { versionRegex = KdocFormatConfiguration( configRules.getRuleConfig(KDOC_CONTAINS_DATE_OR_AUTHOR)?.configuration ?: emptyMap() ) .versionRegex } if (node.elementType == KDOC && isKdocNotEmpty(node)) { checkNoDeprecatedTag(node) checkEmptyTags(node.kDocTags()) checkSpaceAfterTag(node.kDocTags()) checkEmptyLineBeforeBasicTags(node.kDocBasicTags()) checkEmptyLinesBetweenBasicTags(node.kDocBasicTags()) checkBasicTagsOrder(node) checkNewLineAfterSpecialTags(node) checkAuthorAndDate(node) } } private fun isKdocNotEmpty(node: ASTNode): Boolean { val isKdocNotEmpty = node.getFirstChildWithType(KDocElementTypes.KDOC_SECTION) ?.hasChildMatching { it.elementType != KDocTokens.LEADING_ASTERISK && it.elementType != WHITE_SPACE } ?: false if (!isKdocNotEmpty) { KDOC_EMPTY_KDOC.warn(configRules, emitWarn, node.treeParent.getIdentifierName()?.text ?: node.nextSibling { it.elementType in KtTokens.KEYWORDS }?.text ?: node.text, node.startOffset, node) } return isKdocNotEmpty } @Suppress("UnsafeCallOnNullableType") private fun checkNoDeprecatedTag(node: ASTNode) { val kdocTags = node.kDocTags() kdocTags.find { it.name == "deprecated" } ?.let { kdocTag -> KDOC_NO_DEPRECATED_TAG.warnAndFix(configRules, emitWarn, isFixMode, kdocTag.text, kdocTag.node.startOffset, kdocTag.node) { val kdocSection = kdocTag.node.treeParent val deprecatedTagNode = kdocTag.node kdocSection.removeRange(deprecatedTagNode.prevSibling { it.elementType == WHITE_SPACE }!!, deprecatedTagNode.nextSibling { it.elementType == WHITE_SPACE } ) node.treeParent.addChild(LeafPsiElement(KtNodeTypes.ANNOTATION, "@Deprecated(message = \"${kdocTag.getContent()}\")"), node.treeNext) // copy to get all necessary indents node.treeParent.addChild(node.nextSibling { it.elementType == WHITE_SPACE }!!.clone() as PsiWhiteSpaceImpl, node.treeNext) } } } @Suppress("UnsafeCallOnNullableType") private fun checkEmptyTags(kdocTags: Collection?) { kdocTags?.filter { it.getSubjectName() == null && it.getContent().isEmpty() }?.forEach { KDOC_NO_EMPTY_TAGS.warn(configRules, emitWarn, "@${it.name!!}", it.node.startOffset, it.node) } } @Suppress("UnsafeCallOnNullableType") private fun checkSpaceAfterTag(kdocTags: Collection?) { // tags can have 'parameters' and content, either can be missing // we always can find white space after tag name, but after tag parameters only if content is present kdocTags?.filter { tag -> val hasSubject = tag.getSubjectName()?.isNotBlank() ?: false if (!hasSubject && tag.getContent().isBlank()) { return@filter false } val (isSpaceBeforeContentError, isSpaceAfterTagError) = findBeforeAndAfterSpaces(tag) hasSubject && isSpaceBeforeContentError || isSpaceAfterTagError }?.forEach { tag -> KDOC_WRONG_SPACES_AFTER_TAG.warnAndFix(configRules, emitWarn, isFixMode, "@${tag.name!!}", tag.node.startOffset, tag.node) { tag.node.findChildBefore(KDocTokens.TEXT, WHITE_SPACE) ?.let { tag.node.replaceChild(it, LeafPsiElement(WHITE_SPACE, " ")) } tag.node.findChildAfter(KDocTokens.TAG_NAME, WHITE_SPACE) ?.let { tag.node.replaceChild(it, LeafPsiElement(WHITE_SPACE, " ")) } } } } private fun findBeforeAndAfterSpaces(tag: KDocTag) = Pair(tag.node.findChildBefore(KDocTokens.TEXT, WHITE_SPACE).let { it?.text != " " && !(it?.isWhiteSpaceWithNewline() ?: false) }, tag.node.findChildAfter(KDocTokens.TAG_NAME, WHITE_SPACE).let { it?.text != " " && !(it?.isWhiteSpaceWithNewline() ?: false) } ) @Suppress("UnsafeCallOnNullableType", "TOO_LONG_FUNCTION") private fun checkBasicTagsOrder(node: ASTNode) { val kdocTags = node.kDocTags() // distinct basic tags which are present in current KDoc, in proper order val basicTagsOrdered = basicTagsList.filter { basicTag -> kdocTags.any { it.knownTag == basicTag } } // all basic tags from current KDoc val basicTags = kdocTags.filter { basicTagsOrdered.contains(it.knownTag) } val isTagsInCorrectOrder = basicTags .fold(mutableListOf()) { acc, kdocTag -> if (acc.size > 0 && acc.last().knownTag != kdocTag.knownTag) { acc.add(kdocTag) } else if (acc.size == 0) { acc.add(kdocTag) } acc } .map { it.knownTag } == basicTagsOrdered if (!isTagsInCorrectOrder) { KDOC_WRONG_TAGS_ORDER.warnAndFix(configRules, emitWarn, isFixMode, basicTags.joinToString(", ") { "@${it.name}" }, basicTags .first() .node .startOffset, basicTags.first().node) { val basicTagChildren = kdocTags .filter { basicTagsOrdered.contains(it.knownTag) } .map { it.node } val correctKdocOrder = basicTags .sortedBy { basicTagsOrdered.indexOf(it.knownTag) } .map { it.node } basicTagChildren.mapIndexed { index, astNode -> val kdocSection = astNode.treeParent kdocSection.addChild(correctKdocOrder[index].clone() as CompositeElement, astNode) kdocSection.removeChild(astNode) } } } } @Suppress("UnsafeCallOnNullableType", "TOO_LONG_FUNCTION") private fun checkEmptyLineBeforeBasicTags(basicTags: List) { val firstBasicTag = basicTags.firstOrNull() ?: return val hasContentBefore = firstBasicTag .node .allSiblings(true) .let { it.subList(0, it.indexOf(firstBasicTag.node)) } .any { it.elementType !in arrayOf(WHITE_SPACE, KDocTokens.LEADING_ASTERISK) && it.text.isNotBlank() } val previousTag = firstBasicTag.node.prevSibling { it.elementType == KDocElementTypes.KDOC_TAG } val hasEmptyLineBefore = previousTag?.hasEmptyLineAfter() ?: (firstBasicTag .node .previousAsterisk() ?.previousAsterisk() ?.treeNext ?.elementType == WHITE_SPACE) if (hasContentBefore xor hasEmptyLineBefore) { KDOC_NEWLINES_BEFORE_BASIC_TAGS.warnAndFix(configRules, emitWarn, isFixMode, "@${firstBasicTag.name!!}", firstBasicTag.node.startOffset, firstBasicTag.node) { if (hasContentBefore) { previousTag?.applyToPrevSibling(KDocTokens.LEADING_ASTERISK) { previousTag.addChild(treePrev.clone() as ASTNode, null) previousTag.addChild(this.clone() as ASTNode, null) } ?: firstBasicTag.node.applyToPrevSibling(KDocTokens.LEADING_ASTERISK) { treeParent.addChild(treePrev.clone() as ASTNode, this) treeParent.addChild(this.clone() as ASTNode, treePrev) } } else { firstBasicTag.node.apply { val asteriskNode = previousAsterisk()!! treeParent.removeChild(asteriskNode.treePrev) treeParent.removeChild(asteriskNode.treePrev) } } } } } private fun checkEmptyLinesBetweenBasicTags(basicTags: List) { val tagsWithRedundantEmptyLines = basicTags.dropLast(1).filter { tag -> val nextWhiteSpace = tag.node.nextSibling { it.elementType == WHITE_SPACE } // either there is a trailing blank line in tag's body OR there are empty lines right after this tag tag.hasTrailingNewlineInTagBody() || nextWhiteSpace?.text?.count { it == '\n' } != 1 } tagsWithRedundantEmptyLines.forEach { tag -> KDOC_NO_NEWLINES_BETWEEN_BASIC_TAGS.warnAndFix(configRules, emitWarn, isFixMode, "@${tag.name}", tag.startOffset, tag.node) { if (tag.hasTrailingNewlineInTagBody()) { // if there is a blank line in tag's body, we remove it and everything after it, so that the next white space is kept in place // we look for the last LEADING_ASTERISK and take its previous node which should be WHITE_SPACE with newline tag.node.reversedChildren() .takeWhile { it.elementType == WHITE_SPACE || it.elementType == KDocTokens.LEADING_ASTERISK } .firstOrNull { it.elementType == KDocTokens.LEADING_ASTERISK } ?.let { tag.node.removeRange(it.treePrev, null) } } else { // otherwise we remove redundant blank lines from white space node after tag tag.node.nextSibling { it.elementType == WHITE_SPACE }?.leaveOnlyOneNewLine() } } } } @Suppress("UnsafeCallOnNullableType", "ComplexMethod") private fun checkNewLineAfterSpecialTags(node: ASTNode) { val presentSpecialTagNodes = node .getFirstChildWithType(KDocElementTypes.KDOC_SECTION) ?.getAllChildrenWithType(KDocElementTypes.KDOC_TAG) ?.filter { (it.psi as KDocTag).name in specialTagNames } val poorlyFormattedTagNodes = presentSpecialTagNodes?.filterNot { specialTagNode -> // empty line with just * followed by white space or end of block specialTagNode.lastChildNode.elementType == KDocTokens.LEADING_ASTERISK && (specialTagNode.treeNext == null || specialTagNode.treeNext.elementType == WHITE_SPACE && specialTagNode.treeNext.text.count { it == '\n' } == 1) && // and with no empty line before specialTagNode.lastChildNode.treePrev.elementType == WHITE_SPACE && specialTagNode.lastChildNode.treePrev.treePrev.elementType != KDocTokens.LEADING_ASTERISK } if (poorlyFormattedTagNodes != null && poorlyFormattedTagNodes.isNotEmpty()) { KDOC_NO_NEWLINE_AFTER_SPECIAL_TAGS.warnAndFix(configRules, emitWarn, isFixMode, poorlyFormattedTagNodes.joinToString(", ") { "@${(it.psi as KDocTag).name!!}" }, poorlyFormattedTagNodes.first().startOffset, node) { poorlyFormattedTagNodes.forEach { node -> while (node.lastChildNode.elementType == KDocTokens.LEADING_ASTERISK && node.lastChildNode.treePrev.treePrev.elementType == KDocTokens.LEADING_ASTERISK) { node.removeChild(node.lastChildNode) // KDOC_LEADING_ASTERISK node.removeChild(node.lastChildNode) // WHITE_SPACE } if (node.treeParent.lastChildNode != node && node.lastChildNode.elementType != KDocTokens.LEADING_ASTERISK) { val indent = node .prevSibling { it.elementType == WHITE_SPACE } ?.text ?.substringAfter('\n') ?.count { it == ' ' } ?: 0 node.addChild(PsiWhiteSpaceImpl("\n${" ".repeat(indent)}"), null) node.addChild(LeafPsiElement(KDocTokens.LEADING_ASTERISK, "*"), null) } } } } } private fun checkAuthorAndDate(node: ASTNode) { node.kDocTags() .filter { it.knownTag == KDocKnownTag.AUTHOR || it.knownTag == KDocKnownTag.SINCE && it.hasInvalidVersion() } .forEach { KDOC_CONTAINS_DATE_OR_AUTHOR.warn(configRules, emitWarn, it.text.trim(), it.startOffset, it.node) } } // fixme this method can be improved and extracted to utils private fun ASTNode.hasEmptyLineAfter(): Boolean { require(this.elementType == KDocElementTypes.KDOC_TAG) { "This check is only for KDOC_TAG" } return lastChildNode.elementType == KDocTokens.LEADING_ASTERISK && (treeNext == null || treeNext.elementType == WHITE_SPACE && treeNext.text.count { it == '\n' } == 1) } private fun ASTNode.kDocBasicTags() = kDocTags().filter { basicTagsList.contains(it.knownTag) } private fun ASTNode.previousAsterisk() = prevSibling { it.elementType == KDocTokens.LEADING_ASTERISK } private fun ASTNode.applyToPrevSibling(elementType: IElementType, consumer: ASTNode.() -> Unit) { prevSibling { it.elementType == elementType }?.apply(consumer) } /** * Checks whether this tag's content represents an invalid version */ private fun KDocTag.hasInvalidVersion(): Boolean { val content = getContent().trim() if (' ' in content || '/' in content) { // Filter based on symbols that are not allowed in versions. Assuming that people put either version or date in `@since` tag. return true } return versionRegex?.matches(content)?.not() ?: dateFormats.mapNotNull { // try to parse using some standard date patterns runCatching { it.parse(content).get(ChronoField.YEAR) } .getOrNull() } .isNotEmpty() } /** * A [RuleConfiguration] for KDoc formatting */ class KdocFormatConfiguration(config: Map) : RuleConfiguration(config) { /** * Regular expression, if present, against which a version should be matched in `@since` tag. */ val versionRegex: Regex? by lazy { config["versionRegex"]?.let { Regex(it) } } } companion object { const val NAME_ID = "kdoc-formatting" val dateFormats: List = listOf("yyyy-dd-mm", "yy-dd-mm", "yyyy-mm-dd", "yy-mm-dd", "yyyy.mm.dd", "yyyy.dd.mm") .map { DateTimeFormatter.ofPattern(it) } } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter2/kdoc/KdocMethods.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter2.kdoc import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.common.config.rules.getCommonConfiguration import com.saveourtool.diktat.ruleset.constants.Warnings.KDOC_TRIVIAL_KDOC_ON_FUNCTION import com.saveourtool.diktat.ruleset.constants.Warnings.KDOC_WITHOUT_PARAM_TAG import com.saveourtool.diktat.ruleset.constants.Warnings.KDOC_WITHOUT_RETURN_TAG import com.saveourtool.diktat.ruleset.constants.Warnings.KDOC_WITHOUT_THROWS_TAG import com.saveourtool.diktat.ruleset.constants.Warnings.MISSING_KDOC_ON_FUNCTION import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.KotlinParser import com.saveourtool.diktat.ruleset.utils.appendNewlineMergingWhiteSpace import com.saveourtool.diktat.ruleset.utils.findAllDescendantsWithSpecificType import com.saveourtool.diktat.ruleset.utils.findChildAfter import com.saveourtool.diktat.ruleset.utils.findParentNodeWithSpecificType import com.saveourtool.diktat.ruleset.utils.getAllChildrenWithType import com.saveourtool.diktat.ruleset.utils.getBodyLines import com.saveourtool.diktat.ruleset.utils.getFilePath import com.saveourtool.diktat.ruleset.utils.getFirstChildWithType import com.saveourtool.diktat.ruleset.utils.getIdentifierName import com.saveourtool.diktat.ruleset.utils.hasChildOfType import com.saveourtool.diktat.ruleset.utils.hasKnownKdocTag import com.saveourtool.diktat.ruleset.utils.hasTestAnnotation import com.saveourtool.diktat.ruleset.utils.insertTagBefore import com.saveourtool.diktat.ruleset.utils.isAccessibleOutside import com.saveourtool.diktat.ruleset.utils.isAnonymousFunction import com.saveourtool.diktat.ruleset.utils.isGetterOrSetter import com.saveourtool.diktat.ruleset.utils.isLocatedInTest import com.saveourtool.diktat.ruleset.utils.isOverridden import com.saveourtool.diktat.ruleset.utils.isStandardMethod import com.saveourtool.diktat.ruleset.utils.kDocTags import com.saveourtool.diktat.ruleset.utils.parameterNames import com.saveourtool.diktat.ruleset.utils.splitPathToDirs import org.jetbrains.kotlin.KtNodeTypes import org.jetbrains.kotlin.KtNodeTypes.BLOCK import org.jetbrains.kotlin.KtNodeTypes.CALL_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.CATCH import org.jetbrains.kotlin.KtNodeTypes.DOT_QUALIFIED_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.FUN import org.jetbrains.kotlin.KtNodeTypes.MODIFIER_LIST import org.jetbrains.kotlin.KtNodeTypes.REFERENCE_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.THIS_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.THROW import org.jetbrains.kotlin.KtNodeTypes.TRY import org.jetbrains.kotlin.KtNodeTypes.TYPE_REFERENCE import org.jetbrains.kotlin.com.intellij.lang.ASTFactory import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl import org.jetbrains.kotlin.com.intellij.psi.tree.TokenSet import org.jetbrains.kotlin.kdoc.lexer.KDocTokens import org.jetbrains.kotlin.kdoc.lexer.KDocTokens.KDOC import org.jetbrains.kotlin.kdoc.parser.KDocElementTypes import org.jetbrains.kotlin.kdoc.parser.KDocKnownTag import org.jetbrains.kotlin.kdoc.psi.impl.KDocTag import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.lexer.KtTokens.COLON import org.jetbrains.kotlin.lexer.KtTokens.EQ import org.jetbrains.kotlin.lexer.KtTokens.IDENTIFIER import org.jetbrains.kotlin.lexer.KtTokens.WHITE_SPACE import org.jetbrains.kotlin.psi.KtCallExpression import org.jetbrains.kotlin.psi.KtCatchClause import org.jetbrains.kotlin.psi.KtDotQualifiedExpression import org.jetbrains.kotlin.psi.KtFunction import org.jetbrains.kotlin.psi.KtThrowExpression import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType import org.jetbrains.kotlin.psi.psiUtil.referenceExpression import java.lang.Class.forName /** * This rule checks that whenever the method has arguments, return value, can throw exceptions, * KDoc block should contain `@param`, `@return`, `@throws`. * For `@return` check methods with explicit return type are supported and methods with inferred return * type are supported the following way: they should either declare return type `Unit` or have `@return` tag. * Currently only `throw` keyword from this methods body is supported for `@throws` check. */ @Suppress("ForbiddenComment") class KdocMethods(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(KDOC_TRIVIAL_KDOC_ON_FUNCTION, KDOC_WITHOUT_PARAM_TAG, KDOC_WITHOUT_RETURN_TAG, KDOC_WITHOUT_THROWS_TAG, MISSING_KDOC_ON_FUNCTION) ) { /** * @param node */ override fun logic(node: ASTNode) { val isModifierAccessibleOutsideOrActual: Boolean by lazy { node.getFirstChildWithType(MODIFIER_LIST).run { isAccessibleOutside() && this?.hasChildOfType(KtTokens.ACTUAL_KEYWORD) != true } } if (node.elementType == FUN && isModifierAccessibleOutsideOrActual && !node.isOverridden()) { val config = configRules.getCommonConfiguration() val filePath = node.getFilePath() val isTestMethod = node.hasTestAnnotation() || isLocatedInTest(filePath.splitPathToDirs(), config.testAnchors) if (!isTestMethod && !node.isStandardMethod() && !node.isSingleLineGetterOrSetter() && !node.isAnonymousFunction()) { checkSignatureDescription(node) } } else if (node.elementType == KDocElementTypes.KDOC_SECTION) { checkKdocBody(node) } } private fun hasFunParent(node: ASTNode): Boolean { var parent = node.treeParent while (parent != null) { if (parent.elementType == FUN) { return true } parent = parent.treeParent } return false } @Suppress( "UnsafeCallOnNullableType", "AVOID_NULL_CHECKS", "CyclomaticComplexMethod" ) private fun checkSignatureDescription(node: ASTNode) { val kdoc = node.getFirstChildWithType(KDOC) val kdocTags = kdoc?.kDocTags() val name = node.getIdentifierName()!!.text val (missingParameters, kDocMissingParameters) = getMissingParameters(node, kdocTags) val explicitlyThrownExceptions = getExplicitlyThrownExceptions(node) + getRethrownExceptions(node) val missingExceptions = explicitlyThrownExceptions .minus((kdocTags ?.filter { it.knownTag == KDocKnownTag.THROWS } ?.mapNotNull { it.getSubjectLinkName() } ?.toSet() ?: emptySet()).toSet()) val paramCheckFailed = (missingParameters.isNotEmpty() && !node.isSingleLineGetterOrSetter()) || kDocMissingParameters.isNotEmpty() val returnCheckFailed = hasReturnCheckFailed(node, kdocTags) val throwsCheckFailed = missingExceptions.isNotEmpty() val anyTagFailed = paramCheckFailed || returnCheckFailed || throwsCheckFailed // if no tag failed, we have too little information to suggest KDoc - it would just be empty if (kdoc == null && hasFunParent(node)) { return } else if (kdoc == null && anyTagFailed) { addKdocTemplate(node, name, missingParameters, explicitlyThrownExceptions, returnCheckFailed) } else if (kdoc == null && !isReferenceExpressionWithSameName(node)) { MISSING_KDOC_ON_FUNCTION.warn(configRules, emitWarn, name, node.startOffset, node) } else { if (paramCheckFailed) { handleParamCheck(node, kdoc, missingParameters, kDocMissingParameters, kdocTags) } if (returnCheckFailed) { handleReturnCheck(node, kdoc, kdocTags) } if (throwsCheckFailed) { handleThrowsCheck(node, kdoc, missingExceptions) } } } private fun KDocTag.getSubjectLinkName(): String? = node.getChildren(null) .dropWhile { it.elementType == KDocTokens.TAG_NAME } .dropWhile { it.elementType == WHITE_SPACE } .firstOrNull { it.elementType == KDocTokens.MARKDOWN_LINK } ?.text @Suppress("TYPE_ALIAS") private fun getMissingParameters(node: ASTNode, kdocTags: Collection?): Pair, List> { val parameterNames = node.parameterNames() val kdocParamList = kdocTags?.filter { it.knownTag == KDocKnownTag.PARAM && it.getSubjectLinkName() != null } return if (parameterNames.isEmpty()) { Pair(emptyList(), kdocParamList ?: emptyList()) } else if (!kdocParamList.isNullOrEmpty()) { Pair(parameterNames.minus(kdocParamList.map { it.getSubjectLinkName() }.toSet()), kdocParamList.filter { it.getSubjectLinkName() !in parameterNames }) } else { Pair(parameterNames.toList(), emptyList()) } } private fun isReferenceExpressionWithSameName(node: ASTNode): Boolean { val lastDotQualifiedExpression = node.findChildByType(DOT_QUALIFIED_EXPRESSION)?.psi ?.let { (it as KtDotQualifiedExpression).selectorExpression?.text?.substringBefore('(') } val funName = (node.psi as KtFunction).name return funName == lastDotQualifiedExpression } @Suppress("WRONG_NEWLINES") private fun hasReturnCheckFailed(node: ASTNode, kdocTags: Collection?): Boolean { if (node.isSingleLineGetterOrSetter()) { return false } val explicitReturnType = node.findChildAfter(COLON, TYPE_REFERENCE) val hasNotExpressionBodyTypes = allExpressionBodyTypes.any { node.hasChildOfType(it) } val hasExplicitNotUnitReturnType = explicitReturnType != null && explicitReturnType.text != "Unit" val hasExplicitUnitReturnType = explicitReturnType != null && explicitReturnType.text == "Unit" val isFunWithExpressionBody = node.hasChildOfType(EQ) val isReferenceExpressionWithSameName = node.findAllDescendantsWithSpecificType(REFERENCE_EXPRESSION).map { it.text }.contains((node.psi as KtFunction).name) val hasReturnKdoc = kdocTags != null && kdocTags.hasKnownKdocTag(KDocKnownTag.RETURN) return (hasExplicitNotUnitReturnType || isFunWithExpressionBody && !hasExplicitUnitReturnType && hasNotExpressionBodyTypes) && !hasReturnKdoc && !isReferenceExpressionWithSameName } private fun isThrowInTryCatchBlock(node: ASTNode?): Boolean { node ?: return false val parent = node.findParentNodeWithSpecificType(TRY) val nodeName = node.findAllDescendantsWithSpecificType(IDENTIFIER).firstOrNull() ?: return false if (parent?.elementType == TRY) { val catchNodes = parent?.getAllChildrenWithType(CATCH) val findNodeWithMatchingCatch = catchNodes?.firstOrNull { catchNode -> val matchingNodeForCatchNode = catchNode.findAllDescendantsWithSpecificType(IDENTIFIER) .firstOrNull { catchNodeName -> nodeName.text == catchNodeName.text || try { val nodeClass = forName("java.lang.${nodeName.text}") val nodeInstance = nodeClass.getDeclaredConstructor().newInstance() val catchNodeClass = forName("java.lang.${catchNodeName.text}") catchNodeClass.isInstance(nodeInstance) } catch (e: ClassNotFoundException) { false } } matchingNodeForCatchNode != null } return findNodeWithMatchingCatch != null } return false } private fun getExplicitlyThrownExceptions(node: ASTNode): Set { val codeBlock = node.getFirstChildWithType(BLOCK) val throwKeywords = codeBlock?.findAllDescendantsWithSpecificType(THROW) return throwKeywords ?.asSequence() ?.filter { !isThrowInTryCatchBlock(it) } ?.map { it.psi as KtThrowExpression } ?.filter { // we only take freshly created exceptions here: `throw IAE("stuff")` vs `throw e` it.thrownExpression is KtCallExpression } ?.mapNotNull { it.thrownExpression?.referenceExpression()?.text } ?.toSet() ?: emptySet() } @Suppress("UnsafeCallOnNullableType") private fun getRethrownExceptions(node: ASTNode) = node.findAllDescendantsWithSpecificType(CATCH).flatMap { catchClauseNode -> // `parameterList` is `@Nullable @IfNotParsed` (catchClauseNode.psi as KtCatchClause).parameterList!!.parameters .filter { // `catch (_: Exception)` - parameter can be anonymous it.name != "_" } .filter { param -> // check whether caught parameter is rethrown in the same catch clause (catchClauseNode.psi as KtCatchClause).catchBody?.collectDescendantsOfType()?.any { it.thrownExpression?.referenceExpression()?.text == param.name } == true } .map { // parameter in catch statement `catch (e: Type)` should always have type it.typeReference!!.text } } .toSet() @Suppress("UnsafeCallOnNullableType") private fun handleParamCheck(node: ASTNode, kdoc: ASTNode?, missingParameters: Collection, kdocMissingParameters: List, kdocTags: Collection? ) { kdocMissingParameters.forEach { KDOC_WITHOUT_PARAM_TAG.warn(configRules, emitWarn, "${ it.getSubjectLinkName() } param isn't present in argument list", it.node.startOffset, it.node) } if (missingParameters.isNotEmpty()) { KDOC_WITHOUT_PARAM_TAG.warnAndFix(configRules, emitWarn, isFixMode, "${node.getIdentifierName()!!.text} (${missingParameters.joinToString()})", node.startOffset, node) { val beforeTag = kdocTags?.find { it.knownTag == KDocKnownTag.RETURN } ?: kdocTags?.find { it.knownTag == KDocKnownTag.THROWS } missingParameters.filterNotNull().forEach { missingParameter -> kdoc?.insertTagBefore(beforeTag?.node) { addChild(ASTFactory.leaf(KDocTokens.TAG_NAME, "@param")) addChild(ASTFactory.whitespace(" ")) val kdocMarkdownLink = ASTFactory.composite(KDocTokens.MARKDOWN_LINK) .also { addChild(it) } val kdocName = ASTFactory.composite(KDocElementTypes.KDOC_NAME) .also { kdocMarkdownLink.addChild(it) } kdocName.addChild(ASTFactory.leaf(KtTokens.IDENTIFIER, missingParameter)) } } } } } @Suppress("UnsafeCallOnNullableType") private fun handleReturnCheck(node: ASTNode, kdoc: ASTNode?, kdocTags: Collection?, ) { KDOC_WITHOUT_RETURN_TAG.warnAndFix(configRules, emitWarn, isFixMode, node.getIdentifierName()!!.text, node.startOffset, node) { val beforeTag = kdocTags?.find { it.knownTag == KDocKnownTag.THROWS } kdoc?.insertTagBefore(beforeTag?.node) { addChild(LeafPsiElement(KDocTokens.TAG_NAME, "@return")) } } } @Suppress("UnsafeCallOnNullableType") private fun handleThrowsCheck(node: ASTNode, kdoc: ASTNode?, missingExceptions: Collection, ) { KDOC_WITHOUT_THROWS_TAG.warnAndFix(configRules, emitWarn, isFixMode, "${node.getIdentifierName()!!.text} (${missingExceptions.joinToString()})", node.startOffset, node) { missingExceptions.forEach { kdoc?.insertTagBefore(null) { addChild(LeafPsiElement(KDocTokens.TAG_NAME, "@throws")) addChild(PsiWhiteSpaceImpl(" ")) addChild(LeafPsiElement(KDocTokens.MARKDOWN_LINK, it)) } } } } @Suppress("UnsafeCallOnNullableType") private fun addKdocTemplate(node: ASTNode, name: String, missingParameters: Collection, explicitlyThrownExceptions: Collection, returnCheckFailed: Boolean, ) { MISSING_KDOC_ON_FUNCTION.warnAndFix(configRules, emitWarn, isFixMode, name, node.startOffset, node) { val kdocTemplate = "/**\n" + (missingParameters.joinToString("") { " * @param $it\n" } + (if (returnCheckFailed) " * @return\n" else "") + explicitlyThrownExceptions.joinToString("") { " * @throws $it\n" } + " */\n" ) val kdocNode = KotlinParser().createNode(kdocTemplate).findChildByType(KDOC)!! node.appendNewlineMergingWhiteSpace(node.firstChildNode, node.firstChildNode) node.addChild(kdocNode, node.firstChildNode) } } private fun checkKdocBody(node: ASTNode) { val kdocTextNodes = node.getChildren(TokenSet.create(KDocTokens.TEXT)) if (kdocTextNodes.size == 1) { val kdocText = kdocTextNodes .first() .text .trim() if (kdocText.matches(uselessKdocRegex)) { KDOC_TRIVIAL_KDOC_ON_FUNCTION.warn(configRules, emitWarn, kdocText, kdocTextNodes.first().startOffset, node) } } } private fun ASTNode.isSingleLineGetterOrSetter(): Boolean { val dotQualifiedExp = this.findChildByType(DOT_QUALIFIED_EXPRESSION)?.psi?.let { it as KtDotQualifiedExpression } val isThisExpression = dotQualifiedExp != null && dotQualifiedExp.receiverExpression.node.elementType == THIS_EXPRESSION val isExpressionBodyTypes = expressionBodyTypes.any { hasChildOfType(it) } return isGetterOrSetter() && (isExpressionBodyTypes || getBodyLines().size == 1 || isThisExpression) } companion object { const val NAME_ID = "kdoc-methods" private val expressionBodyTypes = setOf(CALL_EXPRESSION, REFERENCE_EXPRESSION) private val allExpressionBodyTypes = setOf( DOT_QUALIFIED_EXPRESSION, CALL_EXPRESSION, REFERENCE_EXPRESSION, KtNodeTypes.BINARY_EXPRESSION, KtNodeTypes.LAMBDA_EXPRESSION, KtNodeTypes.CALLABLE_REFERENCE_EXPRESSION, KtNodeTypes.SAFE_ACCESS_EXPRESSION, KtNodeTypes.WHEN_CONDITION_EXPRESSION, KtNodeTypes.COLLECTION_LITERAL_EXPRESSION, ) private val uselessKdocRegex = """^([rR]eturn|[gGsS]et)[s]?\s+\w+(\s+\w+)?$""".toRegex() } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/AnnotationNewLineRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter3 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.ANNOTATION_NEW_LINE import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.* import org.jetbrains.kotlin.KtNodeTypes.ANNOTATION_ENTRY import org.jetbrains.kotlin.KtNodeTypes.CLASS import org.jetbrains.kotlin.KtNodeTypes.FUN import org.jetbrains.kotlin.KtNodeTypes.MODIFIER_LIST import org.jetbrains.kotlin.KtNodeTypes.PRIMARY_CONSTRUCTOR import org.jetbrains.kotlin.KtNodeTypes.SECONDARY_CONSTRUCTOR import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl /** * This rule makes each annotation applied to a class, method or constructor is on its own line. Except: if first annotation of constructor, class or method */ class AnnotationNewLineRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(ANNOTATION_NEW_LINE) ) { override fun logic(node: ASTNode) { when (node.elementType) { CLASS, FUN, PRIMARY_CONSTRUCTOR, SECONDARY_CONSTRUCTOR -> checkAnnotation(node) else -> return } } private fun checkAnnotation(node: ASTNode) { node.findChildByType(MODIFIER_LIST)?.let { modList -> fixAnnotation(modList) } } private fun fixAnnotation(node: ASTNode) { if (node.getAllChildrenWithType(ANNOTATION_ENTRY).size <= 1) { return } node.getAllChildrenWithType(ANNOTATION_ENTRY).forEach { if (!it.isFollowedByNewlineWithComment() || !it.isBeginNewLineWithComment()) { deleteSpaces(it, !it.isFollowedByNewlineWithComment()) } } } private fun deleteSpaces(node: ASTNode, rightSide: Boolean) { ANNOTATION_NEW_LINE.warnAndFix(configRules, emitWarn, isFixMode, "${node.text} not on a single line", node.startOffset, node) { if (rightSide) { if (node.treeNext?.isWhiteSpace() == true) { node.removeChild(node.treeNext) } node.treeParent.addChild(PsiWhiteSpaceImpl("\n"), node.treeNext) } if (node == node.treeParent.getFirstChildWithType(node.elementType)) { // Current node is ANNOTATION_ENTRY. treeParent(ModifierList) -> treeParent(PRIMARY_CONSTRUCTOR) // Checks if there is a white space before grandparent node val hasSpaceBeforeGrandparent = node .treeParent .treeParent .treePrev .isWhiteSpace() if (hasSpaceBeforeGrandparent) { (node.treeParent.treeParent.treePrev as LeafPsiElement).rawReplaceWithText("\n") } } } } companion object { const val NAME_ID = "annotation-new-line" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/BlockStructureBraces.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter3 import com.saveourtool.diktat.common.config.rules.RuleConfiguration import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.common.config.rules.getRuleConfig import com.saveourtool.diktat.ruleset.constants.Warnings.BRACES_BLOCK_STRUCTURE_ERROR import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.* import com.saveourtool.diktat.ruleset.utils.isWhiteSpaceWithNewline import org.jetbrains.kotlin.KtNodeTypes.BLOCK import org.jetbrains.kotlin.KtNodeTypes.BODY import org.jetbrains.kotlin.KtNodeTypes.CATCH import org.jetbrains.kotlin.KtNodeTypes.CLASS import org.jetbrains.kotlin.KtNodeTypes.CLASS_BODY import org.jetbrains.kotlin.KtNodeTypes.CLASS_INITIALIZER import org.jetbrains.kotlin.KtNodeTypes.DO_WHILE import org.jetbrains.kotlin.KtNodeTypes.ELSE import org.jetbrains.kotlin.KtNodeTypes.FINALLY import org.jetbrains.kotlin.KtNodeTypes.FUN import org.jetbrains.kotlin.KtNodeTypes.FUNCTION_LITERAL import org.jetbrains.kotlin.KtNodeTypes.IF import org.jetbrains.kotlin.KtNodeTypes.LAMBDA_ARGUMENT import org.jetbrains.kotlin.KtNodeTypes.OBJECT_DECLARATION import org.jetbrains.kotlin.KtNodeTypes.SECONDARY_CONSTRUCTOR import org.jetbrains.kotlin.KtNodeTypes.THEN import org.jetbrains.kotlin.KtNodeTypes.TRY import org.jetbrains.kotlin.KtNodeTypes.WHEN import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType import org.jetbrains.kotlin.lexer.KtTokens.CATCH_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.ELSE_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.FINALLY_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.LBRACE import org.jetbrains.kotlin.lexer.KtTokens.RBRACE import org.jetbrains.kotlin.lexer.KtTokens.WHILE_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.WHITE_SPACE import org.jetbrains.kotlin.psi.KtIfExpression import org.jetbrains.kotlin.psi.KtTryExpression /** * This rule checks that *non-empty* code blocks with braces follow the K&R style (1TBS or OTBS style): * - The opening brace is on the same same line with the first line of the code block * - The closing brace is on it's new line * - The closing brace can be followed by a new line. Only exceptions are: `else`, `finally`, `while` (from do-while statement) or `catch` keywords. * These keywords should not be split from the closing brace by a newline. * Exceptions: * - opening brace of lambda * - braces around `else`/`catch`/`finally`/`while` (in `do-while` loop) */ class BlockStructureBraces(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(BRACES_BLOCK_STRUCTURE_ERROR), ) { override fun logic(node: ASTNode) { val configuration = BlockStructureBracesConfiguration( configRules.getRuleConfig(BRACES_BLOCK_STRUCTURE_ERROR)?.configuration ?: emptyMap() ) when (node.elementType) { FUNCTION_LITERAL -> checkLambda(node, configuration) CLASS, OBJECT_DECLARATION -> checkClass(node, configuration) FUN, CLASS_INITIALIZER, SECONDARY_CONSTRUCTOR -> checkFun(node, configuration) IF -> checkIf(node, configuration) WHEN -> checkWhen(node, configuration) in loopType -> checkLoop(node, configuration) TRY -> checkTry(node, configuration) else -> return } } private fun checkLambda(node: ASTNode, configuration: BlockStructureBracesConfiguration) { val isSingleLineLambda = node.text.lines().size == 1 if (!isSingleLineLambda) { checkCloseBrace(node, configuration) } } @Suppress("UnsafeCallOnNullableType") private fun checkClass(node: ASTNode, configuration: BlockStructureBracesConfiguration) { if (node.hasChildOfType(CLASS_BODY) && !node.findChildByType(CLASS_BODY).isBlockEmpty()) { checkOpenBraceOnSameLine(node, CLASS_BODY, configuration) checkCloseBrace(node.findChildByType(CLASS_BODY)!!, configuration) } } @Suppress("UnsafeCallOnNullableType") // `catch` and `finally` clauses should always have body in `{}`, therefore !! private fun checkTry(node: ASTNode, configuration: BlockStructureBracesConfiguration) { val tryBlock = node.psi as KtTryExpression val catchBlocks = tryBlock.catchClauses.map { it.node } val finallyBlock = tryBlock.finallyBlock?.node checkOpenBraceOnSameLine(tryBlock.node, BLOCK, configuration) val allMiddleSpaceNodes = node.findAllDescendantsWithSpecificType(CATCH).map { it.treePrev } checkMidBrace(allMiddleSpaceNodes, node, CATCH_KEYWORD) catchBlocks.forEach { checkOpenBraceOnSameLine(it, BLOCK, configuration) checkCloseBrace(it.findChildByType(BLOCK)!!, configuration) } finallyBlock?.let { block -> checkOpenBraceOnSameLine(block, BLOCK, configuration) checkCloseBrace(block.findChildByType(BLOCK)!!, configuration) val newAllMiddleSpaceNodes = node.findAllDescendantsWithSpecificType(FINALLY).map { it.treePrev } checkMidBrace(newAllMiddleSpaceNodes, node, FINALLY_KEYWORD) } } @Suppress("UnsafeCallOnNullableType") private fun checkLoop(node: ASTNode, configuration: BlockStructureBracesConfiguration) { node.findChildByType(BODY)?.let { if (!it.findChildByType(BLOCK).isBlockEmpty()) { checkOpenBraceOnSameLine(node, BODY, configuration) // check that there is a `BLOCK` child is done inside `!isBlockEmpty` checkCloseBrace(it.findChildByType(BLOCK)!!, configuration) if (node.elementType == DO_WHILE) { val allMiddleNode = listOf(node.findChildByType(BODY)!!.treeNext) checkMidBrace(allMiddleNode, node, WHILE_KEYWORD) } } } } private fun checkWhen(node: ASTNode, configuration: BlockStructureBracesConfiguration) { /// WHEN expression doesn't contain BLOCK element and LBRECE isn't the first child, so we should to find it. val childrenAfterLbrace = node .getChildren(null) .toList() .run { subList(indexOfFirst { it.elementType == LBRACE }, size) } if (!emptyBlockList.containsAll(childrenAfterLbrace.distinct().map { it.elementType })) { checkOpenBraceOnSameLine(node, LBRACE, configuration) checkCloseBrace(node, configuration) } } @Suppress("UnsafeCallOnNullableType") private fun checkFun(node: ASTNode, configuration: BlockStructureBracesConfiguration) { if (!node.findChildByType(BLOCK).isBlockEmpty()) { checkOpenBraceOnSameLine(node, BLOCK, configuration) checkCloseBrace(node.findChildByType(BLOCK)!!, configuration) } } @Suppress("UnsafeCallOnNullableType") private fun checkIf(node: ASTNode, configuration: BlockStructureBracesConfiguration) { val ifPsi = node.psi as KtIfExpression val thenNode = ifPsi.then?.node val hasElseBranch = ifPsi.elseKeyword != null val elseNode = ifPsi.`else`?.node if (thenNode != null && thenNode.hasChildOfType(LBRACE)) { checkOpenBraceOnSameLine(node, THEN, configuration) checkCloseBrace(thenNode, configuration) if (hasElseBranch) { // thenNode might have been altered by this point val allMiddleNode = listOf(node.findChildByType(THEN)!!.treeNext) checkMidBrace(allMiddleNode, node, ELSE_KEYWORD) } } if (hasElseBranch && elseNode != null && elseNode.elementType != IF && elseNode.hasChildOfType(LBRACE)) { checkOpenBraceOnSameLine(node, ELSE, configuration) checkCloseBrace(elseNode, configuration) } } private fun checkOpenBraceOnSameLine( node: ASTNode, beforeType: IElementType, configuration: BlockStructureBracesConfiguration ) { if (!configuration.openBrace) { return } val nodeBefore = node.findChildByType(beforeType) val braceSpace = nodeBefore?.treePrev if (braceSpace == null || checkBraceNode(braceSpace, true)) { BRACES_BLOCK_STRUCTURE_ERROR.warnAndFix(configRules, emitWarn, isFixMode, "incorrect newline before opening brace", (braceSpace ?: node).startOffset, node) { if (braceSpace == null || braceSpace.elementType != WHITE_SPACE) { node.addChild(PsiWhiteSpaceImpl(" "), nodeBefore) } else { if (braceSpace.treePrev.elementType in commentType) { val commentBefore = braceSpace.treePrev if (commentBefore.treePrev.elementType == WHITE_SPACE) { commentBefore.treeParent.removeChild(commentBefore.treePrev) } commentBefore.treeParent.removeChild(commentBefore) node.treeParent.addChild(commentBefore.clone() as ASTNode, node) node.treeParent.addChild(PsiWhiteSpaceImpl("\n"), node) } braceSpace.treeParent.replaceWhiteSpaceText(braceSpace, " ") } } } checkOpenBraceEndLine(node, beforeType) } private fun checkOpenBraceEndLine(node: ASTNode, beforeType: IElementType) { val newNode = (if (beforeType == THEN || beforeType == ELSE) node.findChildByType(beforeType) else node) ?.findLBrace() ?.treeNext ?: return if (checkBraceNode(newNode)) { BRACES_BLOCK_STRUCTURE_ERROR.warnAndFix(configRules, emitWarn, isFixMode, "incorrect same line after opening brace", newNode.startOffset, newNode) { if (newNode.elementType != WHITE_SPACE) { newNode.treeParent.addChild(PsiWhiteSpaceImpl("\n"), newNode) } else { (newNode as LeafPsiElement).rawReplaceWithText("\n") } } } } private fun checkMidBrace( allMiddleSpace: List, node: ASTNode, keyword: IElementType ) { allMiddleSpace.forEach { space -> if (checkBraceNode(space, true)) { BRACES_BLOCK_STRUCTURE_ERROR.warnAndFix(configRules, emitWarn, isFixMode, "incorrect new line after closing brace", space.startOffset, space) { if (space.elementType != WHITE_SPACE) { node.addChild(PsiWhiteSpaceImpl(" "), node.findChildByType(keyword)) } else { (space as LeafPsiElement).rawReplaceWithText(" ") } } } } } @Suppress("UnsafeCallOnNullableType") private fun checkCloseBrace(node: ASTNode, configuration: BlockStructureBracesConfiguration) { if (!configuration.closeBrace) { return } val space = node.findChildByType(RBRACE)!!.treePrev node.findParentNodeWithSpecificType(LAMBDA_ARGUMENT)?.let { if (space.text.isEmpty()) { return } } if (checkBraceNode(space)) { BRACES_BLOCK_STRUCTURE_ERROR.warnAndFix(configRules, emitWarn, isFixMode, "no newline before closing brace", (space.treeNext ?: node.findChildByType(RBRACE))!!.startOffset, node) { if (space.elementType != WHITE_SPACE) { node.addChild(PsiWhiteSpaceImpl("\n"), node.findChildByType(RBRACE)) } else { (space as LeafPsiElement).rawReplaceWithText("\n") } } } } private fun checkBraceNode(node: ASTNode, shouldContainNewline: Boolean = false) = shouldContainNewline == node.isWhiteSpaceWithNewline() /** * Configuration for style of braces in block */ class BlockStructureBracesConfiguration(config: Map) : RuleConfiguration(config) { /** * Whether the opening brace should be placed on a new line */ val openBrace = config["openBraceNewline"]?.toBoolean() ?: true /** * Whether a closing brace should be placed on a new line */ val closeBrace = config["closeBraceNewline"]?.toBoolean() ?: true } companion object { const val NAME_ID = "block-structure" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/BooleanExpressionsRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter3 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.COMPLEX_BOOLEAN_EXPRESSION import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.KotlinParser import com.saveourtool.diktat.ruleset.utils.findAllNodesWithCondition import com.saveourtool.diktat.ruleset.utils.isLeaf import com.saveourtool.diktat.ruleset.utils.isPartOfComment import com.saveourtool.diktat.ruleset.utils.logicalInfixMethodMapping import com.saveourtool.diktat.ruleset.utils.logicalInfixMethods import com.bpodgursky.jbool_expressions.Expression import com.bpodgursky.jbool_expressions.options.ExprOptions import com.bpodgursky.jbool_expressions.parsers.ExprParser import com.bpodgursky.jbool_expressions.parsers.TokenMapper import com.bpodgursky.jbool_expressions.rules.DeMorgan import com.bpodgursky.jbool_expressions.rules.DistributiveLaw import com.bpodgursky.jbool_expressions.rules.Rule import com.bpodgursky.jbool_expressions.rules.RuleList import com.bpodgursky.jbool_expressions.rules.RulesHelper import org.jetbrains.kotlin.KtNodeTypes.BINARY_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.CONDITION import org.jetbrains.kotlin.KtNodeTypes.PARENTHESIZED import org.jetbrains.kotlin.KtNodeTypes.PREFIX_EXPRESSION import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.psi.KtBinaryExpression import org.jetbrains.kotlin.psi.KtParenthesizedExpression import org.jetbrains.kotlin.psi.KtPrefixExpression import org.jetbrains.kotlin.psi.psiUtil.parents /** * Rule that checks if the boolean expression can be simplified. */ class BooleanExpressionsRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(COMPLEX_BOOLEAN_EXPRESSION) ) { override fun logic(node: ASTNode) { if (node.elementType == CONDITION) { checkBooleanExpression(node) } } @Suppress("TooGenericExceptionCaught") private fun checkBooleanExpression(node: ASTNode) { // This class is used to assign a variable name for every elementary boolean expression. It is required for jbool to operate. val expressionsReplacement = ExpressionsReplacement() val correctedExpression = formatBooleanExpressionAsString(node, expressionsReplacement) if (expressionsReplacement.isEmpty()) { // this happens, if we haven't found any expressions that can be simplified return } // If there are method calls in conditions val expr: Expression = try { ExprParser.parse(correctedExpression, expressionsReplacement.orderedTokenMapper) } catch (exc: RuntimeException) { if (exc.message?.startsWith("Unrecognized!") == true) { // this comes up if there is an unparsable expression (jbool doesn't have own exception type). For example a.and(b) return } else { throw exc } } val simplifiedExpression = RulesHelper.applySet(expr, allRules(), ExprOptions.noCaching()) if (expr != simplifiedExpression) { COMPLEX_BOOLEAN_EXPRESSION.warnAndFix(configRules, emitWarn, isFixMode, node.text, node.startOffset, node) { fixBooleanExpression(node, simplifiedExpression, expressionsReplacement) } } } /** * Converts a complex boolean expression into a string representation, mapping each elementary expression to a letter token. * These tokens are collected into [expressionsReplacement]. * For example: * ``` * (a > 5 && b != 2) -> A & B * (a > 5 || false) -> A | false * (a > 5 || x.foo()) -> A | B * ``` * * @param node * @param expressionsReplacement a special class for replacements expression->token * @return formatted string representation of expression */ @Suppress("UnsafeCallOnNullableType", "ForbiddenComment") internal fun formatBooleanExpressionAsString(node: ASTNode, expressionsReplacement: ExpressionsReplacement): String { val (booleanBinaryExpressions, otherBinaryExpressions) = node.collectElementaryExpressions() val logicalExpressions = otherBinaryExpressions.filter { otherBinaryExpression -> // keeping only boolean expressions, keeping things like `a + b < 6` and excluding `a + b` (otherBinaryExpression.psi as KtBinaryExpression).operationReference.text in logicalInfixMethods && // todo: support xor; for now skip all expressions that are nested in xor otherBinaryExpression.parents() .takeWhile { it != node } .none { (it.psi as? KtBinaryExpression)?.isXorExpression() ?: false } } // Boolean expressions like `a > 5 && b < 7` or `x.isEmpty() || (y.isNotEmpty())` we convert to individual parts. val elementaryBooleanExpressions = booleanBinaryExpressions .map { it.psi as KtBinaryExpression } .flatMap { listOf(it.left!!.node, it.right!!.node) } .map { // remove parentheses around expression, if there are any it.removeAllParentheses() } .filterNot { // finally, if parts are binary expressions themselves, they should be present in our lists and we will process them later. it.elementType == BINARY_EXPRESSION || // !(a || b) should be skipped too, `a` and `b` should be present later (it.psi as? KtPrefixExpression)?.lastChild ?.node ?.removeAllParentheses() ?.elementType == BINARY_EXPRESSION || // `true` and `false` are valid tokens for jBool, so we keep them. it.text == "true" || it.text == "false" } (logicalExpressions + elementaryBooleanExpressions).forEach { expression -> expressionsReplacement.addExpression(expression) } // Prepare final formatted string // At first, substitute all elementary expressions with variables val correctedExpression = expressionsReplacement.replaceExpressions(node.textWithoutComments()) // jBool library is using & as && and | as || return "(${correctedExpression .replace("&&", "&") .replace("||", "|")})" } /** * Split the complex expression into elementary parts */ private fun ASTNode.collectElementaryExpressions() = this .findAllNodesWithCondition { astNode -> astNode.elementType == BINARY_EXPRESSION && // filter out boolean conditions in nested lambdas, e.g. `if (foo.filter { a && b })` (astNode == this || astNode.parents().takeWhile { it != this } .all { it.elementType in setOf(BINARY_EXPRESSION, PARENTHESIZED, PREFIX_EXPRESSION) }) } .partition { val operationReferenceText = (it.psi as KtBinaryExpression).operationReference.text operationReferenceText == "&&" || operationReferenceText == "||" } private fun ASTNode.removeAllParentheses(): ASTNode { val result = (this.psi as? KtParenthesizedExpression)?.expression?.node ?: return this return result.removeAllParentheses() } private fun ASTNode.textWithoutComments() = findAllNodesWithCondition(withSelf = false) { it.isLeaf() } .filterNot { it.isPartOfComment() } .joinToString(separator = "") { it.text } .replace("\n", " ") private fun fixBooleanExpression( node: ASTNode, simplifiedExpr: Expression, expressionsReplacement: ExpressionsReplacement ) { val correctKotlinBooleanExpression = simplifiedExpr .toString() .replace("&", "&&") .replace("|", "||") .removePrefix("(") .removeSuffix(")") node.replaceChild(node.firstChildNode, KotlinParser().createNode(expressionsReplacement.restoreFullExpression(correctKotlinBooleanExpression))) } private fun KtBinaryExpression.isXorExpression() = operationReference.text == "xor" /** * A special class to replace expressions (and restore it back) * Note: mapping is String to Char(and Char to Char) actually, but will keep it as String for simplicity */ internal inner class ExpressionsReplacement { private val expressionToToken: HashMap = LinkedHashMap() private val tokenToExpression: HashMap = HashMap() private val tokenToOrderedToken: HashMap = HashMap() /** * TokenMapper for first call ExprParser which remembers the order of expression. */ val orderedTokenMapper: TokenMapper = TokenMapper { name -> getLetter(tokenToOrderedToken, name) } /** * Returns true if this object contains no replacements. * * @return true if this object contains no replacements */ fun isEmpty(): Boolean = expressionToToken.isEmpty() /** * Returns the number of replacements in this object. * * @return the number of replacements in this object */ fun size(): Int = expressionToToken.size /** * Register an expression for further replacement * * @param expressionAstNode astNode which contains boolean expression */ fun addExpression(expressionAstNode: ASTNode) { val expressionText = expressionAstNode.textWithoutComments() // support case when `boolean_expression` matches to `!boolean_expression` val (expression, negativeExpression) = if (expressionText.startsWith('!')) { expressionText.substring(1) to expressionText } else { expressionText to getNegativeExpression(expressionAstNode, expressionText) } val letter = getLetter(expressionToToken, expression) tokenToExpression["!$letter"] = negativeExpression tokenToExpression[letter] = expression } /** * Replaces registered expressions in provided expression * * @param fullExpression full boolean expression in kotlin * @return full expression in jbool format */ fun replaceExpressions(fullExpression: String): String { var resultExpression = fullExpression expressionToToken.keys .sortedByDescending { it.length } .forEach { refExpr -> resultExpression = resultExpression.replace(refExpr, expressionToToken.getValue(refExpr)) } return resultExpression } /** * Restores full expression by replacing tokens and restoring the order * * @param fullExpression full boolean expression in jbool format * @return full boolean expression in kotlin */ fun restoreFullExpression(fullExpression: String): String { // restore order var resultExpression = fullExpression tokenToOrderedToken.values.forEachIndexed { index, value -> resultExpression = resultExpression.replace(value, "%${index + 1}\$s") } resultExpression = resultExpression.format(args = tokenToOrderedToken.keys.toTypedArray()) // restore expression tokenToExpression.keys.forEachIndexed { index, value -> resultExpression = resultExpression.replace(value, "%${index + 1}\$s") } resultExpression = resultExpression.format(args = tokenToExpression.values.toTypedArray()) return resultExpression } private fun getLetter(letters: HashMap, key: String) = letters .computeIfAbsent(key) { ('A'.code + letters.size).toChar().toString() } private fun getNegativeExpression(expressionAstNode: ASTNode, expression: String): String = if (expressionAstNode.elementType == BINARY_EXPRESSION) { val operation = (expressionAstNode.psi as KtBinaryExpression).operationReference.text logicalInfixMethodMapping[operation]?.let { expression.replace(operation, it) } ?: "!($expression)" } else { "!$expression" } } companion object { const val NAME_ID = "boolean-expressions-rule" private fun allRules(): RuleList { val rules: MutableList> = ArrayList(RulesHelper.simplifyRules().rules) rules.add(DeMorgan()) rules.add(DistributiveLaw()) return RuleList(rules) } } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/BracesInConditionalsAndLoopsRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter3 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.NO_BRACES_IN_CONDITIONALS_AND_LOOPS import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.findChildrenMatching import com.saveourtool.diktat.ruleset.utils.isPartOfComment import com.saveourtool.diktat.ruleset.utils.isSingleLineIfElse import com.saveourtool.diktat.ruleset.utils.loopType import com.saveourtool.diktat.ruleset.utils.prevSibling import org.jetbrains.kotlin.KtNodeTypes.BLOCK import org.jetbrains.kotlin.KtNodeTypes.BLOCK_CODE_FRAGMENT import org.jetbrains.kotlin.KtNodeTypes.BODY import org.jetbrains.kotlin.KtNodeTypes.CALL_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.ELSE import org.jetbrains.kotlin.KtNodeTypes.IF import org.jetbrains.kotlin.KtNodeTypes.LAMBDA_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.REFERENCE_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.SAFE_ACCESS_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.THEN import org.jetbrains.kotlin.KtNodeTypes.WHEN import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.CompositeElement import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl import org.jetbrains.kotlin.lexer.KtTokens.DO_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.ELSE_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.IF_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.LBRACE import org.jetbrains.kotlin.lexer.KtTokens.RBRACE import org.jetbrains.kotlin.lexer.KtTokens.WHILE_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.WHITE_SPACE import org.jetbrains.kotlin.psi.KtBlockExpression import org.jetbrains.kotlin.psi.KtElement import org.jetbrains.kotlin.psi.KtIfExpression import org.jetbrains.kotlin.psi.KtLoopExpression import org.jetbrains.kotlin.psi.KtWhenExpression import org.jetbrains.kotlin.psi.psiUtil.astReplace import org.jetbrains.kotlin.psi.psiUtil.children /** * Rule that checks that all conditionals and loops have braces. */ class BracesInConditionalsAndLoopsRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(NO_BRACES_IN_CONDITIONALS_AND_LOOPS) ) { override fun logic(node: ASTNode) { when (node.elementType) { IF -> checkIfNode(node) WHEN -> checkWhenBranches(node) in loopType -> checkLoop(node) else -> return } } /** * Check braces in if-else statements. Check for both IF and ELSE needs to be done in one method to discover single-line if-else statements correctly. */ @Suppress( "ForbiddenComment", "UnsafeCallOnNullableType", "ComplexMethod", "TOO_LONG_FUNCTION" ) private fun checkIfNode(node: ASTNode) { val ifPsi = node.psi as KtIfExpression val thenNode = ifPsi.then?.node val elseKeyword = ifPsi.elseKeyword val elseNode = ifPsi.`else`?.node val indent = node.findIndentBeforeNode() if (node.isSingleLineIfElse()) { return } if (thenNode?.elementType != BLOCK) { NO_BRACES_IN_CONDITIONALS_AND_LOOPS.warnAndFix(configRules, emitWarn, isFixMode, "IF", (thenNode?.prevSibling { it.elementType == IF_KEYWORD } ?: node).startOffset, node) { thenNode?.run { (psi as KtElement).replaceWithBlock(indent) if (elseNode != null && elseKeyword != null) { node.replaceChild(elseKeyword.prevSibling.node, PsiWhiteSpaceImpl(" ")) } } ?: run { node.insertEmptyBlockInsideThenNode(indent) } } } if (elseKeyword != null && elseNode?.elementType != IF && elseNode?.elementType != BLOCK) { // Looking for scope functions, for which we won't trigger val callAndSafeAccessExpressionChildren = elseNode?.findChildrenMatching { it.elementType == CALL_EXPRESSION || it.elementType == SAFE_ACCESS_EXPRESSION } val scopeFunctionChildren = callAndSafeAccessExpressionChildren?.flatMap { it.children() }?.filter { it.elementType == REFERENCE_EXPRESSION } val isNodeHaveScopeFunctionChildren = scopeFunctionChildren?.any { it.text in scopeFunctions } if (isNodeHaveScopeFunctionChildren == true) { return } NO_BRACES_IN_CONDITIONALS_AND_LOOPS.warnAndFix(configRules, emitWarn, isFixMode, "ELSE", (elseNode?.treeParent?.prevSibling { it.elementType == ELSE_KEYWORD } ?: node).startOffset, node) { elseNode?.run { (psi as KtElement).replaceWithBlock(indent) } ?: run { // `else` can have empty body e.g. when there is a semicolon after: `else ;` node.insertEmptyBlockInsideElseNode(indent) } } } } private fun ASTNode.insertEmptyBlockInsideThenNode(indent: Int) { val ifPsi = psi as KtIfExpression val elseKeyword = ifPsi.elseKeyword val emptyThenNode = findChildByType(THEN) emptyThenNode?.findChildByType(BLOCK_CODE_FRAGMENT) ?: run { val whiteSpacesAfterCondition = ifPsi.rightParenthesis?.node?.treeNext whiteSpacesAfterCondition?.let { replaceChild(it, PsiWhiteSpaceImpl(" ")) } emptyThenNode?.insertEmptyBlock(indent) elseKeyword?.let { addChild(PsiWhiteSpaceImpl(" "), elseKeyword.node) } } } private fun ASTNode.insertEmptyBlockInsideElseNode(indent: Int) { val ifPsi = psi as KtIfExpression val elseKeyword = ifPsi.elseKeyword val emptyElseNode = findChildByType(ELSE) emptyElseNode?.findChildByType(BLOCK_CODE_FRAGMENT) ?: run { val whiteSpacesAfterElseKeyword = elseKeyword?.node?.treeNext whiteSpacesAfterElseKeyword?.let { replaceChild(it, PsiWhiteSpaceImpl(" ")) } emptyElseNode?.insertEmptyBlock(indent) } } @Suppress("UnsafeCallOnNullableType") private fun checkLoop(node: ASTNode) { val loopBody = (node.psi as KtLoopExpression).body val loopBodyNode = loopBody?.node if (loopBodyNode == null || loopBodyNode.elementType != BLOCK) { NO_BRACES_IN_CONDITIONALS_AND_LOOPS.warnAndFix(configRules, emitWarn, isFixMode, node.elementType.toString(), node.startOffset, node) { // fixme proper way to calculate indent? or get step size (instead of hardcoded 4) val indent = node.findIndentBeforeNode() loopBody?.run { replaceWithBlock(indent) } ?: run { // this corresponds to do-while with empty body node.insertEmptyBlockInsideDoWhileNode(indent) } } } } private fun ASTNode.insertEmptyBlockInsideDoWhileNode(indent: Int) { findChildByType(BODY) ?: run { val doKeyword = findChildByType(DO_KEYWORD) val whileKeyword = findChildByType(WHILE_KEYWORD) val whiteSpacesAfterDoKeyword = doKeyword?.treeNext addChild(CompositeElement(BODY), whileKeyword) val emptyWhenNode = findChildByType(BODY) whiteSpacesAfterDoKeyword?.let { replaceChild(it, PsiWhiteSpaceImpl(" ")) } emptyWhenNode?.insertEmptyBlock(indent) addChild(PsiWhiteSpaceImpl(" "), whileKeyword) } } private fun ASTNode.findIndentBeforeNode(): Int { val isElseIfStatement = treeParent.elementType == ELSE val primaryIfNode = if (isElseIfStatement) treeParent.treeParent else this val indentNode = if (primaryIfNode.treeParent?.treeParent?.treeParent?.elementType == LAMBDA_EXPRESSION) { primaryIfNode.treeParent.prevSibling { it.elementType == WHITE_SPACE } } else { primaryIfNode.prevSibling { it.elementType == WHITE_SPACE } } return indentNode ?.text ?.lines() ?.last() ?.count { it == ' ' } ?: 0 } @Suppress("UnsafeCallOnNullableType") private fun checkWhenBranches(node: ASTNode) { (node.psi as KtWhenExpression) .entries .asSequence() .filter { it.expression != null && it.expression!!.node.elementType == BLOCK } .map { it.expression as KtBlockExpression } .filter { block -> block.statements.size == 1 && block.findChildrenMatching { it.isPartOfComment() }.isEmpty() } .forEach { block -> NO_BRACES_IN_CONDITIONALS_AND_LOOPS.warnAndFix(configRules, emitWarn, isFixMode, "WHEN", block.node.startOffset, block.node) { block.astReplace(block.firstStatement!!.node.psi) } } } private fun KtElement.replaceWithBlock(indent: Int) { this.astReplace(KtBlockExpression( "{\n${" ".repeat(indent + INDENT_STEP)}$text\n${" ".repeat(indent)}}" )) } private fun ASTNode.insertEmptyBlock(indent: Int) { val emptyBlock = CompositeElement(BLOCK_CODE_FRAGMENT) addChild(emptyBlock, null) emptyBlock.addChild(LeafPsiElement(LBRACE, "{")) emptyBlock.addChild(PsiWhiteSpaceImpl("\n${" ".repeat(indent)}")) emptyBlock.addChild(LeafPsiElement(RBRACE, "}")) } companion object { private const val INDENT_STEP = 4 const val NAME_ID = "races-rule" private val scopeFunctions = listOf("let", "run", "with", "apply", "also") } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/ClassLikeStructuresOrderRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter3 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.BLANK_LINE_BETWEEN_PROPERTIES import com.saveourtool.diktat.ruleset.constants.Warnings.WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.* import com.saveourtool.diktat.ruleset.utils.isPartOfComment import com.saveourtool.diktat.ruleset.utils.nextSibling import com.saveourtool.diktat.ruleset.utils.parent import com.saveourtool.diktat.ruleset.utils.prevSibling import org.jetbrains.kotlin.KtNodeTypes.CLASS import org.jetbrains.kotlin.KtNodeTypes.CLASS_BODY import org.jetbrains.kotlin.KtNodeTypes.CLASS_INITIALIZER import org.jetbrains.kotlin.KtNodeTypes.ENUM_ENTRY import org.jetbrains.kotlin.KtNodeTypes.FUN import org.jetbrains.kotlin.KtNodeTypes.OBJECT_DECLARATION import org.jetbrains.kotlin.KtNodeTypes.PROPERTY import org.jetbrains.kotlin.KtNodeTypes.REFERENCE_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.SECONDARY_CONSTRUCTOR import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType import org.jetbrains.kotlin.com.intellij.psi.tree.TokenSet import org.jetbrains.kotlin.kdoc.lexer.KDocTokens.KDOC import org.jetbrains.kotlin.lexer.KtTokens.BLOCK_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.COMPANION_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.CONST_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.EOL_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.IDENTIFIER import org.jetbrains.kotlin.lexer.KtTokens.LATEINIT_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.LBRACE import org.jetbrains.kotlin.lexer.KtTokens.PRIVATE_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.RBRACE import org.jetbrains.kotlin.lexer.KtTokens.WHITE_SPACE import org.jetbrains.kotlin.psi.KtClassBody import org.jetbrains.kotlin.psi.KtProperty import org.jetbrains.kotlin.psi.psiUtil.children import org.jetbrains.kotlin.psi.psiUtil.parents import org.jetbrains.kotlin.psi.psiUtil.siblings /** * Rule that checks order of declarations inside classes, interfaces and objects. */ class ClassLikeStructuresOrderRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(BLANK_LINE_BETWEEN_PROPERTIES, WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES) ) { override fun logic(node: ASTNode) { if (node.elementType == CLASS_BODY) { checkDeclarationsOrderInClass(node) } else if (node.elementType == PROPERTY) { checkNewLinesBeforeProperty(node) } } private fun checkDeclarationsOrderInClass(node: ASTNode) { val allProperties = AllProperties.fromClassBody(node) val initBlocks = node.getAllChildrenWithType(CLASS_INITIALIZER) val constructors = node.getAllChildrenWithType(SECONDARY_CONSTRUCTOR) val methods = node.getAllChildrenWithType(FUN) val (usedClasses, unusedClasses) = node.getUsedAndUnusedClasses() val (companionObject, objects) = node.getAllChildrenWithType(OBJECT_DECLARATION) .partition { it.hasModifier(COMPANION_KEYWORD) } val blocks = Blocks( (node.psi as KtClassBody).enumEntries.map { it.node }, allProperties, objects, initBlocks, constructors, methods, usedClasses, companionObject, unusedClasses ) .allBlockFlattened() .map { astNode -> listOf(astNode) + astNode.siblings(false) .takeWhile { it.elementType == WHITE_SPACE || it.isPartOfComment() } .toList() } node.checkAndReorderBlocks(blocks) } @Suppress("UnsafeCallOnNullableType", "CyclomaticComplexMethod") private fun checkNewLinesBeforeProperty(node: ASTNode) { // checking only top-level and class-level properties if (node.treeParent.elementType != CLASS_BODY) { return } val previousProperty = node.prevSibling { it.elementType == PROPERTY } ?: return val nearComment = node.findChildByType(TokenSet.create(KDOC, EOL_COMMENT, BLOCK_COMMENT)) val prevComment = nearComment?.prevSibling() val nextComment = nearComment?.nextSibling() val isCorrectEolComment = (prevComment == null || !prevComment.textContains('\n')) && nextComment == null val hasCommentBefore = node .findChildByType(TokenSet.create(KDOC, EOL_COMMENT, BLOCK_COMMENT)) ?.isFollowedByNewline() ?: false val hasAnnotationsBefore = (node.psi as KtProperty) .annotationEntries .any { it.node.isFollowedByNewline() } val hasCustomAccessors = (node.psi as KtProperty).accessors.isNotEmpty() || (previousProperty.psi as KtProperty).accessors.isNotEmpty() val whiteSpaceBefore = previousProperty.nextSibling { it.elementType == WHITE_SPACE } ?: return val isBlankLineRequired = (!isCorrectEolComment && hasCommentBefore) || hasAnnotationsBefore || hasCustomAccessors val numRequiredNewLines = 1 + (if (isBlankLineRequired) 1 else 0) val actualNewLines = whiteSpaceBefore.text.count { it == '\n' } // for some cases (now - if this or previous property has custom accessors), blank line is allowed before it if (!hasCustomAccessors && actualNewLines != numRequiredNewLines || hasCustomAccessors && actualNewLines > numRequiredNewLines) { BLANK_LINE_BETWEEN_PROPERTIES.warnAndFix(configRules, emitWarn, isFixMode, node.getIdentifierName()?.text ?: node.text, node.startOffset, node) { whiteSpaceBefore.leaveExactlyNumNewLines(numRequiredNewLines) } } } /** * Returns nested classes grouped by whether they are used inside [this] file. * [this] ASTNode should have elementType [CLASS_BODY] */ private fun ASTNode.getUsedAndUnusedClasses() = getAllChildrenWithType(CLASS) .partition { classNode -> classNode.getIdentifierName()?.let { identifierNode -> parents() .last() .findAllDescendantsWithSpecificType(REFERENCE_EXPRESSION) .any { ref -> ref.parent { it == classNode } == null && ref.text.contains(identifierNode.text) } } ?: false } /** * Checks whether all class elements in [this] node are correctly ordered and reorders them in fix mode. * [this] ASTNode should have elementType [CLASS_BODY] * * @param blocks list of class elements with leading whitespaces and comments */ @Suppress("UnsafeCallOnNullableType") private fun ASTNode.checkAndReorderBlocks(blocks: List>) { val classChildren = this.children().filter { it.elementType in childrenTypes }.toList() check(blocks.size == classChildren.size) { StringBuilder().apply { append("`classChildren` has a size of ${classChildren.size} while `blocks` has a size of ${blocks.size}$NEWLINE") append("`blocks`:$NEWLINE") blocks.forEachIndexed { index, block -> append("\t$index: ${block.firstOrNull()?.text}$NEWLINE") } append("`classChildren`:$NEWLINE") classChildren.forEachIndexed { index, child -> append("\t$index: ${child.text}$NEWLINE") } } } if (classChildren != blocks.map { it.first() }) { blocks.filterIndexed { index, pair -> classChildren[index] != pair.first() } .forEach { listOfChildren -> val astNode = listOfChildren.first() WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES.warnAndFix(configRules, emitWarn, isFixMode, "${astNode.elementType}: ${astNode.findChildByType(IDENTIFIER)?.text ?: astNode.text}", astNode.startOffset, astNode) { removeRange(findChildByType(LBRACE)!!.treeNext, findChildByType(RBRACE)!!) blocks.reversed() .forEach { bodyChild -> bodyChild.forEach { this.addChild(it, this.children().take(2).last()) } } // Add newline before the closing `}`. All other newlines will be properly formatted by `NewlinesRule`. this.addChild(PsiWhiteSpaceImpl("\n"), this.lastChildNode) } } } } /** * Data class containing different groups of properties in file * * @property loggers loggers (for example, properties called `log` or `logger`) * @property constProperties `const val`s * @property properties all other properties * @property lateInitProperties `lateinit var`s */ private data class AllProperties( val loggers: List, val constProperties: List, val properties: List, val lateInitProperties: List ) { companion object { /** * Create [AllProperties] wrapper from node with type [CLASS_BODY] * * @param node an ASTNode with type [CLASS_BODY] * @return an instance of [AllProperties] */ @Suppress("UnsafeCallOnNullableType") fun fromClassBody(node: ASTNode): AllProperties { val allProperties = node.getAllChildrenWithType(PROPERTY) val constProperties = allProperties.filterByModifier(CONST_KEYWORD) val lateInitProperties = allProperties.filterByModifier(LATEINIT_KEYWORD) val referencesFromSameScope = allProperties.mapNotNull { it.getIdentifierName()?.text } val loggers = allProperties.filterByModifier(PRIVATE_KEYWORD) .filterNot { astNode -> /* * A `const` field named "logger" is unlikely to be a logger. */ astNode in constProperties } .filterNot { astNode -> /* * A `lateinit` field named "logger" is unlikely to be a logger. */ astNode in lateInitProperties } .filter { astNode -> astNode.getIdentifierName()?.text?.matches(loggerPropertyRegex) ?: false } .let { getLoggerDependencyNames(it) } .filter { (_, dependencyReferences) -> dependencyReferences.all { it !in referencesFromSameScope } } .keys .toList() val properties = allProperties.filter { it !in lateInitProperties && it !in loggers && it !in constProperties } return AllProperties(loggers, constProperties, properties, lateInitProperties) } @Suppress("TYPE_ALIAS") private fun getLoggerDependencyNames(loggers: List): Map> = loggers.map { astNode -> astNode to astNode.findAllDescendantsWithSpecificType(REFERENCE_EXPRESSION, false) }.associate { (astNode, possibleDependencies) -> astNode to possibleDependencies.map { it.text } } } } /** * @property enumEntries if this class is a enum class, list of its entries. Otherwise an empty list. * @property allProperties an instance of [AllProperties] * @property objects objects * @property initBlocks `init` blocks * @property constructors constructors * @property methods functions * @property usedClasses nested classes that are used in the enclosing class * @property companion `companion object`s * @property unusedClasses nested classes that are *not* used in the enclosing class */ private data class Blocks(val enumEntries: List, val allProperties: AllProperties, val objects: List, val initBlocks: List, val constructors: List, val methods: List, val usedClasses: List, val companion: List, val unusedClasses: List ) { init { require(companion.size in 0..1) { "There is more than one companion object in class" } } /** * @return all groups of structures in the class */ fun allBlocks() = with(allProperties) { listOf(enumEntries, loggers, constProperties, properties, lateInitProperties, objects, initBlocks, constructors, methods, usedClasses, companion, unusedClasses) } /** * @return all blocks as a flattened list of [ASTNode]s */ fun allBlockFlattened() = allBlocks().flatten() } companion object { const val NAME_ID = "class-like-structures" private val childrenTypes = listOf(PROPERTY, CLASS, CLASS_INITIALIZER, SECONDARY_CONSTRUCTOR, FUN, OBJECT_DECLARATION, ENUM_ENTRY) } } private fun Iterable.filterByModifier(modifier: IElementType) = filter { it.findLeafWithSpecificType(modifier) != null } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/CollapseIfStatementsRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter3 import com.saveourtool.diktat.common.config.rules.RuleConfiguration import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.common.config.rules.getRuleConfig import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.KotlinParser import org.jetbrains.kotlin.KtNodeTypes.BINARY_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.IF import org.jetbrains.kotlin.KtNodeTypes.OPERATION_REFERENCE import org.jetbrains.kotlin.KtNodeTypes.THEN import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.lexer.KtTokens.BLOCK_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.EOL_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.LBRACE import org.jetbrains.kotlin.lexer.KtTokens.LPAR import org.jetbrains.kotlin.lexer.KtTokens.RBRACE import org.jetbrains.kotlin.lexer.KtTokens.RPAR import org.jetbrains.kotlin.lexer.KtTokens.WHITE_SPACE import org.jetbrains.kotlin.psi.KtBlockExpression import org.jetbrains.kotlin.psi.KtIfExpression import org.jetbrains.kotlin.psi.psiUtil.children import java.util.Stack typealias PlaceOfWarningForCurrentNode = Pair /** * Rule for redundant nested if-statements, which could be collapsed into a single one */ class CollapseIfStatementsRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf( Warnings.COLLAPSE_IF_STATEMENTS ) ) { private val configuration by lazy { CollapseIfStatementsConfiguration( configRules.getRuleConfig(Warnings.COLLAPSE_IF_STATEMENTS)?.configuration ?: emptyMap() ) } // We hold the warnings, which we raised, since in case of multi nested if-statement, // there are could be several identical warning for one line private val listOfWarnings: MutableSet = mutableSetOf() override fun logic(node: ASTNode) { if (node.elementType == IF) { process(node) } } private fun process(node: ASTNode) { val startCollapseFromLevel = configuration.startCollapseFromNestedLevel val listOfNestedNodes: Stack = Stack() var nestedIfNode = findNestedIf(node) while (nestedIfNode != null) { listOfNestedNodes.push(nestedIfNode) nestedIfNode = findNestedIf(nestedIfNode) } val nestedLevel = listOfNestedNodes.size + 1 if (nestedLevel < startCollapseFromLevel) { return } while (listOfNestedNodes.isNotEmpty()) { val currNode = listOfNestedNodes.pop() // Since the external `if` statement is not the direct parent, // we need multiple steps to take the required one // BLOCK -> THEN -> IF val currParentNode = currNode.treeParent.treeParent.treeParent if (listOfWarnings.add(currNode.startOffset to currNode)) { Warnings.COLLAPSE_IF_STATEMENTS.warnAndFix( configRules, emitWarn, isFixMode, "avoid using redundant nested if-statements", currNode.startOffset, currNode ) { collapse(currParentNode, currNode) } } } } private fun findNestedIf(parentNode: ASTNode): ASTNode? { val parentThenNode = (parentNode.psi as KtIfExpression).then?.node ?: return null val nestedIfNode = parentThenNode.findChildByType(IF) ?: return null // We won't collapse if-statements, if some of them have `else` node if ((parentNode.psi as KtIfExpression).`else` != null || (nestedIfNode.psi as KtIfExpression).`else` != null) { return null } // We monitor which types of nodes are followed before and after nested `if` // and we allow only a limited number of types to pass through. // Otherwise discovered `if` is not nested // We don't expect KDOC in `if-statements`, since it's a bad practise, and such code by meaning of our // code analyzer is invalid // However, if in some case we will hit the KDOC, than we won't collapse statements val listOfNodesBeforeNestedIf = parentThenNode.getChildren(null).takeWhile { it.elementType != IF } val listOfNodesAfterNestedIf = parentThenNode.getChildren(null).takeLastWhile { it != parentThenNode.findChildByType(IF) } val allowedTypes = listOf(LBRACE, WHITE_SPACE, RBRACE, BLOCK_COMMENT, EOL_COMMENT) if (listOfNodesBeforeNestedIf.any { it.elementType !in allowedTypes } || listOfNodesAfterNestedIf.any { it.elementType !in allowedTypes }) { return null } return nestedIfNode } private fun takeCommentsBeforeNestedIf(node: ASTNode): List { val thenNode = (node.psi as KtIfExpression).then?.node return thenNode?.children() ?.takeWhile { it.elementType != IF } ?.filter { it.elementType == EOL_COMMENT || it.elementType == BLOCK_COMMENT } ?.toList() ?: emptyList() } private fun collapse(parentNode: ASTNode, nestedNode: ASTNode) { collapseConditions(parentNode, nestedNode) collapseThenBlocks(parentNode, nestedNode) } private fun collapseConditions(parentNode: ASTNode, nestedNode: ASTNode) { // If there are comments before nested if, we will move them into parent condition val comments = takeCommentsBeforeNestedIf(parentNode) val commentsText = if (comments.isNotEmpty()) { comments.joinToString(prefix = "\n", postfix = "\n", separator = "\n") { it.text } } else { " " } // Merge parent and nested conditions val parentConditionText = extractConditions(parentNode) val nestedCondition = (nestedNode.psi as KtIfExpression).condition val nestedConditionText = extractConditions(nestedNode) // If the nested condition is compound, // we need to put it to the brackets, according algebra of logic val mergeCondition = if (nestedCondition?.node?.elementType == BINARY_EXPRESSION && nestedCondition?.node?.findChildByType(OPERATION_REFERENCE)?.text == "||" ) { "if ($parentConditionText &&$commentsText($nestedConditionText)) {}" } else { "if ($parentConditionText &&$commentsText$nestedConditionText) {}" } val newParentIfNode = KotlinParser().createNode(mergeCondition) // Remove THEN block newParentIfNode.removeChild(newParentIfNode.lastChildNode) // Remove old `if` from parent parentNode.removeRange(parentNode.firstChildNode, parentNode.findChildByType(THEN)) // Add to parent all child from new `if` node var addAfter = parentNode.firstChildNode newParentIfNode.getChildren(null).forEachIndexed { index, child -> parentNode.addChild(child, addAfter) addAfter = parentNode.children().drop(index + 1).first() } } // If condition contains comments, we need additional actions // Because of `node.condition` will ignore comments private fun extractConditions(node: ASTNode): String { val condition = node.getChildren(null) .takeLastWhile { it != node.findChildByType(LPAR) } .takeWhile { it != node.findChildByType(RPAR) } return condition.joinToString("") { it.text } } private fun collapseThenBlocks(parentNode: ASTNode, nestedNode: ASTNode) { // Remove comments from parent node, since we already moved them into parent condition val comments = takeCommentsBeforeNestedIf(parentNode) comments.forEach { if (it.treeNext.elementType == WHITE_SPACE && it.treePrev.elementType == WHITE_SPACE) { parentNode.removeChild(it.treePrev) } parentNode.removeChild(it) } // Merge parent and nested `THEN` blocks val nestedThenNode = (nestedNode.psi as KtIfExpression).then val nestedContent = (nestedThenNode as KtBlockExpression).children().toMutableList() // Remove {, }, and white spaces repeat(2) { val firstElType = nestedContent.first().elementType if (firstElType == WHITE_SPACE || firstElType == LBRACE) { nestedContent.removeFirst() } val lastElType = nestedContent.last().elementType if (lastElType == WHITE_SPACE || lastElType == RBRACE) { nestedContent.removeLast() } } val nestedThenText = nestedContent.joinToString("") { it.text } val newNestedNode = KotlinParser().createNode(nestedThenText).treeParent val parentThenNode = (parentNode.psi as KtIfExpression).then?.node newNestedNode.getChildren(null).forEach { parentThenNode?.addChild(it, nestedNode) } parentThenNode?.removeChild(nestedNode) } /** * [RuleConfiguration] for configuration */ class CollapseIfStatementsConfiguration(config: Map) : RuleConfiguration(config) { /** * Collapse statements only if nested level more than this value */ val startCollapseFromNestedLevel = config["startCollapseFromNestedLevel"]?.toInt() ?: DEFAULT_NESTED_LEVEL } companion object { private const val DEFAULT_NESTED_LEVEL = 2 const val NAME_ID = "collapse-if" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/ConsecutiveSpacesRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter3 import com.saveourtool.diktat.common.config.rules.RuleConfiguration import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.common.config.rules.getRuleConfig import com.saveourtool.diktat.ruleset.constants.Warnings.TOO_MANY_CONSECUTIVE_SPACES import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.isWhiteSpaceWithNewline import com.saveourtool.diktat.ruleset.utils.parent import org.jetbrains.kotlin.KtNodeTypes.ENUM_ENTRY import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafElement import org.jetbrains.kotlin.lexer.KtTokens.EOL_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.WHITE_SPACE /** * This visitor covers recommendation 3.8 of Huawei code style. It covers following recommendations: * 1) No spaces should be inserted for horizontal alignment * 2) If saveInitialFormattingForEnums is true then white spaces in enums will not be affected * */ class ConsecutiveSpacesRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(TOO_MANY_CONSECUTIVE_SPACES), ) { override fun logic(node: ASTNode) { val configuration = TooManySpacesRuleConfiguration( configRules.getRuleConfig(TOO_MANY_CONSECUTIVE_SPACES)?.configuration ?: emptyMap()) if (node.elementType == WHITE_SPACE) { checkWhiteSpace(node, configuration) } } private fun checkWhiteSpace(node: ASTNode, configuration: TooManySpacesRuleConfiguration) { if (configuration.enumInitialFormatting) { checkWhiteSpaceEnum(node, configuration) } else { squeezeSpacesToOne(node, configuration) } } private fun checkWhiteSpaceEnum(node: ASTNode, configuration: TooManySpacesRuleConfiguration) { val isInEnum = isWhitespaceInEnum(node) if (!isInEnum) { squeezeSpacesToOne(node, configuration) } } private fun isWhitespaceInEnum(node: ASTNode): Boolean = node.parent(ENUM_ENTRY) != null private fun squeezeSpacesToOne(node: ASTNode, configuration: TooManySpacesRuleConfiguration) { val spaces = node.textLength if (spaces > configuration.numberOfSpaces && !node.isWhiteSpaceWithNewline() && !node.hasEolComment()) { TOO_MANY_CONSECUTIVE_SPACES.warnAndFix(configRules, emitWarn, isFixMode, "found: $spaces. need to be: ${configuration.numberOfSpaces}", node.startOffset, node) { node.squeezeSpaces() } } } private fun ASTNode.hasEolComment(): Boolean = this.treeNext.elementType == EOL_COMMENT private fun ASTNode.squeezeSpaces() = (this as LeafElement).rawReplaceWithText(" ") /** * [RuleConfiguration] for consecutive spaces */ class TooManySpacesRuleConfiguration(config: Map) : RuleConfiguration(config) { /** * Maximum allowed number of consecutive spaces (not counting indentation) */ val numberOfSpaces = config["maxSpaces"]?.toIntOrNull() ?: MAX_SPACES /** * Whether formatting for enums should be kept without checking */ val enumInitialFormatting = config["saveInitialFormattingForEnums"]?.toBoolean() ?: false } companion object { private const val MAX_SPACES = 1 const val NAME_ID = "too-many-spaces" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/DebugPrintRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter3 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.DiktatRule import org.jetbrains.kotlin.KtNodeTypes import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.tree.TokenSet import org.jetbrains.kotlin.lexer.KtTokens /** * This rule detects `print()` or `println()`. * Assumption that it's a debug logging * */ class DebugPrintRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(Warnings.DEBUG_PRINT) ) { override fun logic(node: ASTNode) { checkPrintln(node) checkJsConsole(node) } // check kotlin.io.print()/kotlin.io.println() private fun checkPrintln(node: ASTNode) { if (node.elementType == KtNodeTypes.CALL_EXPRESSION) { val referenceExpression = node.findChildByType(KtNodeTypes.REFERENCE_EXPRESSION)?.text val valueArgumentList = node.findChildByType(KtNodeTypes.VALUE_ARGUMENT_LIST) if (referenceExpression in setOf("print", "println") && node.treePrev?.elementType != KtTokens.DOT && valueArgumentList?.getChildren(TokenSet.create(KtNodeTypes.VALUE_ARGUMENT))?.size?.let { it <= 1 } == true && node.findChildByType(KtNodeTypes.LAMBDA_ARGUMENT) == null) { Warnings.DEBUG_PRINT.warn( configRules, emitWarn, "found $referenceExpression()", node.startOffset, node, ) } } } // check kotlin.js.console.*() private fun checkJsConsole(node: ASTNode) { if (node.elementType == KtNodeTypes.DOT_QUALIFIED_EXPRESSION) { val isConsole = node.firstChildNode.let { referenceExpression -> referenceExpression.elementType == KtNodeTypes.REFERENCE_EXPRESSION && referenceExpression.firstChildNode.let { it.elementType == KtTokens.IDENTIFIER && it.text == "console" } } if (isConsole) { val logMethod = node.lastChildNode .takeIf { it.elementType == KtNodeTypes.CALL_EXPRESSION } ?.takeIf { it.findChildByType(KtNodeTypes.LAMBDA_ARGUMENT) == null } ?.firstChildNode ?.takeIf { it.elementType == KtNodeTypes.REFERENCE_EXPRESSION } ?.text if (logMethod in setOf("error", "info", "log", "warn")) { Warnings.DEBUG_PRINT.warn( configRules, emitWarn, "found console.$logMethod()", node.startOffset, node, ) } } } } internal companion object { const val NAME_ID = "debug-print" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/EmptyBlock.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter3 import com.saveourtool.diktat.common.config.rules.RuleConfiguration import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.common.config.rules.getRuleConfig import com.saveourtool.diktat.ruleset.constants.Warnings.EMPTY_BLOCK_STRUCTURE_ERROR import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.* import org.jetbrains.kotlin.KtNodeTypes import org.jetbrains.kotlin.KtNodeTypes.CALL_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.DOT_QUALIFIED_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.FUNCTION_LITERAL import org.jetbrains.kotlin.KtNodeTypes.LAMBDA_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.VALUE_ARGUMENT_LIST import org.jetbrains.kotlin.KtNodeTypes.VALUE_PARAMETER import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl import org.jetbrains.kotlin.lexer.KtTokens.IDENTIFIER import org.jetbrains.kotlin.lexer.KtTokens.RBRACE import org.jetbrains.kotlin.lexer.KtTokens.WHITE_SPACE import org.jetbrains.kotlin.psi.psiUtil.parents /** * Rule that checks if empty code blocks (`{ }`) are used and checks their formatting. */ class EmptyBlock(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(EMPTY_BLOCK_STRUCTURE_ERROR) ) { override fun logic(node: ASTNode) { val configuration = EmptyBlockStyleConfiguration( configRules.getRuleConfig(EMPTY_BLOCK_STRUCTURE_ERROR)?.configuration ?: emptyMap() ) searchNode(node, configuration) } private fun searchNode(node: ASTNode, configuration: EmptyBlockStyleConfiguration) { val newNode = node.findLBrace()?.treeParent ?: return if (!isAllowedEmptyBlock(newNode) && newNode.isBlockEmpty()) { checkEmptyBlock(newNode, configuration) } } private fun isNewLine(node: ASTNode) = node.findChildByType(WHITE_SPACE)?.text?.contains("\n") ?: false private fun isAllowedEmptyBlock(node: ASTNode) = node.treeParent.isOverridden() || isAnonymousSamClass(node) || isLambdaUsedAsFunction(node) || isKotlinLogging(node) @Suppress("UnsafeCallOnNullableType", "TOO_LONG_FUNCTION") private fun checkEmptyBlock(node: ASTNode, configuration: EmptyBlockStyleConfiguration) { if (!configuration.emptyBlockExist) { EMPTY_BLOCK_STRUCTURE_ERROR.warn(configRules, emitWarn, "empty blocks are forbidden unless it is function with override keyword", node.startOffset, node) } else { node.findParentNodeWithSpecificType(KtNodeTypes.LAMBDA_ARGUMENT)?.let { // Lambda body is always has a BLOCK -> run { } - (LBRACE, WHITE_SPACE, BLOCK "", RBRACE) if (isNewLine(node)) { val freeText = "do not put newlines in empty lambda" EMPTY_BLOCK_STRUCTURE_ERROR.warnAndFix(configRules, emitWarn, isFixMode, freeText, node.startOffset, node) { val whiteSpaceNode = node.findChildByType(WHITE_SPACE) whiteSpaceNode?.let { node.replaceChild(whiteSpaceNode, PsiWhiteSpaceImpl(" ")) } } } return } val space = node.findChildByType(RBRACE)!!.treePrev if (configuration.emptyBlockNewline && !space.text.contains("\n")) { EMPTY_BLOCK_STRUCTURE_ERROR.warnAndFix(configRules, emitWarn, isFixMode, "different style for empty block", node.startOffset, node) { if (space.elementType == WHITE_SPACE) { (space.treeNext as LeafPsiElement).rawReplaceWithText("\n") } else { node.addChild(PsiWhiteSpaceImpl("\n"), space.treeNext) } } } else if (!configuration.emptyBlockNewline && space.text.contains("\n")) { EMPTY_BLOCK_STRUCTURE_ERROR.warnAndFix(configRules, emitWarn, isFixMode, "different style for empty block", node.startOffset, node) { node.removeChild(space) } } } } @Suppress("UnsafeCallOnNullableType") private fun isAnonymousSamClass(node: ASTNode): Boolean = if (node.elementType == FUNCTION_LITERAL && node.hasParent(CALL_EXPRESSION)) { // We are checking identifier because it is not class in AST, // SAM conversions are indistinguishable from lambdas. // So we just verify that identifier is in PascalCase val valueArgument = node.findParentNodeWithSpecificType(CALL_EXPRESSION)!! valueArgument.findLeafWithSpecificType(IDENTIFIER)?.text?.isPascalCase() ?: false } else { false } @Suppress("UnsafeCallOnNullableType") private fun isLambdaUsedAsFunction(node: ASTNode): Boolean { val parents = node.parents() return when { parents.any { it.elementType == CALL_EXPRESSION } -> { val callExpression = parents.find { it.elementType == CALL_EXPRESSION }!! // excepting cases like list.map { }. In this case call expression will not have value argument list // And in this case: Parser.parse({}, some, thing) it will have value argument list callExpression.hasChildOfType(VALUE_ARGUMENT_LIST) } parents.any { it.elementType == LAMBDA_EXPRESSION } -> { val lambdaExpression = parents.find { it.elementType == LAMBDA_EXPRESSION }!! // cases like A({}). Here Lambda expression is used as a value parameter. lambdaExpression.treeParent.elementType == VALUE_PARAMETER } else -> false } } private fun isKotlinLogging(node: ASTNode): Boolean = node.findParentNodeWithSpecificType(DOT_QUALIFIED_EXPRESSION) ?.text ?.replace(" ", "") .let { it == "KotlinLogging.logger{}" } /** * [RuleConfiguration] for empty blocks formatting */ class EmptyBlockStyleConfiguration(config: Map) : RuleConfiguration(config) { /** * Whether empty code blocks should be allowed */ val emptyBlockExist = config["allowEmptyBlocks"]?.toBoolean() ?: false /** * Whether a newline after `{` is required in an empty block */ val emptyBlockNewline = config["styleEmptyBlockWithNewline"]?.toBoolean() ?: true } companion object { const val NAME_ID = "empty-block-structure" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/EnumsSeparated.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter3 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.ENUMS_SEPARATED import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.AstNodePredicate import com.saveourtool.diktat.ruleset.utils.allSiblings import com.saveourtool.diktat.ruleset.utils.appendNewline import com.saveourtool.diktat.ruleset.utils.appendNewlineMergingWhiteSpace import com.saveourtool.diktat.ruleset.utils.getAllChildrenWithType import com.saveourtool.diktat.ruleset.utils.hasChildOfType import com.saveourtool.diktat.ruleset.utils.isClassEnum import com.saveourtool.diktat.ruleset.utils.isWhiteSpaceWithNewline import org.jetbrains.kotlin.KtNodeTypes.CLASS import org.jetbrains.kotlin.KtNodeTypes.CLASS_BODY import org.jetbrains.kotlin.KtNodeTypes.ENUM_ENTRY import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl import org.jetbrains.kotlin.lexer.KtTokens.BLOCK_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.COMMA import org.jetbrains.kotlin.lexer.KtTokens.EOL_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.IDENTIFIER import org.jetbrains.kotlin.lexer.KtTokens.LBRACE import org.jetbrains.kotlin.lexer.KtTokens.RBRACE import org.jetbrains.kotlin.lexer.KtTokens.SEMICOLON import org.jetbrains.kotlin.lexer.KtTokens.WHITE_SPACE /** * Rule that checks enum classes formatting */ class EnumsSeparated(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(ENUMS_SEPARATED), ) { override fun logic(node: ASTNode) { if (node.elementType == CLASS && node.hasChildOfType(CLASS_BODY) && node.isClassEnum()) { checkEnumEntry(node) } } // Fixme prefer enum classes if it is possible instead of variables @Suppress("UnsafeCallOnNullableType") private fun checkEnumEntry(node: ASTNode) { val enumEntries = node.findChildByType(CLASS_BODY)!!.getAllChildrenWithType(ENUM_ENTRY) if (enumEntries.isEmpty() || (isEnumSimple(enumEntries) && isEnumOneLine(enumEntries))) { return } enumEntries.forEach { enumEntry -> if (!enumEntry.treeNext.isWhiteSpaceWithNewline()) { ENUMS_SEPARATED.warnAndFix(configRules, emitWarn, isFixMode, "enum entries must end with a line break", enumEntry.startOffset, enumEntry) { enumEntry.appendNewline() } } } checkLastEnum(enumEntries.last()) } private fun isEnumOneLine(nodes: List) = nodes.dropLast(1).none { it.treeNext.isWhiteSpaceWithNewline() } private fun isEnumSimple(enumEntries: List): Boolean { enumEntries.forEach { node -> if (!simpleValue.containsAll(node.getChildren(null).map { it.elementType })) { return false } } return simpleEnum.containsAll(enumEntries .last() .allSiblings(withSelf = true) .map { it.elementType }) } @Suppress("UnsafeCallOnNullableType") private fun checkLastEnum(node: ASTNode) { if (!node.hasChildOfType(SEMICOLON)) { ENUMS_SEPARATED.warnAndFix(configRules, emitWarn, isFixMode, "enums must end with semicolon", node.startOffset, node) { node.addChild(LeafPsiElement(SEMICOLON, ";"), null) node.addChild(PsiWhiteSpaceImpl("\n"), node.findChildByType(SEMICOLON)!!) } } else if (!node.findChildByType(SEMICOLON)!!.treePrev.isWhiteSpaceWithNewline()) { ENUMS_SEPARATED.warnAndFix(configRules, emitWarn, isFixMode, "semicolon must be on a new line", node.startOffset, node) { node.appendNewlineMergingWhiteSpace(node.findChildByType(SEMICOLON)!!, node.findChildByType(SEMICOLON)!!) } } if (!node.hasChildOfType(COMMA)) { ENUMS_SEPARATED.warnAndFix(configRules, emitWarn, isFixMode, "last enum entry must end with a comma", node.startOffset, node) { val commaLocation = node.findChildByType(SEMICOLON)!!.findLatestTreePrevMatching { it.elementType !in setOf(EOL_COMMENT, BLOCK_COMMENT, WHITE_SPACE) } node.addChild(LeafPsiElement(COMMA, ","), commaLocation.treeNext) } } } private fun ASTNode.findLatestTreePrevMatching(predicate: AstNodePredicate): ASTNode { val result = this.treePrev return if (predicate(result)) result else result.findLatestTreePrevMatching(predicate) } companion object { const val NAME_ID = "enum-separated" private val simpleValue = listOf(IDENTIFIER, WHITE_SPACE, COMMA, SEMICOLON) private val simpleEnum = listOf(ENUM_ENTRY, WHITE_SPACE, LBRACE, RBRACE) } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/LineLength.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter3 import com.saveourtool.diktat.common.config.rules.RuleConfiguration import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.common.config.rules.getRuleConfig import com.saveourtool.diktat.ruleset.constants.Warnings.LONG_LINE import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.KotlinParser import com.saveourtool.diktat.ruleset.utils.appendNewlineMergingWhiteSpace import com.saveourtool.diktat.ruleset.utils.calculateLineColByOffset import com.saveourtool.diktat.ruleset.utils.countCodeLines import com.saveourtool.diktat.ruleset.utils.findAllNodesWithConditionOnLine import com.saveourtool.diktat.ruleset.utils.findChildAfter import com.saveourtool.diktat.ruleset.utils.findChildBefore import com.saveourtool.diktat.ruleset.utils.findChildrenMatching import com.saveourtool.diktat.ruleset.utils.findParentNodeWithSpecificType import com.saveourtool.diktat.ruleset.utils.getAllChildrenWithType import com.saveourtool.diktat.ruleset.utils.getFirstChildWithType import com.saveourtool.diktat.ruleset.utils.getLineNumber import com.saveourtool.diktat.ruleset.utils.hasChildOfType import com.saveourtool.diktat.ruleset.utils.isChildAfterAnother import com.saveourtool.diktat.ruleset.utils.isWhiteSpace import com.saveourtool.diktat.ruleset.utils.isWhiteSpaceWithNewline import com.saveourtool.diktat.ruleset.utils.nextSibling import com.saveourtool.diktat.ruleset.utils.prevSibling import io.github.oshai.kotlinlogging.KotlinLogging import org.jetbrains.kotlin.KtNodeTypes.BINARY_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.BLOCK import org.jetbrains.kotlin.KtNodeTypes.DOT_QUALIFIED_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.FUN import org.jetbrains.kotlin.KtNodeTypes.FUNCTION_LITERAL import org.jetbrains.kotlin.KtNodeTypes.IMPORT_LIST import org.jetbrains.kotlin.KtNodeTypes.OPERATION_REFERENCE import org.jetbrains.kotlin.KtNodeTypes.PACKAGE_DIRECTIVE import org.jetbrains.kotlin.KtNodeTypes.PARENTHESIZED import org.jetbrains.kotlin.KtNodeTypes.POSTFIX_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.PREFIX_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.PROPERTY import org.jetbrains.kotlin.KtNodeTypes.SAFE_ACCESS_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.STRING_TEMPLATE import org.jetbrains.kotlin.KtNodeTypes.VALUE_ARGUMENT_LIST import org.jetbrains.kotlin.KtNodeTypes.WHEN_CONDITION_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.WHEN_ENTRY import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType import org.jetbrains.kotlin.kdoc.lexer.KDocTokens.MARKDOWN_INLINE_LINK import org.jetbrains.kotlin.kdoc.lexer.KDocTokens.TEXT import org.jetbrains.kotlin.lexer.KtTokens.ANDAND import org.jetbrains.kotlin.lexer.KtTokens.ARROW import org.jetbrains.kotlin.lexer.KtTokens.COMMA import org.jetbrains.kotlin.lexer.KtTokens.DOT import org.jetbrains.kotlin.lexer.KtTokens.ELVIS import org.jetbrains.kotlin.lexer.KtTokens.EOL_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.EQ import org.jetbrains.kotlin.lexer.KtTokens.EQEQ import org.jetbrains.kotlin.lexer.KtTokens.EQEQEQ import org.jetbrains.kotlin.lexer.KtTokens.EXCL import org.jetbrains.kotlin.lexer.KtTokens.EXCLEQ import org.jetbrains.kotlin.lexer.KtTokens.EXCLEQEQEQ import org.jetbrains.kotlin.lexer.KtTokens.GT import org.jetbrains.kotlin.lexer.KtTokens.GTEQ import org.jetbrains.kotlin.lexer.KtTokens.LBRACE import org.jetbrains.kotlin.lexer.KtTokens.LPAR import org.jetbrains.kotlin.lexer.KtTokens.LT import org.jetbrains.kotlin.lexer.KtTokens.LTEQ import org.jetbrains.kotlin.lexer.KtTokens.OROR import org.jetbrains.kotlin.lexer.KtTokens.RBRACE import org.jetbrains.kotlin.lexer.KtTokens.RPAR import org.jetbrains.kotlin.lexer.KtTokens.SAFE_ACCESS import org.jetbrains.kotlin.lexer.KtTokens.WHITE_SPACE import org.jetbrains.kotlin.psi.stubs.elements.KtFileElementType import java.net.MalformedURLException import java.net.URL /** * The rule checks for lines in the file that exceed the maximum length. * Rule ignores URL in KDoc. This rule can also fix some particular corner cases. * This inspection can fix long binary expressions in condition inside `if`, * in property declarations and in single line functions. */ @Suppress("ForbiddenComment") class LineLength(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(LONG_LINE) ) { private val configuration by lazy { LineLengthConfiguration( configRules.getRuleConfig(LONG_LINE)?.configuration ?: emptyMap() ) } private lateinit var positionByOffset: (Int) -> Pair override fun logic(node: ASTNode) { var currentFixNumber = 0 var isFixedSmthInPreviousStep: Boolean // loop that trying to fix LineLength rule warnings until warnings run out do { isFixedSmthInPreviousStep = false currentFixNumber++ if (node.elementType == KtFileElementType.INSTANCE) { node.getChildren(null).forEach { if (it.elementType != PACKAGE_DIRECTIVE && it.elementType != IMPORT_LIST) { val isFixedSmthInChildNode = checkLength(it, configuration) if (!isFixedSmthInPreviousStep && isFixedSmthInChildNode) { isFixedSmthInPreviousStep = true } } } } } while (isFixedSmthInPreviousStep && currentFixNumber < MAX_FIX_NUMBER) if (currentFixNumber == MAX_FIX_NUMBER) { log.error { "The limit on the number of consecutive fixes has been reached. There may be a bug causing an endless loop of fixes." } } } @Suppress( "UnsafeCallOnNullableType", "TOO_LONG_FUNCTION", "FUNCTION_BOOLEAN_PREFIX" ) private fun checkLength(node: ASTNode, configuration: LineLengthConfiguration): Boolean { var isFixedSmthInChildNode = false var offset = 0 node.text.lines().forEach { line -> if (line.length > configuration.lineLength) { val newNode = node.psi.findElementAt(offset + configuration.lineLength.toInt() - 1)!!.node if ((newNode.elementType != TEXT && newNode.elementType != MARKDOWN_INLINE_LINK) || !isKdocValid(newNode)) { positionByOffset = node.treeParent.calculateLineColByOffset() val fixableType = isFixable(newNode, configuration) LONG_LINE.warnOnlyOrWarnAndFix( configRules, emitWarn, "max line length ${configuration.lineLength}, but was ${line.length}", offset + node.startOffset, node, shouldBeAutoCorrected = fixableType !is None, isFixMode, ) { val textBeforeFix = node.text val textLenBeforeFix = node.textLength val blankLinesBeforeFix = node.text.lines().size - countCodeLines(node) fixableType.fix() val textAfterFix = node.text val textLenAfterFix = node.textLength val blankLinesAfterFix = node.text.lines().size - countCodeLines(node) // checking that any fix may have been made isFixedSmthInChildNode = fixableType !is None // for cases when text doesn't change, and then we need to stop fixes if (textBeforeFix == textAfterFix) { isFixedSmthInChildNode = false } // in some kernel cases of long lines, when in fix step we adding `\n` to certain place of the line // and part of the line is transferred to the new line, this part may still be too long, // and then in next fix step we can start generating unnecessary blank lines, // to detect this we count blank lines and make unfix, if necessary if (blankLinesAfterFix > blankLinesBeforeFix) { isFixedSmthInChildNode = false fixableType.unFix() } else { // we should keep in mind, that in the course of fixing we change the offset // offset for all next nodes changed to this delta offset += (textLenAfterFix - textLenBeforeFix) } } } } offset += line.length + 1 } return isFixedSmthInChildNode } @Suppress( "TOO_LONG_FUNCTION", "LongMethod", "ComplexMethod", "GENERIC_VARIABLE_WRONG_DECLARATION", ) private fun isFixable(wrongNode: ASTNode, configuration: LineLengthConfiguration): LongLineFixableCases { var parent = wrongNode var stringOrDot: ASTNode? = null do { when (parent.elementType) { BINARY_EXPRESSION, PARENTHESIZED -> { val parentIsValArgListOrFunLitOrWhenEntry = listOf(VALUE_ARGUMENT_LIST, FUNCTION_LITERAL, WHEN_CONDITION_EXPRESSION) findParentNodeMatching(parent, parentIsValArgListOrFunLitOrWhenEntry)?.let { parent = it } ?: run { val splitOffset = searchRightSplitAfterOperationReference(parent, configuration)?.second splitOffset?.let { val parentIsBiExprOrParenthesized = parent.treeParent.elementType in listOf(BINARY_EXPRESSION, PARENTHESIZED) val parentIsFunOrProperty = parent.treeParent.elementType in listOf(FUN, PROPERTY) if (parentIsBiExprOrParenthesized || (parentIsFunOrProperty && splitOffset > configuration.lineLength)) { parent = parent.treeParent } else { return checkBinaryExpression(parent, configuration) } } ?: run { stringOrDot?.let { val returnElem = checkStringTemplateAndDotQualifiedExpression(it, configuration) if (returnElem !is None) { return returnElem } } parent = parent.treeParent } } } FUN, PROPERTY -> return checkFunAndProperty(parent) VALUE_ARGUMENT_LIST -> parent.findParentNodeWithSpecificType(BINARY_EXPRESSION)?.let { parent = it } ?: return checkArgumentsList(parent, configuration) WHEN_ENTRY -> return WhenEntry(parent) WHEN_CONDITION_EXPRESSION -> return None() EOL_COMMENT -> return checkComment(parent, configuration) FUNCTION_LITERAL -> return Lambda(parent) STRING_TEMPLATE, DOT_QUALIFIED_EXPRESSION, SAFE_ACCESS_EXPRESSION -> { stringOrDot = parent val parentIsBinExpOrValArgListOrWhenEntry = listOf(BINARY_EXPRESSION, VALUE_ARGUMENT_LIST, WHEN_CONDITION_EXPRESSION) findParentNodeMatching(parent, parentIsBinExpOrValArgListOrWhenEntry)?.let { parent = it } ?: run { val returnElem = checkStringTemplateAndDotQualifiedExpression(parent, configuration) if (returnElem !is None) { return returnElem } parent = parent.treeParent } } else -> parent = parent.treeParent } } while (parent.treeParent != null) return None() } private fun findParentNodeMatching(node: ASTNode, listType: List): ASTNode? { listType.forEach { type -> node.findParentNodeWithSpecificType(type)?.let { return it } } return null } @Suppress("PARAMETER_NAME_IN_OUTER_LAMBDA") private fun checkArgumentsList(node: ASTNode, configuration: LineLengthConfiguration): LongLineFixableCases { node.findParentNodeWithSpecificType(WHEN_ENTRY)?.let { it.findChildByType(BLOCK)?.run { return ValueArgumentList(node, configuration, positionByOffset) } ?: return WhenEntry(it) } return ValueArgumentList(node, configuration, positionByOffset) } /** * Parses the existing binary expression and passes the necessary parameters to the fix function for splitting */ private fun checkBinaryExpression(node: ASTNode, configuration: LineLengthConfiguration): LongLineFixableCases { val leftOffset = positionByOffset(node.firstChildNode.startOffset).second val binList: MutableList = mutableListOf() searchBinaryExpression(node, binList) if (binList.size == 1) { return BinaryExpression(node) } return LongBinaryExpression(node, configuration, leftOffset, binList, positionByOffset) } @Suppress("TOO_MANY_LINES_IN_LAMBDA", "GENERIC_VARIABLE_WRONG_DECLARATION") private fun checkStringTemplateAndDotQualifiedExpression( node: ASTNode, configuration: LineLengthConfiguration ): LongLineFixableCases { val isPropertyOrFun = listOf(PROPERTY, FUN) val funOrPropertyNode = findParentNodeMatching(node, isPropertyOrFun) funOrPropertyNode?.let { if (it.hasChildOfType(EQ)) { val positionByOffset = positionByOffset(it.getFirstChildWithType(EQ)?.startOffset ?: 0).second if (positionByOffset < configuration.lineLength / 2) { val returnedClass = parserStringAndDot(node, configuration) if (returnedClass !is None) { return returnedClass } } return FunAndProperty(it) } return parserStringAndDot(node, configuration) } ?: return parserStringAndDot(node, configuration) } private fun parserStringAndDot(node: ASTNode, configuration: LineLengthConfiguration) = if (node.elementType == STRING_TEMPLATE) { parserStringTemplate(node, configuration) } else { parserDotQualifiedExpression(node, configuration) } /** * This class finds where the string can be split * * @return StringTemplate - if the string can be split, * BinaryExpression - if there is two concatenated strings and new line should be inserted after `+` * None - if the string can't be split */ @Suppress("TOO_LONG_FUNCTION", "UnsafeCallOnNullableType") private fun parserStringTemplate(node: ASTNode, configuration: LineLengthConfiguration): LongLineFixableCases { var multiLineOffset = 0 val leftOffset = if (node.text.lines().size > 1) { node .text .lines() .takeWhile { it.length < configuration.lineLength } .forEach { multiLineOffset += it.length } node .text .lines() .first { it.length > configuration.lineLength } .takeWhile { it.isWhitespace() } .count() } else { positionByOffset(node.startOffset).second } val delimiterIndex = node.text.substring(0, multiLineOffset + configuration.lineLength.toInt() - leftOffset).lastIndexOf(' ') if (delimiterIndex == -1) { // we can't split this string, however may be we can move it entirely: // case when new line should be inserted after `+`. Example: "first" + "second" node.treeParent.findChildByType(OPERATION_REFERENCE)?.let { return BinaryExpression(node.treeParent) } // can't fix this case return None() } // check, that space to split is a part of text - not code // If the space split is part of the code, then there is a chance of breaking the code when fixing, that why we should ignore it val isSpaceIsWhiteSpace = node.psi .findElementAt(delimiterIndex)!! .node .isWhiteSpace() if (isSpaceIsWhiteSpace) { return None() } // minus 2 here as we are inserting ` +` and we don't want it to exceed line length val shouldAddTwoSpaces = (multiLineOffset == 0) && (leftOffset + delimiterIndex > configuration.lineLength.toInt() - 2) val correcterDelimiter = if (shouldAddTwoSpaces) { node.text.substring(0, delimiterIndex - 2).lastIndexOf(' ') } else { delimiterIndex } if (correcterDelimiter == -1) { return None() } return StringTemplate(node, correcterDelimiter, multiLineOffset == 0) } private fun parserDotQualifiedExpression( wrongNode: ASTNode, configuration: LineLengthConfiguration ): LongLineFixableCases { val nodeDot = searchRightSplitBeforeDotOrSafeAccess(wrongNode, configuration, DOT) val nodeSafeAccess = searchRightSplitBeforeDotOrSafeAccess(wrongNode, configuration, SAFE_ACCESS) return nodeDot?.let { DotQualifiedExpression(wrongNode) } ?: nodeSafeAccess?.let { DotQualifiedExpression(wrongNode) } ?: None() } private fun checkFunAndProperty(wrongNode: ASTNode) = if (wrongNode.hasChildOfType(EQ)) FunAndProperty(wrongNode) else None() private fun checkComment(wrongNode: ASTNode, configuration: LineLengthConfiguration): LongLineFixableCases { val leftOffset = positionByOffset(wrongNode.startOffset).second val stringBeforeCommentContent = wrongNode.text.takeWhile { it == ' ' || it == '/' } if (stringBeforeCommentContent.length >= configuration.lineLength.toInt() - leftOffset) { return None() } val indexLastSpace = wrongNode.text.substring(stringBeforeCommentContent.length, configuration.lineLength.toInt() - leftOffset).lastIndexOf(' ') val isNewLine = wrongNode.treePrev?.isWhiteSpaceWithNewline() ?: wrongNode.treeParent?.treePrev?.isWhiteSpaceWithNewline() ?: false if (isNewLine && indexLastSpace == -1) { return None() } return Comment(wrongNode, isNewLine, indexLastSpace + stringBeforeCommentContent.length) } // fixme json method private fun isKdocValid(node: ASTNode) = try { if (node.elementType == TEXT) { URL(node.text.split("\\s".toRegex()).last { it.isNotEmpty() }) } else { URL(node.text.substring(node.text.indexOfFirst { it == ']' } + 2, node.textLength - 1)) } true } catch (e: MalformedURLException) { false } /** * This method uses recursion to store binary node in the order in which they are located * Also binList contains nodes with PREFIX_EXPRESSION element type ( !isFoo(), !isValid) * *@param node node in which to search *@param binList mutable list of ASTNode to store nodes */ private fun searchBinaryExpression(node: ASTNode, binList: MutableList) { if (node.hasChildOfType(BINARY_EXPRESSION) || node.hasChildOfType(PARENTHESIZED) || node.hasChildOfType(POSTFIX_EXPRESSION)) { node.getChildren(null) .forEach { searchBinaryExpression(it, binList) } } if (node.elementType == BINARY_EXPRESSION) { binList.add(node) binList.add(node.treeParent.findChildByType(PREFIX_EXPRESSION) ?: return) } } /** * This method uses recursion to store dot qualified expression node in the order in which they are located * Also dotList contains nodes with PREFIX_EXPRESSION element type ( !isFoo(), !isValid)) * *@param node node in which to search *@param dotList mutable list of ASTNode to store nodes */ private fun searchDotOrSafeAccess(node: ASTNode, dotList: MutableList) { if (node.elementType == DOT_QUALIFIED_EXPRESSION || node.elementType == SAFE_ACCESS_EXPRESSION || node.elementType == POSTFIX_EXPRESSION) { node.getChildren(null) .forEach { searchDotOrSafeAccess(it, dotList) } if (node.elementType != POSTFIX_EXPRESSION) { dotList.add(node) } } } /** * Finds the first binary expression closer to the separator */ @Suppress("UnsafeCallOnNullableType") private fun searchRightSplitAfterOperationReference( parent: ASTNode, configuration: LineLengthConfiguration, ): Pair? { val list: MutableList = mutableListOf() searchBinaryExpression(parent, list) return list.asSequence() .map { it to positionByOffset(it.getFirstChildWithType(OPERATION_REFERENCE)!!.startOffset).second } .sortedBy { it.second } .lastOrNull { (it, offset) -> offset + (it.getFirstChildWithType(OPERATION_REFERENCE)?.text?.length ?: 0) <= configuration.lineLength + 1 } } /** * Finds the first dot or safe access closer to the separator */ @Suppress( "MAGIC_NUMBER", "MagicNumber", "PARAMETER_NAME_IN_OUTER_LAMBDA" ) private fun searchRightSplitBeforeDotOrSafeAccess( parent: ASTNode, configuration: LineLengthConfiguration, type: IElementType ): Pair? { val list: MutableList = mutableListOf() searchDotOrSafeAccess(parent, list) val offsetFromMaximum = 10 return list.asSequence() .map { val offset = it.getFirstChildWithType(type)?.run { positionByOffset(this.startOffset).second } ?: run { configuration.lineLength.toInt() + offsetFromMaximum } it to offset } .sortedBy { it.second } .lastOrNull { (_, offset) -> offset <= configuration.lineLength + 1 } } /** * * [RuleConfiguration] for maximum line length */ class LineLengthConfiguration(config: Map) : RuleConfiguration(config) { /** * Maximum allowed line length */ val lineLength = config["lineLength"]?.toLongOrNull() ?: MAX_LENGTH } /** * Class LongLineFixableCases is parent class for several specific error classes */ @Suppress("KDOC_NO_CONSTRUCTOR_PROPERTY", "MISSING_KDOC_CLASS_ELEMENTS") // todo add proper docs abstract class LongLineFixableCases(val node: ASTNode) { /** * Abstract fix - fix anything nodes */ abstract fun fix() /** * Function unFix - unfix incorrect unnecessary fix-changes */ @Suppress("EmptyFunctionBlock") open fun unFix() { // Nothing to do here by default. } } /** * Class None show error that long line have unidentified type or something else that we can't analyze */ private class None : LongLineFixableCases(KotlinParser().createNode("ERROR")) { @Suppress("EmptyFunctionBlock") override fun fix() {} } /** * Class Comment show that long line should be split in comment * @property hasNewLineBefore flag to handle type of comment: ordinary comment (long part of which should be moved to the next line) * and inline comments (which should be moved entirely to the previous line) * @property indexLastSpace index of last space to substring comment */ private class Comment( node: ASTNode, val hasNewLineBefore: Boolean, val indexLastSpace: Int = 0 ) : LongLineFixableCases(node) { override fun fix() { if (this.hasNewLineBefore) { val indexLastSpace = this.indexLastSpace val nodeText = "//${node.text.substring(indexLastSpace, node.text.length)}" node.treeParent.apply { addChild(LeafPsiElement(EOL_COMMENT, node.text.substring(0, indexLastSpace)), node) addChild(PsiWhiteSpaceImpl("\n"), node) addChild(LeafPsiElement(EOL_COMMENT, nodeText), node) removeChild(node) } } else { if (node.treePrev.isWhiteSpace()) { node.treeParent.removeChild(node.treePrev) } // for cases when property has multiline initialization, and then we need to move comment before first line val newLineNodeOnPreviousLine = if (node.treeParent.elementType == PROPERTY) { node.treeParent.treeParent.findChildrenMatching { it.elementType == WHITE_SPACE && node.treeParent.treeParent.isChildAfterAnother(node.treeParent, it) && it.textContains('\n') } .lastOrNull() } else { node.findAllNodesWithConditionOnLine(node.getLineNumber() - 1) { it.elementType == WHITE_SPACE && it.textContains('\n') }?.lastOrNull() } newLineNodeOnPreviousLine?.let { val parent = node.treeParent parent.removeChild(node) newLineNodeOnPreviousLine.treeParent.addChild(node, newLineNodeOnPreviousLine.treeNext) newLineNodeOnPreviousLine.treeParent.addChild(PsiWhiteSpaceImpl("\n"), newLineNodeOnPreviousLine.treeNext.treeNext) } } } } /** * Class StringTemplate show that long line should be split in string template * @property delimiterIndex * @property isOneLineString */ private class StringTemplate( node: ASTNode, val delimiterIndex: Int, val isOneLineString: Boolean ) : LongLineFixableCases(node) { override fun fix() { val incorrectText = node.text val firstPart = incorrectText.substring(0, delimiterIndex) val secondPart = incorrectText.substring(delimiterIndex, incorrectText.length) val textBetweenParts = if (isOneLineString) { "\" +\n\"" } else { "\n" } val correctNode = KotlinParser().createNode("$firstPart$textBetweenParts$secondPart") node.treeParent.replaceChild(node, correctNode) } } /** * Class BinaryExpression show that long line should be split in short binary expression? after operation reference */ private class BinaryExpression(node: ASTNode) : LongLineFixableCases(node) { override fun fix() { val binNode = if (node.elementType == PARENTHESIZED) { node.findChildByType(BINARY_EXPRESSION) } else { node } val nodeOperationReference = binNode?.findChildByType(OPERATION_REFERENCE) val nextNode = if (nodeOperationReference?.firstChildNode?.elementType != ELVIS) { nodeOperationReference?.treeNext } else { if (nodeOperationReference?.treePrev?.elementType == WHITE_SPACE) { nodeOperationReference?.treePrev } else { nodeOperationReference } } binNode?.appendNewlineMergingWhiteSpace(nextNode, nextNode) } } /** * Class LongBinaryExpression show that long line should be split between other parts long binary expression, * after one of operation reference * @property maximumLineLength is number of maximum line length * @property leftOffset is offset before start [node] * @property binList is list of Binary Expression which are children of [node] * @property positionByOffset */ private class LongBinaryExpression( node: ASTNode, val maximumLineLength: LineLengthConfiguration, val leftOffset: Int, val binList: MutableList, var positionByOffset: (Int) -> Pair ) : LongLineFixableCases(node) { /** * Fix a binary expression - * - If the transfer is done on the Elvis operator, then transfers it to a new line * - If not on the Elvis operator, then transfers it to a new line after the operation reference */ @Suppress("UnsafeCallOnNullableType") override fun fix() { val anySplitNode = searchSomeSplitInBinaryExpression(node, maximumLineLength) val rightSplitNode = anySplitNode[0] ?: anySplitNode[1] ?: anySplitNode[2] val nodeOperationReference = rightSplitNode?.first?.getFirstChildWithType(OPERATION_REFERENCE) rightSplitNode?.let { val nextNode = if (nodeOperationReference?.firstChildNode?.elementType != ELVIS) { nodeOperationReference?.treeNext } else { if (nodeOperationReference?.treePrev?.elementType == WHITE_SPACE) { nodeOperationReference?.treePrev } else { nodeOperationReference } } if (!nextNode?.text?.contains(("\n"))!!) { rightSplitNode.first.appendNewlineMergingWhiteSpace(nextNode, nextNode) } } } /** * This method stored all the nodes that have [BINARY_EXPRESSION] or [PREFIX_EXPRESSION] element type. * - First elem in List - Logic Binary Expression (`&&`, `||`) * - Second elem in List - Comparison Binary Expression (`>`, `<`, `==`, `>=`, `<=`, `!=`, `===`, `!==`) * - Other types (Arithmetical and Bitwise operation) (`+`, `-`, `*`, `/`, `%`, `>>`, `<<`, `&`, `|`, `~`, `^`, `>>>`, `<<<`, * `*=`, `+=`, `-=`, `/=`, `%=`, `++`, `--`, `in` `!in`, etc.) * * @return the list of node-to-offset pairs. */ @Suppress("TYPE_ALIAS") private fun searchSomeSplitInBinaryExpression(parent: ASTNode, configuration: LineLengthConfiguration): List?> { val logicListOperationReference = listOf(OROR, ANDAND) val compressionListOperationReference = listOf(GT, LT, EQEQ, GTEQ, LTEQ, EXCLEQ, EQEQEQ, EXCLEQEQEQ) val binList: MutableList = mutableListOf() searchBinaryExpression(parent, binList) val rightBinList = binList.map { it to positionByOffset(it.getFirstChildWithType(OPERATION_REFERENCE)?.startOffset ?: 0).second } .sortedBy { it.second } .reversed() val returnList: MutableList?> = mutableListOf() addInSmartListBinExpression(returnList, rightBinList, logicListOperationReference, configuration) addInSmartListBinExpression(returnList, rightBinList, compressionListOperationReference, configuration) val expression = rightBinList.firstOrNull { (it, offset) -> val binOperationReference = it.getFirstChildWithType(OPERATION_REFERENCE)?.firstChildNode?.elementType offset + (it.getFirstChildWithType(OPERATION_REFERENCE)?.text?.length ?: 0) <= configuration.lineLength + 1 && binOperationReference !in logicListOperationReference && binOperationReference !in compressionListOperationReference && binOperationReference != EXCL } returnList.add(expression) return returnList } private fun searchBinaryExpression(node: ASTNode, binList: MutableList) { if (node.hasChildOfType(BINARY_EXPRESSION) || node.hasChildOfType(PARENTHESIZED) || node.hasChildOfType(POSTFIX_EXPRESSION)) { node.getChildren(null) .forEach { searchBinaryExpression(it, binList) } } if (node.elementType == BINARY_EXPRESSION) { binList.add(node) binList.add(node.treeParent.findChildByType(PREFIX_EXPRESSION) ?: return) } } /** * Runs through the sorted list [rightBinList], finds its last element, the type of which is included in the set [typesList] and adds it in the list [returnList] */ @Suppress("TYPE_ALIAS") private fun addInSmartListBinExpression( returnList: MutableList?>, rightBinList: List>, typesList: List, configuration: LineLengthConfiguration ) { val expression = rightBinList.firstOrNull { (it, offset) -> val binOperationReference = it.getFirstChildWithType(OPERATION_REFERENCE) offset + (it.getFirstChildWithType(OPERATION_REFERENCE)?.text?.length ?: 0) <= configuration.lineLength + 1 && binOperationReference?.firstChildNode?.elementType in typesList } returnList.add(expression) } } /** * Class FunAndProperty show that long line should be split in Fun Or Property: after EQ (between head and body this function) */ private class FunAndProperty(node: ASTNode) : LongLineFixableCases(node) { override fun fix() { node.appendNewlineMergingWhiteSpace(null, node.findChildByType(EQ)?.treeNext) } override fun unFix() { node.findChildAfter(EQ, WHITE_SPACE)?.let { correctWhiteSpace -> if (correctWhiteSpace.textContains('\n')) { correctWhiteSpace.nextSibling()?.let { wrongWhiteSpace -> if (wrongWhiteSpace.textContains('\n')) { node.removeChild(wrongWhiteSpace) } } } } } } /** * Class Lambda show that long line should be split in Lambda: in space after [LBRACE] node and before [RBRACE] node */ private class Lambda(node: ASTNode) : LongLineFixableCases(node) { /** * Splits Lambda expressions - add splits lines, thereby making the lambda expression a separate line */ override fun fix() { node.appendNewlineMergingWhiteSpace(node.findChildByType(LBRACE)?.treeNext, node.findChildByType(LBRACE)?.treeNext) node.appendNewlineMergingWhiteSpace(node.findChildByType(RBRACE)?.treePrev, node.findChildByType(RBRACE)?.treePrev) } } /** * Class DotQualifiedExpression show that line should be split in DotQualifiedExpression */ private class DotQualifiedExpression(node: ASTNode) : LongLineFixableCases(node) { override fun fix() { val dot = node.getFirstChildWithType(DOT) val safeAccess = node.getFirstChildWithType(SAFE_ACCESS) val splitNode = if ((dot?.startOffset ?: 0) > (safeAccess?.startOffset ?: 0)) { dot } else { safeAccess } val nodeBeforeDot = splitNode?.treePrev node.appendNewlineMergingWhiteSpace(nodeBeforeDot, splitNode) } } /** * Class ValueArgumentList show that line should be split in ValueArgumentList: * @property maximumLineLength - max line length * @property positionByOffset */ private class ValueArgumentList( node: ASTNode, val maximumLineLength: LineLengthConfiguration, var positionByOffset: (Int) -> Pair ) : LongLineFixableCases(node) { override fun fix() { val lineLength = maximumLineLength.lineLength val offset = fixFirst() val listComma = node.getAllChildrenWithType(COMMA).map { it to positionByOffset(it.startOffset - offset).second }.sortedBy { it.second } var lineNumber = 1 listComma.forEachIndexed { index, pair -> if (pair.second >= lineNumber * lineLength) { lineNumber++ val commaSplit = if (index > 0) { listComma[index - 1].first } else { pair.first } node.appendNewlineMergingWhiteSpace(commaSplit.treeNext, commaSplit.treeNext) } } node.getFirstChildWithType(RPAR)?.let { child -> if (positionByOffset(child.treePrev.startOffset).second + child.treePrev.text.length - offset > lineLength * lineNumber && listComma.isNotEmpty()) { listComma.last().first.let { node.appendNewlineMergingWhiteSpace(it.treeNext, it.treeNext) } } } } override fun unFix() { node.findChildBefore(RPAR, WHITE_SPACE)?.let { correctWhiteSpace -> if (correctWhiteSpace.textContains('\n')) { correctWhiteSpace.prevSibling()?.let { wrongWhiteSpace -> if (wrongWhiteSpace.textContains('\n')) { node.removeChild(wrongWhiteSpace) } } } } } private fun fixFirst(): Int { val lineLength = maximumLineLength.lineLength var startOffset = 0 node.getFirstChildWithType(COMMA)?.let { if (positionByOffset(it.startOffset).second > lineLength) { node.appendNewlineMergingWhiteSpace(node.findChildByType(LPAR)?.treeNext, node.findChildByType(LPAR)?.treeNext) node.appendNewlineMergingWhiteSpace(node.findChildByType(RPAR), node.findChildByType(RPAR)) startOffset = this.maximumLineLength.lineLength.toInt() } } ?: node.getFirstChildWithType(RPAR)?.let { node.appendNewlineMergingWhiteSpace(node.findChildByType(LPAR)?.treeNext, node.findChildByType(LPAR)?.treeNext) node.appendNewlineMergingWhiteSpace(node.findChildByType(RPAR), node.findChildByType(RPAR)) startOffset = this.maximumLineLength.lineLength.toInt() } return startOffset } } /** * Class WhenEntry show that line should be split in WhenEntry node: * - Added [LBRACE] and [RBRACE] nodes * - Split line in space after [LBRACE] node and before [RBRACE] node */ private class WhenEntry(node: ASTNode) : LongLineFixableCases(node) { override fun fix() { node.getFirstChildWithType(ARROW)?.let { node.appendNewlineMergingWhiteSpace(it.treeNext, it.treeNext) } } } companion object { private val log = KotlinLogging.logger {} private const val MAX_FIX_NUMBER = 10 private const val MAX_LENGTH = 120L const val NAME_ID = "line-length" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/LongNumericalValuesSeparatedRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter3 import com.saveourtool.diktat.common.config.rules.RuleConfiguration import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.common.config.rules.getRuleConfig import com.saveourtool.diktat.ruleset.constants.Warnings.LONG_NUMERICAL_VALUES_SEPARATED import com.saveourtool.diktat.ruleset.rules.DiktatRule import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement import org.jetbrains.kotlin.lexer.KtTokens.FLOAT_LITERAL import org.jetbrains.kotlin.lexer.KtTokens.INTEGER_LITERAL import java.lang.StringBuilder /** * Rule that checks if numerical separators (`_`) are used for long numerical literals */ class LongNumericalValuesSeparatedRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(LONG_NUMERICAL_VALUES_SEPARATED) ) { override fun logic(node: ASTNode) { val configuration = LongNumericalValuesConfiguration( configRules.getRuleConfig(LONG_NUMERICAL_VALUES_SEPARATED)?.configuration ?: emptyMap()) if (node.elementType == INTEGER_LITERAL && !isValidConstant(node.text, configuration, node)) { LONG_NUMERICAL_VALUES_SEPARATED.warnAndFix(configRules, emitWarn, isFixMode, node.text, node.startOffset, node) { fixIntegerConstant(node, configuration.maxBlockLength) } } if (node.elementType == FLOAT_LITERAL && !isValidConstant(node.text, configuration, node)) { val parts = node.text.split(".") LONG_NUMERICAL_VALUES_SEPARATED.warnAndFix(configRules, emitWarn, isFixMode, node.text, node.startOffset, node) { fixFloatConstantPart(parts[0], parts[1], configuration, node) } } } private fun fixIntegerConstant(node: ASTNode, maxBlockLength: Int) { val resultRealPart = StringBuilder(nodePrefix(node.text)) val chunks = removePrefixSuffix(node.text) .reversed() .chunked(maxBlockLength) .reversed() resultRealPart.append(chunks.joinToString(separator = "_") { it.reversed() }) resultRealPart.append(nodeSuffix(node.text)) (node as LeafPsiElement).rawReplaceWithText(resultRealPart.toString()) } private fun fixFloatConstantPart( realPart: String, fractionalPart: String, configuration: LongNumericalValuesConfiguration, node: ASTNode ) { val resultRealPart = StringBuilder(nodePrefix(realPart)) val resultFractionalPart = StringBuilder() val realNumber = removePrefixSuffix(realPart) if (realNumber.length > configuration.maxLength) { val chunks = realNumber .reversed() .chunked(configuration.maxBlockLength) .reversed() resultRealPart.append(chunks.joinToString(separator = "_") { it.reversed() }) resultRealPart.append(nodeSuffix(realPart)).append(".") } else { resultRealPart.append(realNumber).append(".") } val fractionalNumber = removePrefixSuffix(fractionalPart) if (fractionalNumber.length > configuration.maxLength) { val chunks = fractionalNumber.chunked(configuration.maxBlockLength) resultFractionalPart.append(chunks.joinToString(separator = "_", postfix = nodeSuffix(fractionalPart)) { it }) resultFractionalPart.append(nodeSuffix(fractionalPart)) } else { resultFractionalPart.append(fractionalNumber).append(nodeSuffix(fractionalPart)) } (node as LeafPsiElement).rawReplaceWithText(resultRealPart.append(resultFractionalPart).toString()) } private fun nodePrefix(nodeText: String) = when { nodeText.startsWith("0b") -> "0b" nodeText.startsWith("0x") -> "0x" else -> "" } private fun nodeSuffix(nodeText: String) = when { nodeText.endsWith("L") -> "L" nodeText.endsWith("f", true) -> "f" else -> "" } private fun isValidConstant( text: String, configuration: LongNumericalValuesConfiguration, node: ASTNode ): Boolean { if (text.contains("_")) { checkBlocks(removePrefixSuffix(text), configuration, node) return true } return text .split(".") .map { removePrefixSuffix(it) } .all { it.length <= configuration.maxLength } } private fun checkBlocks( text: String, configuration: LongNumericalValuesConfiguration, node: ASTNode ) { val blocks = text.split("_", ".") blocks.forEach { if (it.length > configuration.maxBlockLength) { LONG_NUMERICAL_VALUES_SEPARATED.warn(configRules, emitWarn, "this block is too long $it", node.startOffset, node) } } } private fun removePrefixSuffix(text: String): String { if (text.startsWith("0x")) { return text.removePrefix("0x") } return text.removePrefix("0b") .removeSuffix("L") .removeSuffix("f") .removeSuffix("F") } /** * [RuleConfiguration] for numerical literals separation */ class LongNumericalValuesConfiguration(config: Map) : RuleConfiguration(config) { /** * Maximum number of digits which are not split */ val maxLength = config["maxNumberLength"]?.toIntOrNull() ?: MAX_NUMBER_LENGTH /** * Maximum number of digits between separators */ val maxBlockLength = config["maxBlockLength"]?.toIntOrNull() ?: DELIMITER_LENGTH } companion object { private const val DELIMITER_LENGTH: Int = 3 private const val MAX_NUMBER_LENGTH: Int = 3 const val NAME_ID = "long-numerical-values" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/MagicNumberRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter3 import com.saveourtool.diktat.common.config.rules.RuleConfiguration import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.common.config.rules.getCommonConfiguration import com.saveourtool.diktat.common.config.rules.getRuleConfig import com.saveourtool.diktat.ruleset.constants.Warnings.MAGIC_NUMBER import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.* import com.saveourtool.diktat.ruleset.utils.parent import org.jetbrains.kotlin.KtNodeTypes.BINARY_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.ENUM_ENTRY import org.jetbrains.kotlin.KtNodeTypes.FLOAT_CONSTANT import org.jetbrains.kotlin.KtNodeTypes.FUN import org.jetbrains.kotlin.KtNodeTypes.INTEGER_CONSTANT import org.jetbrains.kotlin.KtNodeTypes.OPERATION_REFERENCE import org.jetbrains.kotlin.KtNodeTypes.PROPERTY import org.jetbrains.kotlin.KtNodeTypes.VALUE_PARAMETER import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.lexer.KtTokens.IDENTIFIER import org.jetbrains.kotlin.lexer.KtTokens.MINUS import org.jetbrains.kotlin.lexer.KtTokens.RANGE import org.jetbrains.kotlin.psi.KtFunction import org.jetbrains.kotlin.psi.KtProperty import org.jetbrains.kotlin.psi.psiUtil.isExtensionDeclaration import org.jetbrains.kotlin.psi.psiUtil.parents /** * Rule for magic number */ class MagicNumberRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(MAGIC_NUMBER) ) { private val configuration by lazy { MagicNumberConfiguration( configRules.getRuleConfig(MAGIC_NUMBER)?.configuration ?: emptyMap() ) } @Suppress("COLLAPSE_IF_STATEMENTS") override fun logic(node: ASTNode) { val filePath = node.getFilePath() val config = configRules.getCommonConfiguration() if (node.elementType == INTEGER_CONSTANT || node.elementType == FLOAT_CONSTANT) { if (!isLocatedInTest(filePath.splitPathToDirs(), config.testAnchors) || !configuration.isIgnoreTest) { checkNumber(node, configuration) } } } @Suppress("ComplexMethod") private fun checkNumber(node: ASTNode, configuration: MagicNumberConfiguration) { val nodeText = node.treePrev?.let { if (it.elementType == OPERATION_REFERENCE && it.hasChildOfType(MINUS)) "-${node.text}" else node.text } ?: node.text val isIgnoreNumber = configuration.ignoreNumbers.contains(nodeText) val isHashFunction = node.parent { it.elementType == FUN && it.isHashFun() } != null val isLocalVariable = node.parent { it.elementType == PROPERTY && (it.isVarProperty() || it.isValProperty()) && (it.psi as KtProperty).isLocal } != null val isValueParameter = node.parent { it.elementType == VALUE_PARAMETER } != null val isConstant = node.parent { it.elementType == PROPERTY && it.isConstant() } != null val isCompanionObjectProperty = node.parent { it.elementType == PROPERTY && it.isNodeFromCompanionObject() } != null val isEnums = node.parent { it.elementType == ENUM_ENTRY } != null val isRanges = node.treeParent.let { it.elementType == BINARY_EXPRESSION && it.findChildByType(OPERATION_REFERENCE)?.hasChildOfType(RANGE) ?: false } val isExtensionFunctions = node.parent { it.elementType == FUN && (it.psi as KtFunction).isExtensionDeclaration() } != null && node.parents().none { it.elementType == PROPERTY } val isPairsCreatedUsingTo = node.treeParent.let { it.elementType == BINARY_EXPRESSION && it.findChildByType(OPERATION_REFERENCE)?.findChildByType(IDENTIFIER)?.text == "to" } val isPropertyDeclaration = !isLocalVariable && !isConstant && !isCompanionObjectProperty && !isRanges && !isPairsCreatedUsingTo && node.parent { it.elementType == PROPERTY } != null val result = listOf(isHashFunction, isPropertyDeclaration, isLocalVariable, isValueParameter, isConstant, isCompanionObjectProperty, isEnums, isRanges, isExtensionFunctions, isPairsCreatedUsingTo).zip(mapConfiguration.map { configuration.getParameter(it.key) }) if (result.any { it.first && !it.second } && !isIgnoreNumber) { MAGIC_NUMBER.warn(configRules, emitWarn, nodeText, node.startOffset, node) } } private fun ASTNode.isHashFun() = (this.psi as KtFunction).run { this.nameIdentifier?.text == "hashCode" && this.annotationEntries.map { it.text }.contains("@Override") } /** * [RuleConfiguration] for configuration */ class MagicNumberConfiguration(config: Map) : RuleConfiguration(config) { /** * Flag to ignore numbers from test */ val isIgnoreTest = config["ignoreTest"]?.toBoolean() ?: IGNORE_TEST /** * List of ignored numbers */ val ignoreNumbers = config["ignoreNumbers"]?.split(",")?.map { it.trim() }?.filter { it.isNumber() || it.isOtherNumberType() } ?: ignoreNumbersList /** * @param param parameter from config * @return value of parameter */ @Suppress("UnsafeCallOnNullableType") fun getParameter(param: String) = config[param]?.toBoolean() ?: mapConfiguration[param]!! /** * Check if string is number */ // || ((this.last().uppercase() == "L" || this.last().uppercase() == "U") && this.substring(0, this.lastIndex-1).isNumber()) private fun String.isNumber() = (this.toLongOrNull() ?: this.toFloatOrNull()) != null /** * Check if string include a char of number type */ private fun String.isOtherNumberType(): Boolean = ((this.last().uppercase() == "L" || this.last().uppercase() == "U") && this.substring(0, this.lastIndex).isNumber()) || (this.substring(this.lastIndex - 1).uppercase() == "UL" && this.substring(0, this.lastIndex - 1).isNumber()) } companion object { const val IGNORE_TEST = true const val NAME_ID = "magic-number" val ignoreNumbersList = listOf("-1", "1", "0", "2", "0U", "1U", "2U", "-1L", "0L", "1L", "2L", "0UL", "1UL", "2UL") val mapConfiguration = mapOf( "ignoreHashCodeFunction" to true, "ignorePropertyDeclaration" to false, "ignoreLocalVariableDeclaration" to false, "ignoreValueParameter" to true, "ignoreConstantDeclaration" to true, "ignoreCompanionObjectPropertyDeclaration" to true, "ignoreEnums" to false, "ignoreRanges" to false, "ignoreExtensionFunctions" to false, "ignorePairsCreatedUsingTo" to false) } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/MultipleModifiersSequence.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter3 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.WRONG_MULTIPLE_MODIFIERS_ORDER import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.findAllDescendantsWithSpecificType import org.jetbrains.kotlin.KtNodeTypes.ANNOTATION_ENTRY import org.jetbrains.kotlin.KtNodeTypes.FUN import org.jetbrains.kotlin.KtNodeTypes.MODIFIER_LIST import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.lexer.KtTokens.FUN_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.WHITE_SPACE import org.jetbrains.kotlin.psi.KtClass import org.jetbrains.kotlin.psi.psiUtil.children /** * @param configRules */ class MultipleModifiersSequence(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(WRONG_MULTIPLE_MODIFIERS_ORDER) ) { override fun logic(node: ASTNode) { if (node.elementType == MODIFIER_LIST) { checkModifierList(node) checkAnnotation(node) } } private fun checkModifierList(node: ASTNode) { val modifierListOfPair = node .getChildren(KtTokens.MODIFIER_KEYWORDS) .toList() .filter { !isSamInterfaces(node, it) } .map { Pair(it, modifierOrder.indexOf(it.elementType)) } val sortModifierListOfPair = modifierListOfPair.sortedBy { it.second }.map { it.first } modifierListOfPair.forEachIndexed { index, (modifierNode, _) -> if (modifierNode != sortModifierListOfPair[index]) { WRONG_MULTIPLE_MODIFIERS_ORDER.warnAndFix(configRules, emitWarn, isFixMode, "${modifierNode.text} should be on position ${sortModifierListOfPair.indexOf(modifierNode) + 1}, but is on position ${index + 1}", modifierNode.startOffset, modifierNode) { val nodeAfter = modifierNode.treeNext node.removeChild(modifierNode) node.addChild((sortModifierListOfPair[index].clone() as ASTNode), nodeAfter) } } } } private fun isSamInterfaces(parent: ASTNode, node: ASTNode): Boolean { val parentPsi = parent.treeParent.psi return if (parentPsi is KtClass) { (parentPsi.isInterface()) && node.elementType == FUN_KEYWORD && parent.treeParent.findAllDescendantsWithSpecificType(FUN).size == 1 } else { false } } private fun checkAnnotation(node: ASTNode) { val firstModifierIndex = node .children() .indexOfFirst { it.elementType in KtTokens.MODIFIER_KEYWORDS } .takeIf { it >= 0 } ?: return node .getChildren(null) .filterIndexed { index, astNode -> astNode.elementType == ANNOTATION_ENTRY && index > firstModifierIndex } .forEach { astNode -> WRONG_MULTIPLE_MODIFIERS_ORDER.warnAndFix(configRules, emitWarn, isFixMode, "${astNode.text} annotation should be before all modifiers", astNode.startOffset, astNode) { val spaceBefore = astNode.treePrev node.removeChild(astNode) if (spaceBefore != null && spaceBefore.elementType == WHITE_SPACE) { node.removeChild(spaceBefore) node.addChild(spaceBefore, node.firstChildNode) node.addChild(astNode.clone() as ASTNode, spaceBefore) } else { node.addChild(PsiWhiteSpaceImpl(" "), node.getChildren(null).first()) node.addChild(astNode.clone() as ASTNode, node.getChildren(null).first()) } } } } companion object { const val NAME_ID = "multiple-modifiers" private val modifierOrder = listOf(KtTokens.PUBLIC_KEYWORD, KtTokens.INTERNAL_KEYWORD, KtTokens.PROTECTED_KEYWORD, KtTokens.PRIVATE_KEYWORD, KtTokens.EXPECT_KEYWORD, KtTokens.ACTUAL_KEYWORD, KtTokens.FINAL_KEYWORD, KtTokens.OPEN_KEYWORD, KtTokens.ABSTRACT_KEYWORD, KtTokens.SEALED_KEYWORD, KtTokens.CONST_KEYWORD, KtTokens.EXTERNAL_KEYWORD, KtTokens.OVERRIDE_KEYWORD, KtTokens.LATEINIT_KEYWORD, KtTokens.TAILREC_KEYWORD, KtTokens.CROSSINLINE_KEYWORD, KtTokens.VARARG_KEYWORD, KtTokens.SUSPEND_KEYWORD, KtTokens.INNER_KEYWORD, KtTokens.OUT_KEYWORD, KtTokens.ENUM_KEYWORD, KtTokens.ANNOTATION_KEYWORD, KtTokens.COMPANION_KEYWORD, KtTokens.VALUE_KEYWORD, KtTokens.INLINE_KEYWORD, KtTokens.NOINLINE_KEYWORD, KtTokens.REIFIED_KEYWORD, KtTokens.INFIX_KEYWORD, KtTokens.OPERATOR_KEYWORD, KtTokens.DATA_KEYWORD, KtTokens.IN_KEYWORD) } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/NullableTypeRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter3 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.NULLABLE_PROPERTY_TYPE import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.KotlinParser import com.saveourtool.diktat.ruleset.utils.findAllDescendantsWithSpecificType import com.saveourtool.diktat.ruleset.utils.hasChildOfType import org.jetbrains.kotlin.KtNodeTypes.BOOLEAN_CONSTANT import org.jetbrains.kotlin.KtNodeTypes.CALL_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.CHARACTER_CONSTANT import org.jetbrains.kotlin.KtNodeTypes.DOT_QUALIFIED_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.FLOAT_CONSTANT import org.jetbrains.kotlin.KtNodeTypes.INTEGER_CONSTANT import org.jetbrains.kotlin.KtNodeTypes.NULL import org.jetbrains.kotlin.KtNodeTypes.NULLABLE_TYPE import org.jetbrains.kotlin.KtNodeTypes.PROPERTY import org.jetbrains.kotlin.KtNodeTypes.REFERENCE_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.STRING_TEMPLATE import org.jetbrains.kotlin.KtNodeTypes.TYPE_REFERENCE import org.jetbrains.kotlin.KtNodeTypes.USER_TYPE import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.CompositeElement import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType import org.jetbrains.kotlin.lexer.KtTokens.CHARACTER_LITERAL import org.jetbrains.kotlin.lexer.KtTokens.CLOSING_QUOTE import org.jetbrains.kotlin.lexer.KtTokens.EQ import org.jetbrains.kotlin.lexer.KtTokens.FLOAT_LITERAL import org.jetbrains.kotlin.lexer.KtTokens.INTEGER_LITERAL import org.jetbrains.kotlin.lexer.KtTokens.OPEN_QUOTE import org.jetbrains.kotlin.lexer.KtTokens.QUEST import org.jetbrains.kotlin.lexer.KtTokens.TRUE_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.VAL_KEYWORD /** * Rule that checks if nullable types are used and suggest to substitute them with non-nullable */ class NullableTypeRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(NULLABLE_PROPERTY_TYPE) ) { override fun logic(node: ASTNode) { if (node.elementType == PROPERTY) { checkProperty(node) } } @Suppress("UnsafeCallOnNullableType") private fun checkProperty(node: ASTNode) { if (node.hasChildOfType(VAL_KEYWORD) && node.hasChildOfType(EQ) && node.hasChildOfType(TYPE_REFERENCE)) { val typeReferenceNode = node.findChildByType(TYPE_REFERENCE)!! // check that property has nullable type, right value one of allow expression if (!node.hasChildOfType(NULL) && node.findAllDescendantsWithSpecificType(DOT_QUALIFIED_EXPRESSION).isEmpty() && typeReferenceNode.hasChildOfType(NULLABLE_TYPE) && typeReferenceNode.findChildByType(NULLABLE_TYPE)!!.hasChildOfType(QUEST) && (node.findChildByType(CALL_EXPRESSION)?.findChildByType(REFERENCE_EXPRESSION) == null || node.findChildByType(CALL_EXPRESSION)!!.findChildByType(REFERENCE_EXPRESSION)!!.text in allowExpression)) { NULLABLE_PROPERTY_TYPE.warn(configRules, emitWarn, "don't use nullable type", node.findChildByType(TYPE_REFERENCE)!!.startOffset, node) } else if (node.hasChildOfType(NULL)) { val fixedParam = findFixableParam(node) NULLABLE_PROPERTY_TYPE.warnOnlyOrWarnAndFix(configRules, emitWarn, "initialize explicitly", node.findChildByType(NULL)!!.startOffset, node, shouldBeAutoCorrected = fixedParam != null, isFixMode) { fixedParam?.let { findSubstitution(node, fixedParam) } } } } } @Suppress("UnsafeCallOnNullableType") private fun findFixableParam(node: ASTNode): FixedParam? { val reference = node.findChildByType(TYPE_REFERENCE)!! .findChildByType(NULLABLE_TYPE)!! .findChildByType(USER_TYPE) ?.findChildByType(REFERENCE_EXPRESSION) ?: return null return when (reference.text) { "Boolean" -> FixedParam(BOOLEAN_CONSTANT, TRUE_KEYWORD, "true") "Int", "Short", "Byte" -> FixedParam(INTEGER_CONSTANT, INTEGER_LITERAL, "0") "Double" -> FixedParam(FLOAT_CONSTANT, FLOAT_LITERAL, "0.0") "Float" -> FixedParam(FLOAT_CONSTANT, FLOAT_LITERAL, "0.0F") "Long" -> FixedParam(INTEGER_CONSTANT, INTEGER_LITERAL, "0L") "Char" -> FixedParam(CHARACTER_CONSTANT, CHARACTER_LITERAL, "''") "String" -> FixedParam(null, null, "", true) else -> findFixableForCollectionParam(reference.text) } } private fun findFixableForCollectionParam(referenceText: String): FixedParam? = when (referenceText) { "List", "Iterable" -> FixedParam(null, null, "emptyList()") "Map" -> FixedParam(null, null, "emptyMap()") "Array" -> FixedParam(null, null, "emptyArray()") "Set" -> FixedParam(null, null, "emptySet()") "Sequence" -> FixedParam(null, null, "emptySequence()") "Queue" -> FixedParam(null, null, "LinkedList()") "MutableList" -> FixedParam(null, null, "mutableListOf()") "MutableMap" -> FixedParam(null, null, "mutableMapOf()") "MutableSet" -> FixedParam(null, null, "mutableSetOf()") "LinkedList" -> FixedParam(null, null, "LinkedList()") "LinkedHashMap" -> FixedParam(null, null, "LinkedHashMap()") "LinkedHashSet" -> FixedParam(null, null, "LinkedHashSet()") else -> null } @Suppress("UnsafeCallOnNullableType") private fun findSubstitution(node: ASTNode, fixedParam: FixedParam) { if (fixedParam.isString) { replaceValueForString(node) } else if (fixedParam.insertConstantType != null && fixedParam.insertType != null) { replaceValue(node, fixedParam.insertConstantType, fixedParam.insertType, fixedParam.textNode) } else { replaceValueByText(node, fixedParam.textNode) } val nullableNode = node.findChildByType(TYPE_REFERENCE)!!.findChildByType(NULLABLE_TYPE)!! val userTypeNode = nullableNode.firstChildNode node.findChildByType(TYPE_REFERENCE)!!.replaceChild(nullableNode, userTypeNode) } @Suppress("UnsafeCallOnNullableType") private fun replaceValueByText(node: ASTNode, nodeText: String) { val newNode = KotlinParser().createNode(nodeText) if (newNode.elementType == CALL_EXPRESSION) { node.replaceChild(node.findChildByType(NULL)!!, newNode) } } @Suppress("UnsafeCallOnNullableType") private fun replaceValue( node: ASTNode, insertConstantType: IElementType, insertType: IElementType, textNode: String ) { val value = CompositeElement(insertConstantType) node.addChild(value, node.findChildByType(NULL)!!) node.removeChild(node.findChildByType(NULL)!!) value.addChild(LeafPsiElement(insertType, textNode)) } @Suppress("UnsafeCallOnNullableType") private fun replaceValueForString(node: ASTNode) { val value = CompositeElement(STRING_TEMPLATE) node.addChild(value, node.findChildByType(NULL)!!) node.removeChild(node.findChildByType(NULL)!!) value.addChild(LeafPsiElement(OPEN_QUOTE, "")) value.addChild(LeafPsiElement(CLOSING_QUOTE, "")) } @Suppress("KDOC_NO_CONSTRUCTOR_PROPERTY") // todo add proper docs private data class FixedParam( val insertConstantType: IElementType?, val insertType: IElementType?, val textNode: String, val isString: Boolean = false ) companion object { const val NAME_ID = "nullable-type" private val allowExpression = listOf("emptyList", "emptySequence", "emptyArray", "emptyMap", "emptySet", "listOf", "mapOf", "arrayOf", "sequenceOf", "setOf") } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/PreviewAnnotationRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter3 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.PREVIEW_ANNOTATION import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.KotlinParser import com.saveourtool.diktat.ruleset.utils.findAllNodesWithCondition import com.saveourtool.diktat.ruleset.utils.getAllChildrenWithType import com.saveourtool.diktat.ruleset.utils.getIdentifierName import org.jetbrains.kotlin.KtNodeTypes.ANNOTATION_ENTRY import org.jetbrains.kotlin.KtNodeTypes.FUN import org.jetbrains.kotlin.KtNodeTypes.MODIFIER_LIST import org.jetbrains.kotlin.com.intellij.lang.ASTFactory import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.lexer.KtTokens.ABSTRACT_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.INTERNAL_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.OPEN_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.PRIVATE_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.PROTECTED_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.PUBLIC_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.WHITE_SPACE import org.jetbrains.kotlin.psi.KtNamedFunction import org.jetbrains.kotlin.psi.psiUtil.isPrivate /** * This rule checks, whether the method has `@Preview` annotation (Jetpack Compose) * If so, such method should be private and function name should end with `Preview` suffix */ class PreviewAnnotationRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(PREVIEW_ANNOTATION) ) { override fun logic(node: ASTNode) { if (node.elementType == FUN) { checkFunctionSignature(node) } } private fun checkFunctionSignature(node: ASTNode) { node.findChildByType(MODIFIER_LIST)?.let { modList -> doCheck(node, modList) } } @Suppress("TOO_LONG_FUNCTION") private fun doCheck(functionNode: ASTNode, modeList: ASTNode) { if (modeList.getAllChildrenWithType(ANNOTATION_ENTRY).isEmpty()) { return } val previewAnnotationNode = modeList.getAllChildrenWithType(ANNOTATION_ENTRY).firstOrNull { it.text.contains("$ANNOTATION_SYMBOL$PREVIEW_ANNOTATION_TEXT") } previewAnnotationNode?.let { val functionNameNode = functionNode.getIdentifierName() val functionName = functionNameNode?.text ?: return // warn if function is not private if (!(functionNode.psi as KtNamedFunction).isPrivate()) { PREVIEW_ANNOTATION.warnAndFix( configRules, emitWarn, isFixMode, "$functionName method should be private", functionNode.startOffset, functionNode ) { addPrivateModifier(functionNode) } } // warn if function has no `Preview` suffix if (!isMethodHasPreviewSuffix(functionName)) { PREVIEW_ANNOTATION.warnAndFix( configRules, emitWarn, isFixMode, "$functionName method should has `Preview` suffix", functionNode.startOffset, functionNode ) { functionNode.replaceChild( functionNameNode, KotlinParser().createNode("${functionNameNode.text}Preview") ) } } } } private fun isMethodHasPreviewSuffix(functionName: String) = functionName.contains(PREVIEW_ANNOTATION_TEXT) private fun addPrivateModifier(functionNode: ASTNode) { // MODIFIER_LIST should be present since ANNOTATION_ENTRY is there val modifierListNode = functionNode.findChildByType(MODIFIER_LIST) ?: return val modifiersList = modifierListNode .getChildren(KtTokens.MODIFIER_KEYWORDS) .toList() val isMethodAbstract = modifiersList.any { it.elementType == ABSTRACT_KEYWORD } // private modifier is not applicable for abstract methods if (isMethodAbstract) { return } // these modifiers could be safely replaced via `private` val modifierForReplacement = modifiersList.firstOrNull { it.elementType in listOf( PUBLIC_KEYWORD, PROTECTED_KEYWORD, INTERNAL_KEYWORD, OPEN_KEYWORD ) } modifierForReplacement?.let { // replace current modifier with `private` val parent = it.treeParent parent.replaceChild(it, createPrivateModifierNode()) } ?: run { // the case, when there is no explicit modifier, i.e. `fun foo` // just add `private` in MODIFIER_LIST at the end // and move WHITE_SPACE before function identifier `fun` to MODIFIER_LIST val funNode = functionNode.findAllNodesWithCondition { it.text == "fun" }.single() val whiteSpaceAfterAnnotation = modifierListNode.treeNext modifierListNode.addChild(whiteSpaceAfterAnnotation, null) modifierListNode.addChild(createPrivateModifierNode(), null) // add ` ` node before `fun` functionNode.addChild(ASTFactory.leaf(WHITE_SPACE, " "), funNode) } } private fun createPrivateModifierNode() = ASTFactory.leaf(PRIVATE_KEYWORD, "private") companion object { const val ANNOTATION_SYMBOL = "@" const val NAME_ID = "preview-annotation" const val PREVIEW_ANNOTATION_TEXT = "Preview" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/RangeConventionalRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter3 import com.saveourtool.diktat.common.config.rules.RuleConfiguration import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.common.config.rules.getRuleConfig import com.saveourtool.diktat.ruleset.constants.Warnings.CONVENTIONAL_RANGE import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.KotlinParser import com.saveourtool.diktat.ruleset.utils.getIdentifierName import com.saveourtool.diktat.ruleset.utils.hasChildOfType import com.saveourtool.diktat.ruleset.utils.isWhiteSpace import com.saveourtool.diktat.ruleset.utils.parent import com.saveourtool.diktat.ruleset.utils.takeByChainOfTypes import org.jetbrains.kotlin.KtNodeTypes.BINARY_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.DOT_QUALIFIED_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.REFERENCE_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.VALUE_ARGUMENT import org.jetbrains.kotlin.KtNodeTypes.VALUE_ARGUMENT_LIST import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl import org.jetbrains.kotlin.com.intellij.psi.tree.TokenSet import org.jetbrains.kotlin.lexer.KtTokens.IDENTIFIER import org.jetbrains.kotlin.lexer.KtTokens.INTEGER_LITERAL import org.jetbrains.kotlin.lexer.KtTokens.MINUS import org.jetbrains.kotlin.lexer.KtTokens.RANGE import org.jetbrains.kotlin.psi.KtBinaryExpression import org.jetbrains.kotlin.psi.KtConstantExpression import org.jetbrains.kotlin.psi.KtDotQualifiedExpression /** * This rule warn and fix cases when it's possible to replace range operator `..` with infix function `until` * or replace `rangeTo` function with range operator `..` */ class RangeConventionalRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(CONVENTIONAL_RANGE) ) { private val configuration by lazy { RangeConventionalConfiguration( this.configRules.getRuleConfig(CONVENTIONAL_RANGE)?.configuration ?: emptyMap(), ) } override fun logic(node: ASTNode) { if (node.elementType == DOT_QUALIFIED_EXPRESSION && !configuration.isRangeToIgnore) { handleQualifiedExpression(node) } if (node.elementType == RANGE) { handleRange(node) } } @Suppress("TOO_MANY_LINES_IN_LAMBDA", "PARAMETER_NAME_IN_OUTER_LAMBDA") private fun handleQualifiedExpression(node: ASTNode) { (node.psi as KtDotQualifiedExpression).selectorExpression?.node?.let { if (it.findChildByType(REFERENCE_EXPRESSION)?.getIdentifierName()?.text == "rangeTo") { val arguments = it.findChildByType(VALUE_ARGUMENT_LIST)?.getChildren(TokenSet.create(VALUE_ARGUMENT)) if (arguments?.size == 1) { CONVENTIONAL_RANGE.warnAndFix(configRules, emitWarn, isFixMode, "replace `rangeTo` with `..`: ${node.text}", node.startOffset, node) { val receiverExpression = (node.psi as KtDotQualifiedExpression).receiverExpression.text val correctNode = KotlinParser().createNode("$receiverExpression..${arguments[0].text}") node.treeParent.addChild(correctNode, node) node.treeParent.removeChild(node) } } } } } @Suppress("TOO_MANY_LINES_IN_LAMBDA") private fun handleRange(node: ASTNode) { val binaryInExpression = node.parent(BINARY_EXPRESSION)?.psi as KtBinaryExpression? binaryInExpression ?.right ?.node // Unwrap parentheses and get `BINARY_EXPRESSION` on the RHS of `..` ?.takeByChainOfTypes(BINARY_EXPRESSION) ?.run { psi as? KtBinaryExpression } ?.takeIf { it.operationReference.node.hasChildOfType(MINUS) } ?.let { upperBoundExpression -> val isMinusOne = (upperBoundExpression.right as? KtConstantExpression)?.firstChild?.let { it.node.elementType == INTEGER_LITERAL && it.text == "1" } ?: false if (!isMinusOne) { return@let } // At this point we are sure that `upperBoundExpression` is `[left] - 1` and should be replaced. val errorNode = binaryInExpression.node CONVENTIONAL_RANGE.warnAndFix(configRules, emitWarn, isFixMode, "replace `..` with `until`: ${errorNode.text}", errorNode.startOffset, errorNode) { // Replace `..` with `until` replaceUntil(node) // fix right side of binary expression to correct form (remove ` - 1 `) : (b-1) -> (b) val astNode = upperBoundExpression.node val parent = astNode.treeParent parent.addChild(astNode.firstChildNode, astNode) parent.removeChild(astNode) } } } private fun replaceUntil(node: ASTNode) { val untilNode = LeafPsiElement(IDENTIFIER, "until") val parent = node.treeParent if (parent.treePrev?.isWhiteSpace() != true) { parent.treeParent.addChild(PsiWhiteSpaceImpl(" "), parent) } if (parent.treeNext?.isWhiteSpace() != true) { parent.treeParent.addChild(PsiWhiteSpaceImpl(" "), parent.treeNext) } parent.addChild(untilNode, node) parent.removeChild(node) } /** * * [RuleConfiguration] for rangeTo function */ class RangeConventionalConfiguration(config: Map) : RuleConfiguration(config) { /** * If true, don't suggest to replace `rangeTo` function with operator `..` */ val isRangeToIgnore = config["isRangeToIgnore"]?.toBoolean() ?: false } companion object { const val NAME_ID = "range" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/SingleLineStatementsRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter3 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.MORE_THAN_ONE_STATEMENT_PER_LINE import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.appendNewlineMergingWhiteSpace import com.saveourtool.diktat.ruleset.utils.extractLineOfText import com.saveourtool.diktat.ruleset.utils.isBeginByNewline import com.saveourtool.diktat.ruleset.utils.isFollowedByNewline import com.saveourtool.diktat.ruleset.utils.parent import org.jetbrains.kotlin.KtNodeTypes.ENUM_ENTRY import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl import org.jetbrains.kotlin.com.intellij.psi.tree.TokenSet import org.jetbrains.kotlin.lexer.KtTokens.SEMICOLON /** * Rule that looks for multiple statements on a single line separated with a `;` and splits them in multiple lines. */ class SingleLineStatementsRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(MORE_THAN_ONE_STATEMENT_PER_LINE) ) { override fun logic(node: ASTNode) { checkSemicolon(node) } private fun checkSemicolon(node: ASTNode) { node.getChildren(semicolonToken).forEach { astNode -> if (!astNode.isFollowedByNewline()) { MORE_THAN_ONE_STATEMENT_PER_LINE.warnAndFix(configRules, emitWarn, isFixMode, astNode.extractLineOfText(), astNode.startOffset, astNode) { if (astNode.treeParent.elementType == ENUM_ENTRY) { node.treeParent.addChild(PsiWhiteSpaceImpl("\n"), node.treeNext) } else { if (!astNode.isBeginByNewline()) { val nextNode = astNode.parent(false) { parent -> parent.treeNext != null }?.treeNext node.appendNewlineMergingWhiteSpace(nextNode, astNode) } node.removeChild(astNode) } } } } } companion object { const val NAME_ID = "statement" private val semicolonToken = TokenSet.create(SEMICOLON) } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/SortRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter3 import com.saveourtool.diktat.common.config.rules.RuleConfiguration import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.common.config.rules.getRuleConfig import com.saveourtool.diktat.ruleset.constants.Warnings.WRONG_DECLARATIONS_ORDER import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.findAllDescendantsWithSpecificType import com.saveourtool.diktat.ruleset.utils.hasChildOfType import com.saveourtool.diktat.ruleset.utils.isClassEnum import com.saveourtool.diktat.ruleset.utils.isPartOfComment import com.saveourtool.diktat.ruleset.utils.isWhiteSpace import com.saveourtool.diktat.ruleset.utils.isWhiteSpaceWithNewline import com.saveourtool.diktat.ruleset.utils.nextSibling import org.jetbrains.kotlin.KtNodeTypes.CLASS_BODY import org.jetbrains.kotlin.KtNodeTypes.ENUM_ENTRY import org.jetbrains.kotlin.KtNodeTypes.MODIFIER_LIST import org.jetbrains.kotlin.KtNodeTypes.PROPERTY import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl import org.jetbrains.kotlin.lexer.KtTokens.COMMA import org.jetbrains.kotlin.lexer.KtTokens.CONST_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.EOL_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.IDENTIFIER import org.jetbrains.kotlin.lexer.KtTokens.SEMICOLON import org.jetbrains.kotlin.psi.KtObjectDeclaration import org.jetbrains.kotlin.psi.psiUtil.siblings /** * Rule that sorts class properties and enum members alphabetically */ class SortRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(WRONG_DECLARATIONS_ORDER), ) { override fun logic(node: ASTNode) { val configuration = SortRuleConfiguration( configRules.getRuleConfig(WRONG_DECLARATIONS_ORDER)?.configuration ?: emptyMap() ) val classBody = node.findChildByType(CLASS_BODY) if (node.isClassEnum() && classBody != null && configuration.sortEnum) { sortEnum(classBody) } if ((node.psi as? KtObjectDeclaration)?.isCompanion() == true && classBody != null && configuration.sortProperty) { sortProperty(classBody) } } @Suppress("UnsafeCallOnNullableType") private fun sortProperty(node: ASTNode) { val propertyList = node .getChildren(null) .filter { it.elementType == PROPERTY } .filter { it.hasChildOfType(MODIFIER_LIST) } .filter { it.findChildByType(MODIFIER_LIST)!!.hasChildOfType(CONST_KEYWORD) } if (propertyList.size <= 1) { return } val consecutivePropertiesGroups = createOrderListOfList(propertyList) val sortedListOfList = consecutivePropertiesGroups.map { group -> group.sortedBy { it.findChildByType(IDENTIFIER)!!.text } } consecutivePropertiesGroups.forEachIndexed { index, mutableList -> if (mutableList != sortedListOfList[index]) { WRONG_DECLARATIONS_ORDER.warnAndFix(configRules, emitWarn, isFixMode, "constant properties inside companion object order is incorrect", mutableList.first().startOffset, mutableList.first()) { swapSortNodes(sortedListOfList[index], mutableList, node) } } } } @OptIn(ExperimentalStdlibApi::class) private fun swapSortNodes( sortList: List, nonSortList: List, node: ASTNode ) { val isEnum = nonSortList.first().elementType == ENUM_ENTRY val spaceBefore = if (node.findAllDescendantsWithSpecificType(EOL_COMMENT).isNotEmpty() && isEnum) { nonSortList.last().run { if (this.hasChildOfType(EOL_COMMENT) && !this.hasChildOfType(COMMA)) { this.addChild(LeafPsiElement(COMMA, ","), this.findChildByType(EOL_COMMENT)) } } buildList(nonSortList.size) { add(null) repeat(nonSortList.size - 1) { add(listOf(PsiWhiteSpaceImpl("\n"))) } } } else { nonSortList.map { astNode -> astNode .siblings(false) .toList() .takeWhile { it.isWhiteSpace() || it.isPartOfComment() } .ifEmpty { null } } } val nodeInsertBefore: ASTNode? = nonSortList.last().treeNext node.removeRange(nonSortList.first(), nonSortList.last().treeNext) sortList.mapIndexed { index, astNode -> spaceBefore[index]?.let { prevList -> prevList.map { node.addChild(it, nodeInsertBefore) } } if (!astNode.hasChildOfType(COMMA) && isEnum) { astNode.addChild(LeafPsiElement(COMMA, ","), null) } node.addChild(astNode, nodeInsertBefore) } } @Suppress("TYPE_ALIAS") private fun createOrderListOfList(propertyList: List): List> { val oneOrderList = mutableListOf(propertyList.first()) val orderListOfList: MutableList> = mutableListOf() propertyList.zipWithNext().forEach { (currentProperty, nextProperty) -> if (currentProperty.nextSibling { it.elementType == PROPERTY } != nextProperty) { orderListOfList.add(oneOrderList.toMutableList()) oneOrderList.clear() } oneOrderList.add(nextProperty) } orderListOfList.add(oneOrderList) return orderListOfList.toList() } @Suppress("UnsafeCallOnNullableType") private fun sortEnum(node: ASTNode) { val enumEntryList = node.getChildren(null).filter { it.elementType == ENUM_ENTRY } if (enumEntryList.size <= 1) { return } val sortList = enumEntryList.sortedBy { it.findChildByType(IDENTIFIER)!!.text } if (enumEntryList != sortList) { WRONG_DECLARATIONS_ORDER.warnAndFix(configRules, emitWarn, isFixMode, "enum entries order is incorrect", node.startOffset, node) { val (isEndSemicolon, isEndSpace) = removeLastSemicolonAndSpace(enumEntryList.last()) val hasTrailingComma = (sortList.last() != enumEntryList.last() && enumEntryList.last().hasChildOfType(COMMA)) swapSortNodes(sortList, enumEntryList, node) if (!hasTrailingComma) { val lastEntry = node.findAllDescendantsWithSpecificType(ENUM_ENTRY).last() lastEntry.removeChild(lastEntry.findChildByType(COMMA)!!) } if (isEndSpace) { sortList.last().addChild(PsiWhiteSpaceImpl("\n"), null) } if (isEndSemicolon) { sortList.last().addChild(LeafPsiElement(SEMICOLON, ";"), null) } } } } @Suppress("UnsafeCallOnNullableType") private fun removeLastSemicolonAndSpace(node: ASTNode): Pair { val isSemicolon = node.hasChildOfType(SEMICOLON) if (isSemicolon) { node.removeChild(node.findChildByType(SEMICOLON)!!) } val isSpace = node.lastChildNode.isWhiteSpaceWithNewline() if (isSpace) { node.removeChild(node.lastChildNode) } return Pair(isSemicolon, isSpace) } /** * [RuleConfiguration] for rule that sorts class members */ class SortRuleConfiguration(config: Map) : RuleConfiguration(config) { /** * Whether enum members should be sorted alphabetically */ val sortEnum = config["sortEnum"]?.toBoolean() ?: false /** * Whether class properties should be sorted alphabetically */ val sortProperty = config["sortProperty"]?.toBoolean() ?: false } companion object { const val NAME_ID = "sort-rule" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/StringConcatenationRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter3 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.STRING_CONCATENATION import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.* import org.jetbrains.kotlin.KtNodeTypes.BINARY_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.DOT_QUALIFIED_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.OPERATION_REFERENCE import org.jetbrains.kotlin.KtNodeTypes.STRING_TEMPLATE import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.lexer.KtTokens.PLUS import org.jetbrains.kotlin.psi.KtBinaryExpression import org.jetbrains.kotlin.psi.KtCallExpression import org.jetbrains.kotlin.psi.KtConstantExpression import org.jetbrains.kotlin.psi.KtDotQualifiedExpression import org.jetbrains.kotlin.psi.KtExpression import org.jetbrains.kotlin.psi.KtParenthesizedExpression import org.jetbrains.kotlin.psi.KtReferenceExpression import org.jetbrains.kotlin.psi.KtStringTemplateExpression /** * This rule covers checks and fixes related to string concatenation. * Rule 3.8 prohibits string concatenation and suggests to use string templates instead * if this expressions fits one line. For example: * """ string """ + "string" will be converted to "string string" * "string " + 1 will be converted to "string 1" * "string one " + "string two " */ class StringConcatenationRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf( STRING_CONCATENATION ) ) { @Suppress("COLLAPSE_IF_STATEMENTS") override fun logic(node: ASTNode) { if (node.elementType == BINARY_EXPRESSION) { // searching top-level binary expression to detect any operations with "plus" (+) // string concatenation is prohibited only for single line statements if (node.findParentNodeWithSpecificType(BINARY_EXPRESSION) == null && isSingleLineStatement(node)) { detectStringConcatenation(node) } } } private fun isSingleLineStatement(node: ASTNode): Boolean = !node.text.contains("\n") /** * This method works only with top-level binary expressions. It should be checked before the call. */ @Suppress("AVOID_NULL_CHECKS") private fun detectStringConcatenation(topLevelBinaryExpr: ASTNode) { val allBinaryExpressions = topLevelBinaryExpr.findAllDescendantsWithSpecificType(BINARY_EXPRESSION) val nodeWithBug = allBinaryExpressions.find { isDetectStringConcatenationInExpression(it) } if (nodeWithBug != null) { STRING_CONCATENATION.warnAndFix( configRules, emitWarn, this.isFixMode, topLevelBinaryExpr.text.lines().first(), nodeWithBug.startOffset, nodeWithBug ) { fixBinaryExpressionWithConcatenation(nodeWithBug) loop(topLevelBinaryExpr.treeParent) } } } private fun loop(parentTopLevelBinaryExpr: ASTNode) { val allBinaryExpressions = parentTopLevelBinaryExpr.findAllDescendantsWithSpecificType(BINARY_EXPRESSION) val nodeWithBug = allBinaryExpressions.find { isDetectStringConcatenationInExpression(it) } val bugDetected = nodeWithBug != null if (bugDetected) { fixBinaryExpressionWithConcatenation(nodeWithBug) loop(parentTopLevelBinaryExpr) } } /** * We can detect string concatenation by the first (left) operand in binary expression. * If it is of type string - then we found string concatenation. * If the right value is not a constant string then don't change them to template. */ private fun isDetectStringConcatenationInExpression(node: ASTNode): Boolean { require(node.elementType == BINARY_EXPRESSION) { "cannot process non binary expression in the process of detecting string concatenation" } val firstChild = node.firstChildNode val lastChild = node.lastChildNode return isPlusBinaryExpression(node) && isStringVar(firstChild, lastChild) } private fun isStringVar(firstChild: ASTNode, lastChild: ASTNode) = firstChild.elementType == STRING_TEMPLATE || ((firstChild.text.endsWith("toString()")) && firstChild.elementType == DOT_QUALIFIED_EXPRESSION && lastChild.elementType == STRING_TEMPLATE) @Suppress("COMMENT_WHITE_SPACE") private fun isPlusBinaryExpression(node: ASTNode): Boolean { require(node.elementType == BINARY_EXPRESSION) // binary expression // / | \ // expr1 operationRef expr2 val operationReference = node.getFirstChildWithType(OPERATION_REFERENCE) return operationReference ?.getFirstChildWithType(PLUS) != null } private fun fixBinaryExpressionWithConcatenation(node: ASTNode?) { val binaryExpressionPsi = node?.psi as KtBinaryExpression val parentNode = node.treeParent val textNode = checkKtExpression(binaryExpressionPsi) val newNode = KotlinParser().createNode("\"$textNode\"") parentNode.replaceChild(node, newNode) } private fun isPlusBinaryExpressionAndFirstElementString(binaryExpressionNode: KtBinaryExpression) = (binaryExpressionNode.left is KtStringTemplateExpression) && PLUS == binaryExpressionNode.operationToken @Suppress( "TOO_LONG_FUNCTION", "NESTED_BLOCK", "SAY_NO_TO_VAR", "ComplexMethod" ) private fun checkKtExpression(binaryExpressionPsi: KtBinaryExpression): String { var lvalueText = binaryExpressionPsi.left?.text?.trim('"') val rvalueText = binaryExpressionPsi.right?.text if (binaryExpressionPsi.isLvalueDotQualifiedExpression() && binaryExpressionPsi.firstChild.text.endsWith("toString()")) { // =========== (1 + 2).toString() -> ${(1 + 2)} val leftText = binaryExpressionPsi.firstChild.firstChild.text lvalueText = "\${$leftText}" } if (binaryExpressionPsi.isLvalueReferenceExpression() || binaryExpressionPsi.isLvalueConstantExpression()) { return binaryExpressionPsi.text } if (binaryExpressionPsi.isLvalueBinaryExpression()) { val rightValue = checkKtExpression(binaryExpressionPsi.left as KtBinaryExpression) val rightEx = binaryExpressionPsi.right val rightVal = if (binaryExpressionPsi.isRvalueParenthesized()) { checkKtExpression(rightEx?.children?.get(0) as KtBinaryExpression) } else { (rightEx?.text?.trim('"')) } if (binaryExpressionPsi.left?.text == rightValue) { return binaryExpressionPsi.text } return "$rightValue$rightVal" } else if (binaryExpressionPsi.isRvalueConstantExpression() || binaryExpressionPsi.isRvalueStringTemplateExpression()) { // =========== "a " + "b" -> "a b" val rvalueTextNew = rvalueText?.trim('"') return "$lvalueText$rvalueTextNew" } else if (binaryExpressionPsi.isRvalueCallExpression()) { // =========== "a " + foo() -> "a ${foo()}}" return "$lvalueText\${$rvalueText}" } else if (binaryExpressionPsi.isRvalueReferenceExpression()) { // =========== "a " + b -> "a $b" return "$lvalueText$$rvalueText" } else if (!binaryExpressionPsi.isRvalueParenthesized() && binaryExpressionPsi.isRvalueExpression()) { return "$lvalueText\${$rvalueText}" } else if (binaryExpressionPsi.isRvalueParenthesized()) { val binExpression = binaryExpressionPsi.right?.children?.first() if (binExpression is KtBinaryExpression) { if (isPlusBinaryExpressionAndFirstElementString(binExpression)) { val rightValue = checkKtExpression(binExpression) return "$lvalueText$rightValue" } else if (binExpression.isLvalueBinaryExpression()) { val rightValue = checkKtExpression(binExpression.left as KtBinaryExpression) val rightEx = binExpression.right val rightVal = if (binExpression.isRvalueParenthesized()) { checkKtExpression(rightEx?.children?.get(0) as KtBinaryExpression) } else { (rightEx?.text?.trim('"')) } if (binExpression.left?.text == rightValue) { return "$lvalueText\${$rvalueText}" } return "$lvalueText$rightValue$rightVal" } } return "$lvalueText\${$rvalueText}" } return binaryExpressionPsi.text } private fun KtBinaryExpression.isRvalueConstantExpression() = this.right is KtConstantExpression private fun KtBinaryExpression.isRvalueStringTemplateExpression() = this.right is KtStringTemplateExpression private fun KtBinaryExpression.isRvalueCallExpression() = this.right is KtCallExpression private fun KtBinaryExpression.isRvalueReferenceExpression() = this.right is KtReferenceExpression private fun KtBinaryExpression.isRvalueParenthesized() = this.right is KtParenthesizedExpression private fun KtBinaryExpression.isLvalueDotQualifiedExpression() = this.left is KtDotQualifiedExpression private fun KtBinaryExpression.isLvalueBinaryExpression() = this.left is KtBinaryExpression private fun KtBinaryExpression.isLvalueReferenceExpression() = this.left is KtReferenceExpression private fun KtBinaryExpression.isLvalueConstantExpression() = this.left is KtConstantExpression private fun KtBinaryExpression.isRvalueExpression() = this.right is KtExpression companion object { const val NAME_ID = "string-concatenation" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/StringTemplateFormatRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter3 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.STRING_TEMPLATE_CURLY_BRACES import com.saveourtool.diktat.ruleset.constants.Warnings.STRING_TEMPLATE_QUOTES import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.findAllDescendantsWithSpecificType import com.saveourtool.diktat.ruleset.utils.hasAnyChildOfTypes import org.jetbrains.kotlin.KtNodeTypes.ARRAY_ACCESS_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.FLOAT_CONSTANT import org.jetbrains.kotlin.KtNodeTypes.INTEGER_CONSTANT import org.jetbrains.kotlin.KtNodeTypes.LITERAL_STRING_TEMPLATE_ENTRY import org.jetbrains.kotlin.KtNodeTypes.LONG_STRING_TEMPLATE_ENTRY import org.jetbrains.kotlin.KtNodeTypes.REFERENCE_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.SHORT_STRING_TEMPLATE_ENTRY import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.CompositeElement import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement import org.jetbrains.kotlin.lexer.KtTokens.CLOSING_QUOTE import org.jetbrains.kotlin.lexer.KtTokens.IDENTIFIER import org.jetbrains.kotlin.lexer.KtTokens.SHORT_TEMPLATE_ENTRY_START /** * In String templates there should not be redundant curly braces. In case of using a not complex statement (one argument) * there should not be curly braces. * * FixMe: The important caveat here: in "$foo" kotlin compiler adds implicit call to foo.toString() in case foo type is not string. */ class StringTemplateFormatRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(STRING_TEMPLATE_CURLY_BRACES, STRING_TEMPLATE_QUOTES) ) { override fun logic(node: ASTNode) { when (node.elementType) { LONG_STRING_TEMPLATE_ENTRY -> handleLongStringTemplate(node) SHORT_STRING_TEMPLATE_ENTRY -> handleShortStringTemplate(node) else -> { } } } @Suppress("UnsafeCallOnNullableType") private fun handleLongStringTemplate(node: ASTNode) { // Checking if in long templates {a.foo()} there are function calls or class toString call if (bracesCanBeOmitted(node)) { STRING_TEMPLATE_CURLY_BRACES.warnAndFix(configRules, emitWarn, isFixMode, node.text, node.startOffset, node) { val identifierName = node.findChildByType(REFERENCE_EXPRESSION) identifierName?.let { val shortTemplate = CompositeElement(SHORT_STRING_TEMPLATE_ENTRY) val reference = CompositeElement(REFERENCE_EXPRESSION) node.treeParent.addChild(shortTemplate, node) shortTemplate.addChild(LeafPsiElement(SHORT_TEMPLATE_ENTRY_START, "$"), null) shortTemplate.addChild(reference) reference.addChild(LeafPsiElement(IDENTIFIER, identifierName.text)) node.treeParent.removeChild(node) } ?: run { val stringTemplate = node.treeParent val appropriateText = node.text.trim('$', '{', '}') stringTemplate.addChild(LeafPsiElement(LITERAL_STRING_TEMPLATE_ENTRY, appropriateText), node) stringTemplate.removeChild(node) } } } } @Suppress("UnsafeCallOnNullableType") private fun handleShortStringTemplate(node: ASTNode) { val identifierName = node.findChildByType(REFERENCE_EXPRESSION)?.text if (identifierName != null && node.treeParent.text.trim('"', '$') == identifierName) { STRING_TEMPLATE_QUOTES.warnAndFix(configRules, emitWarn, isFixMode, node.text, node.startOffset, node) { val identifier = node.findChildByType(REFERENCE_EXPRESSION)!! // node.treeParent is String template that we need to delete node.treeParent.treeParent.addChild(identifier, node.treeParent) node.treeParent.treeParent.removeChild(node.treeParent) } } } @Suppress("UnsafeCallOnNullableType", "FUNCTION_BOOLEAN_PREFIX") private fun bracesCanBeOmitted(node: ASTNode): Boolean { val onlyOneRefExpr = node .findAllDescendantsWithSpecificType(REFERENCE_EXPRESSION) .singleOrNull() ?.treeParent ?.elementType == LONG_STRING_TEMPLATE_ENTRY val isArrayAccessExpression = node // this should be omitted in previous expression, used for safe warranties .findAllDescendantsWithSpecificType(REFERENCE_EXPRESSION) .singleOrNull() ?.treeParent ?.elementType == ARRAY_ACCESS_EXPRESSION return if (onlyOneRefExpr && !isArrayAccessExpression) { (!(node.treeNext .text .first() // checking if first letter is valid .isLetterOrDigit() || node.treeNext.text.startsWith("_")) || node.treeNext.elementType == CLOSING_QUOTE ) } else if (!isArrayAccessExpression) { node.hasAnyChildOfTypes(FLOAT_CONSTANT, INTEGER_CONSTANT) // it also fixes "${1.0}asd" cases } else { false } } companion object { const val NAME_ID = "string-template-format" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/TrailingCommaRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter3 import com.saveourtool.diktat.common.config.rules.RuleConfiguration import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.common.config.rules.getCommonConfiguration import com.saveourtool.diktat.common.config.rules.getRuleConfig import com.saveourtool.diktat.ruleset.constants.Warnings.TRAILING_COMMA import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.isPartOfComment import com.saveourtool.diktat.ruleset.utils.isWhiteSpaceWithNewline import io.github.oshai.kotlinlogging.KotlinLogging import org.jetbrains.kotlin.KtNodeTypes.COLLECTION_LITERAL_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.DESTRUCTURING_DECLARATION import org.jetbrains.kotlin.KtNodeTypes.DESTRUCTURING_DECLARATION_ENTRY import org.jetbrains.kotlin.KtNodeTypes.INDICES import org.jetbrains.kotlin.KtNodeTypes.REFERENCE_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.STRING_TEMPLATE import org.jetbrains.kotlin.KtNodeTypes.TYPE_ARGUMENT_LIST import org.jetbrains.kotlin.KtNodeTypes.TYPE_PARAMETER import org.jetbrains.kotlin.KtNodeTypes.TYPE_PARAMETER_LIST import org.jetbrains.kotlin.KtNodeTypes.TYPE_PROJECTION import org.jetbrains.kotlin.KtNodeTypes.VALUE_ARGUMENT import org.jetbrains.kotlin.KtNodeTypes.VALUE_ARGUMENT_LIST import org.jetbrains.kotlin.KtNodeTypes.VALUE_PARAMETER import org.jetbrains.kotlin.KtNodeTypes.VALUE_PARAMETER_LIST import org.jetbrains.kotlin.KtNodeTypes.WHEN_CONDITION_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.WHEN_CONDITION_IN_RANGE import org.jetbrains.kotlin.KtNodeTypes.WHEN_CONDITION_IS_PATTERN import org.jetbrains.kotlin.KtNodeTypes.WHEN_ENTRY import org.jetbrains.kotlin.com.intellij.lang.ASTFactory import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.kdoc.lexer.KDocTokens.KDOC import org.jetbrains.kotlin.lexer.KtTokens.BLOCK_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.COMMA import org.jetbrains.kotlin.lexer.KtTokens.EOL_COMMENT import org.jetbrains.kotlin.psi.psiUtil.children import org.jetbrains.kotlin.psi.psiUtil.siblings /** * [1] Enumerations (In another rule) * [2] Value arguments * [3] Class properties and parameters * [4] Function value parameters * [5] Parameters with optional type (including setters) * [6] Indexing suffix * [7] Lambda parameters * [8] when entry * [9] Collection literals (in annotations)Type arguments * [10] Type arguments * [11] Type parameters * [12] Destructuring declarations */ @Suppress("TOO_LONG_FUNCTION") class TrailingCommaRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(TRAILING_COMMA) ) { private val commonConfig = configRules.getCommonConfiguration() private val trailingConfig = this.configRules.getRuleConfig(TRAILING_COMMA)?.configuration ?: emptyMap() private val configuration by lazy { if (trailingConfig.isEmpty()) { log.warn { "You have enabled TRAILING_COMMA, but rule will remain inactive until you explicitly set" + " configuration options. See [available-rules.md] for possible configuration options." } } TrailingCommaConfiguration(trailingConfig) } override fun logic(node: ASTNode) { if (commonConfig.kotlinVersion >= ktVersion) { val (type, config) = when (node.elementType) { VALUE_ARGUMENT_LIST -> Pair(VALUE_ARGUMENT, configuration.getParam("valueArgument")) VALUE_PARAMETER_LIST -> Pair(VALUE_PARAMETER, configuration.getParam("valueParameter")) INDICES -> Pair(REFERENCE_EXPRESSION, configuration.getParam("referenceExpression")) WHEN_ENTRY -> { val lastChildType = node .children() .toList() .findLast { it.elementType in whenChildrenTypes } ?.elementType Pair(lastChildType, configuration.getParam("whenConditions")) } COLLECTION_LITERAL_EXPRESSION -> Pair(STRING_TEMPLATE, configuration.getParam("collectionLiteral")) TYPE_ARGUMENT_LIST -> Pair(TYPE_PROJECTION, configuration.getParam("typeArgument")) TYPE_PARAMETER_LIST -> Pair(TYPE_PARAMETER, configuration.getParam("typeParameter")) DESTRUCTURING_DECLARATION -> Pair( DESTRUCTURING_DECLARATION_ENTRY, configuration.getParam("destructuringDeclaration") ) else -> return } val astNode = node .children() .toList() .lastOrNull { it.elementType == type } astNode?.checkTrailingComma(config) } } private fun ASTNode.checkTrailingComma(config: Boolean) { val noCommaInSiblings = siblings(true).toSet() .let { siblings -> siblings.none { it.elementType == COMMA } && siblings.any { it.isWhiteSpaceWithNewline() || it.isPartOfComment() } } val noCommaInChildren = children().none { it.elementType == COMMA } val shouldFix = noCommaInSiblings && noCommaInChildren if (shouldFix && config) { // we should write type of node in warning, to make it easier for user to find the parameter TRAILING_COMMA.warnAndFix(configRules, emitWarn, isFixMode, "after ${this.elementType}: ${this.text}", this.startOffset, this) { val parent = this.treeParent // In case, when we got VALUE_PARAMETER, it may contain comments, which follows the actual parameter and all of them are actually in the same node // Ex: `class A(val a: Int, val b: Int // comment)` // `val b: Int // comment` --> the whole expression is VALUE_PARAMETER // So, in this case we must insert comma before the comment, in other cases we will insert it after current node val comments = listOf(EOL_COMMENT, BLOCK_COMMENT, KDOC) val firstCommentNodeOrNull = if (this.elementType == VALUE_PARAMETER) this.children().firstOrNull { it.elementType in comments } else null firstCommentNodeOrNull?.let { this.addChild(ASTFactory.leaf(COMMA, ","), it) } ?: parent.addChild(ASTFactory.leaf(COMMA, ","), this.treeNext) } } } /** * Configuration for trailing comma */ class TrailingCommaConfiguration(config: Map) : RuleConfiguration(config) { /** * @param name parameters name * @return param based on its name */ fun getParam(name: String) = config[name]?.toBoolean() ?: false } companion object { private val log = KotlinLogging.logger {} const val NAME_ID = "trailing-comma" val ktVersion = KotlinVersion(1, 4) val whenChildrenTypes = listOf(WHEN_CONDITION_EXPRESSION, WHEN_CONDITION_IS_PATTERN, WHEN_CONDITION_IN_RANGE) } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/WhenMustHaveElseRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter3 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.WHEN_WITHOUT_ELSE import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.appendNewlineMergingWhiteSpace import com.saveourtool.diktat.ruleset.utils.getAllChildrenWithType import com.saveourtool.diktat.ruleset.utils.getFirstChildWithType import com.saveourtool.diktat.ruleset.utils.hasChildOfType import com.saveourtool.diktat.ruleset.utils.hasParent import com.saveourtool.diktat.ruleset.utils.isBeginByNewline import com.saveourtool.diktat.ruleset.utils.prevSibling import org.jetbrains.kotlin.KtNodeTypes import org.jetbrains.kotlin.KtNodeTypes.BINARY_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.BLOCK import org.jetbrains.kotlin.KtNodeTypes.DOT_QUALIFIED_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.FUNCTION_LITERAL import org.jetbrains.kotlin.KtNodeTypes.OPERATION_REFERENCE import org.jetbrains.kotlin.KtNodeTypes.PROPERTY import org.jetbrains.kotlin.KtNodeTypes.REFERENCE_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.RETURN import org.jetbrains.kotlin.KtNodeTypes.WHEN_CONDITION_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.WHEN_CONDITION_IN_RANGE import org.jetbrains.kotlin.KtNodeTypes.WHEN_CONDITION_IS_PATTERN import org.jetbrains.kotlin.KtNodeTypes.WHEN_ENTRY import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.CompositeElement import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.java.PsiBlockStatementImpl import org.jetbrains.kotlin.lexer.KtTokens.ARROW import org.jetbrains.kotlin.lexer.KtTokens.ELSE_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.EOL_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.EQ import org.jetbrains.kotlin.lexer.KtTokens.LBRACE import org.jetbrains.kotlin.lexer.KtTokens.RBRACE import org.jetbrains.kotlin.psi.KtWhenExpression /** * Rule 3.10: 'when' statement must have else branch, unless when condition variable is enumerated or sealed type * * Current limitations and FixMe: * If a when statement of type enum or sealed contains all values of a enum - there is no need to have "else" branch. * The compiler can issue a warning when it is missing. */ @Suppress("ForbiddenComment") class WhenMustHaveElseRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(WHEN_WITHOUT_ELSE) ) { override fun logic(node: ASTNode) { if (node.elementType == KtNodeTypes.WHEN && isStatement(node)) { checkEntries(node) } } private fun checkEntries(node: ASTNode) { if (!hasElse(node) && !isOnlyEnumEntries(node)) { WHEN_WITHOUT_ELSE.warnAndFix(configRules, emitWarn, isFixMode, "else was not found", node.startOffset, node) { val whenEntryElse = CompositeElement(WHEN_ENTRY) if (!node.lastChildNode.isBeginByNewline()) { node.appendNewlineMergingWhiteSpace(node.lastChildNode.treePrev, node.lastChildNode) } node.addChild(whenEntryElse, node.lastChildNode) addChildren(whenEntryElse) if (!whenEntryElse.isBeginByNewline()) { node.addChild(PsiWhiteSpaceImpl("\n"), whenEntryElse) } } } } private fun isOnlyEnumEntries(node: ASTNode): Boolean { val whenEntries = node.getAllChildrenWithType(WHEN_ENTRY) val hasConditionsIsPattern = whenEntries.any { it.hasChildOfType(WHEN_CONDITION_IS_PATTERN) } if (hasConditionsIsPattern) { return false } val conditionsInRange = whenEntries.map { it.getAllChildrenWithType(WHEN_CONDITION_IN_RANGE) }.flatten() val conditionsWithExpression = whenEntries.map { it.getAllChildrenWithType(WHEN_CONDITION_EXPRESSION) }.flatten() val areOnlyEnumEntriesWithExpressions = if (conditionsWithExpression.isNotEmpty()) { conditionsWithExpression.all { it.hasChildOfType(DOT_QUALIFIED_EXPRESSION) || it.hasChildOfType(REFERENCE_EXPRESSION) } } else { true } val areOnlyEnumEntriesInRanges = if (conditionsInRange.isNotEmpty()) { conditionsInRange.map { it.getFirstChildWithType(BINARY_EXPRESSION) } .all { val dotExpressionsCount = it?.getAllChildrenWithType(DOT_QUALIFIED_EXPRESSION)?.size ?: 0 val referenceExpressionsCount = it?.getAllChildrenWithType(REFERENCE_EXPRESSION)?.size ?: 0 dotExpressionsCount + referenceExpressionsCount == 2 } } else { true } if (areOnlyEnumEntriesWithExpressions && areOnlyEnumEntriesInRanges) { return true } return false } private fun isStatement(node: ASTNode): Boolean { // Checks if there is return before when if (node.hasParent(RETURN)) { return false } // Checks if `when` is the last statement in lambda body if (node.treeParent.elementType == BLOCK && node.treeParent.treeParent.elementType == FUNCTION_LITERAL && node.treeParent.lastChildNode == node) { return false } if (node.treeParent.elementType == WHEN_ENTRY && node.prevSibling { it.elementType == ARROW } != null) { // `when` is used as a branch in another `when` return false } return node.prevSibling { it.elementType == EQ || it.elementType == OPERATION_REFERENCE && it.firstChildNode.elementType == EQ } ?.let { // `when` is used in an assignment or in a function with expression body false } ?: !node.hasParent(PROPERTY) } /** * Check if this `when` has `else` branch. If `else` branch is empty, `(node.psi as KtWhenExpression).elseExpression` returns `null`, * so we need to manually check if any entry contains `else` keyword. */ private fun hasElse(node: ASTNode): Boolean = (node.psi as KtWhenExpression).entries.any { it.isElse } private fun addChildren(node: ASTNode) { val block = PsiBlockStatementImpl() node.apply { addChild(LeafPsiElement(ELSE_KEYWORD, "else"), null) addChild(PsiWhiteSpaceImpl(" "), null) addChild(LeafPsiElement(ARROW, "->"), null) addChild(PsiWhiteSpaceImpl(" "), null) addChild(block, null) } block.apply { addChild(LeafPsiElement(LBRACE, "{"), null) addChild(PsiWhiteSpaceImpl("\n"), null) addChild(LeafPsiElement(EOL_COMMENT, "// this is a generated else block"), null) addChild(PsiWhiteSpaceImpl("\n"), null) addChild(LeafPsiElement(RBRACE, "}"), null) } } companion object { const val NAME_ID = "no-else-in-when" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/files/BlankLinesRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter3.files import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.TOO_MANY_BLANK_LINES import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.* import org.jetbrains.kotlin.KtNodeTypes.BLOCK import org.jetbrains.kotlin.KtNodeTypes.CLASS_BODY import org.jetbrains.kotlin.KtNodeTypes.FUNCTION_LITERAL import org.jetbrains.kotlin.KtNodeTypes.LAMBDA_ARGUMENT import org.jetbrains.kotlin.KtNodeTypes.SCRIPT import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.lexer.KtTokens.LBRACE import org.jetbrains.kotlin.lexer.KtTokens.RBRACE import org.jetbrains.kotlin.lexer.KtTokens.WHITE_SPACE import org.jetbrains.kotlin.psi.stubs.elements.KtFileElementType /** * This rule checks usage of blank lines in code. * 1. Checks that no more than two consecutive blank lines are used in a row * 2. Checks that blank lines are not put in the beginning or at the end of code blocks with curly braces */ class BlankLinesRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(TOO_MANY_BLANK_LINES), ) { override fun logic(node: ASTNode) { if (node.elementType == WHITE_SPACE) { // note that no blank lines counts as one newline if (node.numNewLines() == 2) { handleBlankLine(node) } else if (node.numNewLines() > 2) { handleTooManyBlankLines(node) } } } private fun handleBlankLine(node: ASTNode) { if (node.treeParent.let { // kts files are parsed as a SCRIPT node containing BLOCK, therefore WHITE_SPACEs from these BLOCKS shouldn't be checked it.elementType == BLOCK && it.treeParent?.elementType != SCRIPT || it.elementType == CLASS_BODY || it.elementType == FUNCTION_LITERAL }) { node.findParentNodeWithSpecificType(LAMBDA_ARGUMENT)?.let { // Lambda body is always has a BLOCK -> run { } - (LBRACE, WHITE_SPACE, BLOCK "", RBRACE) if (node.treeNext.text.isEmpty()) { return } } if ((node.treeNext.elementType == RBRACE) xor (node.treePrev.elementType == LBRACE)) { // if both are present, this is not beginning or end // if both are null, then this block is empty and is handled in another rule val freeText = "do not put newlines ${if (node.treePrev.elementType == LBRACE) "in the beginning" else "at the end"} of code blocks" TOO_MANY_BLANK_LINES.warnAndFix(configRules, emitWarn, isFixMode, freeText, node.startOffset, node) { node.leaveOnlyOneNewLine() } } } } private fun handleTooManyBlankLines(node: ASTNode) { TOO_MANY_BLANK_LINES.warnAndFix(configRules, emitWarn, isFixMode, "do not use more than two consecutive blank lines", node.startOffset, node) { if (node.treeParent.elementType != KtFileElementType.INSTANCE && (node.treeParent.getFirstChildWithType(WHITE_SPACE) == node || node.treeParent.getAllChildrenWithType(WHITE_SPACE).last() == node)) { node.leaveExactlyNumNewLines(1) } else { node.leaveExactlyNumNewLines(2) } } } companion object { const val NAME_ID = "blank-lines" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/files/FileSize.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter3.files import com.saveourtool.diktat.common.config.rules.RuleConfiguration import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.common.config.rules.getRuleConfig import com.saveourtool.diktat.ruleset.constants.Warnings.FILE_IS_TOO_LONG import com.saveourtool.diktat.ruleset.rules.DiktatRule import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.psi.stubs.elements.KtFileElementType /** * Rule that checks number of lines in a file */ class FileSize(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(FILE_IS_TOO_LONG) ) { private val configuration by lazy { FileSizeConfiguration( this.configRules.getRuleConfig(FILE_IS_TOO_LONG)?.configuration ?: emptyMap() ) } override fun logic(node: ASTNode) { if (node.elementType == KtFileElementType.INSTANCE) { checkFileSize(node, configuration.maxSize) } } private fun checkFileSize(node: ASTNode, maxSize: Long) { val size = node .text .split("\n") .size if (size > maxSize) { FILE_IS_TOO_LONG.warn(configRules, emitWarn, size.toString(), node.startOffset, node) } } /** * [RuleConfiguration] for maximun number of lines in a file */ class FileSizeConfiguration(config: Map) : RuleConfiguration(config) { /** * Maximum allowed number of lines in a file */ val maxSize = config["maxSize"]?.toLongOrNull() ?: MAX_SIZE } companion object { const val MAX_SIZE = 2000L const val NAME_ID = "file-size" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/files/FileStructureRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter3.files import com.saveourtool.diktat.common.config.rules.RuleConfiguration import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.common.config.rules.getCommonConfiguration import com.saveourtool.diktat.common.config.rules.getRuleConfig import com.saveourtool.diktat.ruleset.constants.Warnings.FILE_CONTAINS_ONLY_COMMENTS import com.saveourtool.diktat.ruleset.constants.Warnings.FILE_INCORRECT_BLOCKS_ORDER import com.saveourtool.diktat.ruleset.constants.Warnings.FILE_NO_BLANK_LINE_BETWEEN_BLOCKS import com.saveourtool.diktat.ruleset.constants.Warnings.FILE_UNORDERED_IMPORTS import com.saveourtool.diktat.ruleset.constants.Warnings.FILE_WILDCARD_IMPORTS import com.saveourtool.diktat.ruleset.constants.Warnings.UNUSED_IMPORT import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.rules.chapter1.PackageNaming.Companion.PACKAGE_SEPARATOR import com.saveourtool.diktat.ruleset.utils.StandardPlatforms import com.saveourtool.diktat.ruleset.utils.copyrightWords import com.saveourtool.diktat.ruleset.utils.findAllDescendantsWithSpecificType import com.saveourtool.diktat.ruleset.utils.handleIncorrectOrder import com.saveourtool.diktat.ruleset.utils.ignoreImports import com.saveourtool.diktat.ruleset.utils.isPartOf import com.saveourtool.diktat.ruleset.utils.isPartOfComment import com.saveourtool.diktat.ruleset.utils.isWhiteSpace import com.saveourtool.diktat.ruleset.utils.moveChildBefore import com.saveourtool.diktat.ruleset.utils.nextSibling import com.saveourtool.diktat.ruleset.utils.operatorMap import com.saveourtool.diktat.ruleset.utils.prevSibling import org.jetbrains.kotlin.KtNodeTypes.FILE_ANNOTATION_LIST import org.jetbrains.kotlin.KtNodeTypes.IMPORT_DIRECTIVE import org.jetbrains.kotlin.KtNodeTypes.IMPORT_LIST import org.jetbrains.kotlin.KtNodeTypes.OPERATION_REFERENCE import org.jetbrains.kotlin.KtNodeTypes.PACKAGE_DIRECTIVE import org.jetbrains.kotlin.KtNodeTypes.REFERENCE_EXPRESSION import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl import org.jetbrains.kotlin.com.intellij.psi.tree.TokenSet import org.jetbrains.kotlin.kdoc.lexer.KDocTokens import org.jetbrains.kotlin.kdoc.lexer.KDocTokens.KDOC import org.jetbrains.kotlin.lexer.KtTokens.BLOCK_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.EOL_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.WHITE_SPACE import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtImportDirective import org.jetbrains.kotlin.psi.KtPackageDirective import org.jetbrains.kotlin.psi.psiUtil.children import org.jetbrains.kotlin.psi.psiUtil.siblings import org.jetbrains.kotlin.psi.stubs.elements.KtFileElementType /** * Visitor for checking internal file structure. * 1. Checks file contains not only comments * 2. Ensures the following blocks order: Copyright, Header Kdoc, @file annotation, package name, Import statements, * top class header and top function header comments, top-level classes or top-level functions * 3. Ensures there is a blank line between these blocks * 4. Ensures imports are ordered alphabetically without blank lines * 5. Ensures there are no wildcard imports */ class FileStructureRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(FILE_CONTAINS_ONLY_COMMENTS, FILE_INCORRECT_BLOCKS_ORDER, FILE_NO_BLANK_LINE_BETWEEN_BLOCKS, FILE_UNORDERED_IMPORTS, FILE_WILDCARD_IMPORTS, UNUSED_IMPORT), ) { private val domainName by lazy { configRules .getCommonConfiguration() .domainName } private val standardImportsAsName = StandardPlatforms .values() .associate { it to it.packages } .mapValues { (_, value) -> value.map { it.split(PACKAGE_SEPARATOR).map(Name::identifier) } } private var packageName = "" /** * There are groups of methods, which should be excluded from usage check without type resolution. * `componentN` is a method for N-th component in destructuring declarations. */ private val ignoreImportsPatterns = setOf("component\\d+".toRegex()) override fun logic(node: ASTNode) { if (node.elementType == KtFileElementType.INSTANCE) { val wildcardImportsConfig = WildCardImportsConfig( this.configRules.getRuleConfig(FILE_WILDCARD_IMPORTS)?.configuration ?: emptyMap() ) val importsGroupingConfig = ImportsGroupingConfig( this.configRules.getRuleConfig(FILE_UNORDERED_IMPORTS)?.configuration ?: emptyMap() ) checkUnusedImport(node) node.findChildByType(IMPORT_LIST) ?.let { checkImportsOrder(it, wildcardImportsConfig, importsGroupingConfig) } if (checkFileHasCode(node)) { checkCodeBlocksOrderAndEmptyLines(node) } return } } @Suppress("FUNCTION_BOOLEAN_PREFIX") private fun checkFileHasCode(node: ASTNode): Boolean { val codeTokens = TokenSet.andNot( TokenSet.ANY, TokenSet.create(WHITE_SPACE, KDOC, BLOCK_COMMENT, EOL_COMMENT, PACKAGE_DIRECTIVE, IMPORT_LIST) ) val hasCode = node.getChildren(codeTokens).isNotEmpty() if (!hasCode) { val freeText = if (node.text.isEmpty()) "file is empty" else "file contains no code" FILE_CONTAINS_ONLY_COMMENTS.warn(configRules, emitWarn, freeText, node.startOffset, node) } return hasCode } @Suppress( "ComplexMethod", "TOO_LONG_FUNCTION", "SpreadOperator" ) private fun checkCodeBlocksOrderAndEmptyLines(node: ASTNode) { // From KtFile.kt: 'scripts have no package directive, all other files must have package directives'. // Kotlin compiler itself enforces it's position in the file if it is present. // If package directive is missing in .kt file (default package), the node is still present in the AST. val packageDirectiveNode = (node.psi as KtFile) .packageDirective ?.takeUnless { it.isRoot } ?.node // There is a private property node.psi.importLists, but it's size can't be > 1 in valid kotlin code. It exists to help in situations // when, e.g. merge conflict marker breaks the imports list. We shouldn't handle this situation here. val importsList = (node.psi as KtFile) .importList ?.takeIf { it.imports.isNotEmpty() } ?.node // this node will be an anchor with respect to which we will look for all other nodes val firstCodeNode = packageDirectiveNode ?: importsList ?: node.children().firstOrNull { // taking nodes with actual code !it.isWhiteSpace() && !it.isPartOfComment() && // but not the ones we are going to move it.elementType != FILE_ANNOTATION_LIST && // if we are here, then IMPORT_LIST either is not present in the AST, or is empty. Either way, we don't need to select it. it.elementType != IMPORT_LIST && // if we are here, then package is default and we don't need to select the empty PACKAGE_DIRECTIVE node. it.elementType != PACKAGE_DIRECTIVE } ?: return // at this point it means the file contains only comments // We consider the first block comment of the file to be the one that possibly contains copyright information. var copyrightComment = firstCodeNode.prevSibling { it.elementType == BLOCK_COMMENT } ?.takeIf { blockCommentNode -> copyrightWords.any { blockCommentNode.text.contains(it, ignoreCase = true) } } // firstCodeNode could be: // * package directive - in this case we looking for kdoc before package directive // and if it doesn't exist, additionally looking for kdoc before imports list // * imports list or actual code - if there is no kdoc before it, suppose that it is absent in file var headerKdoc = firstCodeNode.prevSibling { it.elementType == KDocTokens.KDOC } ?: if (firstCodeNode == packageDirectiveNode) importsList?.prevSibling { it.elementType == KDocTokens.KDOC } else null // Annotations with target`file` can only be placed before `package` directive. var fileAnnotations = node.findChildByType(FILE_ANNOTATION_LIST) // We also collect all other elements that are placed on top of the file. // These may be other comments, so we just place them before the code starts. val otherNodesBeforeCode = firstCodeNode.siblings(forward = false) .filterNot { it.isWhiteSpace() || it == copyrightComment || it == headerKdoc || it == fileAnnotations || it.elementType == PACKAGE_DIRECTIVE } .toList() .reversed() // checking order listOfNotNull(copyrightComment, headerKdoc, fileAnnotations, *otherNodesBeforeCode.toTypedArray()).handleIncorrectOrder({ getSiblingBlocks(copyrightComment, headerKdoc, fileAnnotations, firstCodeNode, otherNodesBeforeCode) }) { astNode, beforeThisNode -> FILE_INCORRECT_BLOCKS_ORDER.warnAndFix(configRules, emitWarn, isFixMode, astNode.text.lines().first(), astNode.startOffset, astNode) { val result = node.moveChildBefore(astNode, beforeThisNode, true) result.newNodes.first().run { // reassign values to the nodes that could have been moved when (elementType) { BLOCK_COMMENT -> copyrightComment = this KDOC -> headerKdoc = this FILE_ANNOTATION_LIST -> fileAnnotations = this } } astNode.treeNext?.let { node.replaceChild(it, PsiWhiteSpaceImpl("\n\n")) } } } // checking empty lines insertNewlinesBetweenBlocks(listOf(copyrightComment, headerKdoc, fileAnnotations, packageDirectiveNode, importsList)) } @Suppress("UnsafeCallOnNullableType") private fun checkImportsOrder( node: ASTNode, wildCardImportsConfig: WildCardImportsConfig, importsGroupingConfig: ImportsGroupingConfig ) { val imports = node.getChildren(TokenSet.create(IMPORT_DIRECTIVE)).toList() // importPath can be null if import name cannot be parsed, which should be a very rare case, therefore !! should be safe here @Suppress("PARAMETER_NAME_IN_OUTER_LAMBDA") imports .filter { (it.psi as KtImportDirective).importPath!!.run { isAllUnder && toString() !in wildCardImportsConfig.allowedWildcards } } .forEach { FILE_WILDCARD_IMPORTS.warn(configRules, emitWarn, it.text, it.startOffset, it) } val sortedImportsGroups = if (importsGroupingConfig.useRecommendedImportsOrder) { regroupImports(imports.map { it.psi as KtImportDirective }) .map { group -> group.map { it.node } } } else { listOf(imports) } .map { group -> group.sortedBy { it.text } } if (sortedImportsGroups.flatten() != imports) { FILE_UNORDERED_IMPORTS.warnAndFix(configRules, emitWarn, isFixMode, "${sortedImportsGroups.flatten().first().text}...", node.startOffset, node) { rearrangeImports(node, imports, sortedImportsGroups) } } } @Suppress("UnsafeCallOnNullableType") private fun checkUnusedImport( node: ASTNode ) { val refSet = findAllReferences(node) packageName = (node.findChildByType(PACKAGE_DIRECTIVE)?.psi as KtPackageDirective).qualifiedName node.findChildByType(IMPORT_LIST) ?.getChildren(TokenSet.create(IMPORT_DIRECTIVE)) ?.toList() ?.forEach { import -> val ktImportDirective = import.psi as KtImportDirective val importName = ktImportDirective.importPath?.importedName?.asString() val importPath = ktImportDirective.importPath?.pathStr!! // importPath - ifNOtParsed & Nullable if (ktImportDirective.aliasName == null && packageName.isNotEmpty() && importPath.startsWith("$packageName.") && importPath.substring(packageName.length + 1).indexOf('.') == -1 ) { // this branch corresponds to imports from the same package deleteImport(importPath, node, ktImportDirective) } else if (importName != null && !refSet.contains(importName)) { // Fixme: operatorMap imports and `getValue` should be deleted if unused, but we can't detect for sure val shouldImportBeIgnored = ignoreImports.contains(importName) || ignoreImportsPatterns.any { it.matches(importName) } if (!shouldImportBeIgnored) { // this import is not used anywhere deleteImport(importPath, node, ktImportDirective) } } } } private fun deleteImport( importPath: String, node: ASTNode, ktImportDirective: KtImportDirective ) { UNUSED_IMPORT.warnAndFix( configRules, emitWarn, isFixMode, "$importPath - unused import", node.startOffset, node ) { ktImportDirective.delete() } } private fun findAllReferences(node: ASTNode): Set { val referencesFromOperations = node.findAllDescendantsWithSpecificType(OPERATION_REFERENCE) .filterNot { it.isPartOf(IMPORT_DIRECTIVE) } .flatMap { ref -> val references = operatorMap.filterValues { ref.text in it } if (references.isNotEmpty()) { references.keys } else { // this is needed to check infix functions that relate to operation reference setOf(ref.text) } } val referencesFromExpressions = node.findAllDescendantsWithSpecificType(REFERENCE_EXPRESSION) .filterNot { it.isPartOf(IMPORT_DIRECTIVE) } .map { // the importedName method removes the quotes, but the node.text method does not it.text.replace("`", "") } val referencesFromKdocs = node.findAllDescendantsWithSpecificType(KDocTokens.KDOC) .flatMap { it.findAllDescendantsWithSpecificType(KDocTokens.MARKDOWN_LINK) } .map { it.text.removePrefix("[").removeSuffix("]") } .flatMap { if (it.contains(".")) { // support cases with reference to method listOf(it, it.substringBeforeLast(".")) } else { listOf(it) } } return (referencesFromOperations + referencesFromExpressions + referencesFromKdocs).toSet() } private fun rearrangeImports( node: ASTNode, imports: List, sortedImportsGroups: List> ) { require(node.elementType == IMPORT_LIST) // move all commented lines among import before imports block node.getChildren(TokenSet.create(EOL_COMMENT)) .forEach { node.treeParent.addChild(it.clone() as ASTNode, node) node.treeParent.addChild(PsiWhiteSpaceImpl("\n"), node) } node.removeRange(imports.first(), imports.last()) sortedImportsGroups.filterNot { it.isEmpty() } .run { forEachIndexed { groupIndex, group -> group.forEachIndexed { index, importNode -> node.addChild(importNode, null) if (index != group.size - 1) { node.addChild(PsiWhiteSpaceImpl("\n"), null) } } if (groupIndex != size - 1) { node.addChild(PsiWhiteSpaceImpl("\n\n"), null) } } } } private fun insertNewlinesBetweenBlocks(blocks: List) { blocks.forEach { astNode -> // if package directive is missing, node is still present, but it's text is empty, so we need to check treeNext to get meaningful results astNode?.nextSibling { it.text.isNotEmpty() }?.apply { if (elementType == WHITE_SPACE && text.count { it == '\n' } != 2) { FILE_NO_BLANK_LINE_BETWEEN_BLOCKS.warnAndFix(configRules, emitWarn, isFixMode, astNode.text.lines().first(), astNode.startOffset, astNode) { (this as LeafPsiElement).rawReplaceWithText("\n\n${text.replace("\n", "")}") } } } } } /** * @return a pair of nodes between which [this] node should be placed, i.e. after the first and before the second element */ private fun ASTNode.getSiblingBlocks( copyrightComment: ASTNode?, headerKdoc: ASTNode?, fileAnnotations: ASTNode?, firstCodeNode: ASTNode, otherNodesBeforeFirst: List ): Pair = when (this) { copyrightComment -> null to listOfNotNull(headerKdoc, fileAnnotations, otherNodesBeforeFirst.firstOrNull(), firstCodeNode).first() headerKdoc -> copyrightComment to (fileAnnotations ?: otherNodesBeforeFirst.firstOrNull() ?: firstCodeNode) fileAnnotations -> (headerKdoc ?: copyrightComment) to (otherNodesBeforeFirst.firstOrNull() ?: firstCodeNode) else -> (headerKdoc ?: copyrightComment) to firstCodeNode } @Suppress("TYPE_ALIAS", "UnsafeCallOnNullableType") private fun regroupImports(imports: List): List> { val (android, notAndroid) = imports.partition { it.isStandard(StandardPlatforms.ANDROID) } val (ownDomain, tmp) = domainName?.let { domainName -> notAndroid.partition { import -> import .importPath ?.fqName ?.pathSegments() ?.zip(domainName.split(PACKAGE_SEPARATOR).map(Name::identifier)) ?.all { it.first == it.second } ?: false } } ?: Pair(emptyList(), notAndroid) val (others, javaAndKotlin) = tmp.partition { !it.isStandard(StandardPlatforms.JAVA) && !it.isStandard(StandardPlatforms.KOTLIN) } val (java, kotlin) = javaAndKotlin.partition { it.isStandard(StandardPlatforms.JAVA) } return listOf(android, ownDomain, others, java, kotlin) } private fun KtImportDirective.isStandard(platformName: StandardPlatforms) = standardImportsAsName[platformName]?.any { names -> names.zip(importPath?.fqName?.pathSegments() ?: emptyList()) .all { it.first == it.second } } ?: false /** * [RuleConfiguration] for wildcard imports */ class WildCardImportsConfig(config: Map) : RuleConfiguration(config) { /** * A list of imports that are allowed to use wildcards. Input is in a form "foo.bar.*,foo.baz.*". */ val allowedWildcards = config["allowedWildcards"]?.split(",")?.map { it.trim() } ?: emptyList() } /** * [RuleConfiguration] for imports grouping according to the recommendation from diktat code style */ class ImportsGroupingConfig(config: Map) : RuleConfiguration(config) { /** * Use imports grouping according to recommendation 3.1 */ val useRecommendedImportsOrder = config["useRecommendedImportsOrder"]?.toBoolean() ?: true } companion object { const val NAME_ID = "file-structure" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/files/IndentationAmount.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter3.files import com.saveourtool.diktat.ruleset.utils.indentation.IndentationConfig /** * Encapsulates the change in the indentation level. */ @Suppress("WRONG_DECLARATIONS_ORDER") internal enum class IndentationAmount { /** * The indent should be preserved at the current level. */ NONE, /** * The indent should be increased or decreased by 1 (regular single indent). */ SINGLE, /** * Extended, or _continuation_ indent. Applicable when any of * [`extendedIndent*`][IndentationConfig] flags is **on**. */ EXTENDED, ; /** * @return the indentation level. To get the actual indentation (the amount * of space characters), the value needs to be multiplied by * [IndentationConfig.indentationSize]. * @see IndentationConfig.indentationSize */ fun level(): Int = ordinal /** * @return whether this amount represents the change in the indentation * level, i.e. whether the element should be indented or un-indented. */ fun isNonZero(): Boolean = level() > 0 companion object { /** * A convenience factory method. * * @param extendedIndent the actual value of ony of the `extendedIndent*` * flags. * @return the corresponding indentation amount, either [SINGLE] or * [EXTENDED]. */ @JvmStatic fun valueOf(extendedIndent: Boolean): IndentationAmount = when { extendedIndent -> EXTENDED else -> SINGLE } } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/files/IndentationAware.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter3.files /** * A contract for types which encapsulate the indentation level. */ internal interface IndentationAware { /** * @return the indentation (the amount of space characters) of this element. */ val indentation: Int } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/files/IndentationConfigAware.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter3.files import com.saveourtool.diktat.ruleset.utils.indentation.IndentationConfig /** * Higher-level abstractions on top of the [indentation size][IndentationConfig.indentationSize]. */ internal interface IndentationConfigAware { /** * The configuration this instance encapsulates. */ val configuration: IndentationConfig /** * Increases the indentation level by [level] * [IndentationConfig.indentationSize]. * * This extension doesn't modify the receiver. * * @receiver the previous indentation level (in space characters), not * modified by the function call. * @param level the indentation level, 1 by default. * @return the new indentation level. * @see unindent * @see IndentationConfig.indentationSize */ fun Int.indent(level: Int = 1): Int = this + level * configuration.indentationSize /** * Decreases the indentation level by [level] * [IndentationConfig.indentationSize]. * * This extension doesn't modify the receiver. * * @receiver the previous indentation level (in space characters), not * modified by the function call. * @param level the indentation level, 1 by default. * @return the new indentation level. * @see indent * @see IndentationConfig.indentationSize */ fun Int.unindent(level: Int = 1): Int = indent(-level) /** * @receiver the previous indentation level (in space characters), not * modified by the function call. * @param amount the indentation amount. * @return the new (increased) indentation level. * @see minus */ operator fun Int.plus(amount: IndentationAmount): Int = indent(level = amount.level()) /** * @receiver the previous indentation level (in space characters), not * modified by the function call. * @param amount the indentation amount. * @return the new (decreased) indentation level. * @see plus */ operator fun Int.minus(amount: IndentationAmount): Int = unindent(level = amount.level()) /** * Allows the `+` operation between an Int and an IndentationAmount to be * commutative. Now, the following are equivalent: * * ```kotlin * val i = 42 + IndentationAmount.SINGLE * val j = IndentationAmount.SINGLE + 42 * ``` * * — as are these: * * ```kotlin * val i = 42 + IndentationAmount.SINGLE * val j = IndentationAmount.SINGLE + 42 * ``` * * @receiver the indentation amount. * @param indentationSpaces the indentation level (in space characters). * @return the new (increased) indentation level. * @see IndentationAmount.minus */ operator fun IndentationAmount.plus(indentationSpaces: Int): Int = indentationSpaces + this /** * Allows expressions like this: * * ```kotlin * 42 - IndentationAmount.SINGLE + 4 * ``` * * to be rewritten this way: * * ```kotlin * 42 - (IndentationAmount.SINGLE - 4) * ``` * * @receiver the indentation amount. * @param indentationSpaces the indentation level (in space characters). * @return the new (decreased) indentation level. * @see IndentationAmount.plus */ operator fun IndentationAmount.minus(indentationSpaces: Int): Int = this + (-indentationSpaces) /** * @receiver the 1st term. * @param other the 2nd term. * @return the two indentation amounts combined, as the indentation level * (in space characters). * @see IndentationAmount.minus */ operator fun IndentationAmount.plus(other: IndentationAmount): Int = this + (+other) /** * @receiver the minuend. * @param other the subtrahend. * @return one amount subtracted from the other, as the indentation level * (in space characters). * @see IndentationAmount.plus */ operator fun IndentationAmount.minus(other: IndentationAmount): Int = this + (-other) /** * @receiver the indentation amount. * @return the indentation level (in space characters). * @see IndentationAmount.unaryMinus */ operator fun IndentationAmount.unaryPlus(): Int = level() * configuration.indentationSize /** * @receiver the indentation amount. * @return the negated indentation level (in space characters). * @see IndentationAmount.unaryPlus */ operator fun IndentationAmount.unaryMinus(): Int = -(+this) companion object Factory { /** * Creates a new instance. * * While you may call this function directly, consider using * [withIndentationConfig] instead. * * @param configuration the configuration this instance will wrap. * @return the newly created instance. * @see withIndentationConfig */ operator fun invoke(configuration: IndentationConfig): IndentationConfigAware = object : IndentationConfigAware { override val configuration = configuration } /** * Calls the specified function [block] with [IndentationConfigAware] as * its receiver and returns its result. * * @param configuration the configuration for the indentation rule. * @param block the function block to call. * @return the result returned by the function block. */ inline fun withIndentationConfig(configuration: IndentationConfig, block: IndentationConfigAware.() -> T): T = with(IndentationConfigAware(configuration), block) } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/files/IndentationError.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter3.files /** * @property expected expected indentation as a number of spaces * @property actual actual indentation as a number of spaces */ internal data class IndentationError(val expected: Int, val actual: Int) ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/files/IndentationRule.kt ================================================ /** * Main logic of indentation including Rule and utility classes and methods. */ @file:Suppress("FILE_UNORDERED_IMPORTS")// False positives, see #1494. package com.saveourtool.diktat.ruleset.rules.chapter3.files import com.saveourtool.diktat.api.DiktatErrorEmitter import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.common.config.rules.getRuleConfig import com.saveourtool.diktat.ruleset.constants.Warnings.WRONG_INDENTATION import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.rules.chapter3.files.IndentationAmount.NONE import com.saveourtool.diktat.ruleset.rules.chapter3.files.IndentationAmount.SINGLE import com.saveourtool.diktat.ruleset.rules.chapter3.files.IndentationConfigAware.Factory.withIndentationConfig import com.saveourtool.diktat.ruleset.utils.NEWLINE import com.saveourtool.diktat.ruleset.utils.SPACE import com.saveourtool.diktat.ruleset.utils.TAB import com.saveourtool.diktat.ruleset.utils.getAllChildrenWithType import com.saveourtool.diktat.ruleset.utils.getAllLeafsWithSpecificType import com.saveourtool.diktat.ruleset.utils.getFilePath import com.saveourtool.diktat.ruleset.utils.getFirstChildWithType import com.saveourtool.diktat.ruleset.utils.indentBy import com.saveourtool.diktat.ruleset.utils.indentation.ArrowInWhenChecker import com.saveourtool.diktat.ruleset.utils.indentation.AssignmentOperatorChecker import com.saveourtool.diktat.ruleset.utils.indentation.ConditionalsAndLoopsWithoutBracesChecker import com.saveourtool.diktat.ruleset.utils.indentation.CustomGettersAndSettersChecker import com.saveourtool.diktat.ruleset.utils.indentation.CustomIndentationChecker import com.saveourtool.diktat.ruleset.utils.indentation.DotCallChecker import com.saveourtool.diktat.ruleset.utils.indentation.ExpressionIndentationChecker import com.saveourtool.diktat.ruleset.utils.indentation.IndentationConfig import com.saveourtool.diktat.ruleset.utils.indentation.KdocIndentationChecker import com.saveourtool.diktat.ruleset.utils.indentation.SuperTypeListChecker import com.saveourtool.diktat.ruleset.utils.indentation.ValueParameterListChecker import com.saveourtool.diktat.ruleset.utils.lastIndent import com.saveourtool.diktat.ruleset.utils.leadingSpaceCount import com.saveourtool.diktat.ruleset.utils.leaveOnlyOneNewLine import com.saveourtool.diktat.ruleset.utils.visit import org.jetbrains.kotlin.KtNodeTypes.BINARY_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.CALL_EXPRESSION import org.jetbrains.kotlin.lexer.KtTokens.CLOSING_QUOTE import org.jetbrains.kotlin.KtNodeTypes.DOT_QUALIFIED_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.ELSE import org.jetbrains.kotlin.lexer.KtTokens.IDENTIFIER import org.jetbrains.kotlin.lexer.KtTokens.LBRACE import org.jetbrains.kotlin.lexer.KtTokens.LBRACKET import org.jetbrains.kotlin.KtNodeTypes.LITERAL_STRING_TEMPLATE_ENTRY import org.jetbrains.kotlin.KtNodeTypes.LONG_STRING_TEMPLATE_ENTRY import org.jetbrains.kotlin.lexer.KtTokens.LONG_TEMPLATE_ENTRY_END import org.jetbrains.kotlin.lexer.KtTokens.LONG_TEMPLATE_ENTRY_START import org.jetbrains.kotlin.lexer.KtTokens.LPAR import org.jetbrains.kotlin.KtNodeTypes.PARENTHESIZED import org.jetbrains.kotlin.lexer.KtTokens.RBRACE import org.jetbrains.kotlin.lexer.KtTokens.RBRACKET import org.jetbrains.kotlin.KtNodeTypes.REFERENCE_EXPRESSION import org.jetbrains.kotlin.lexer.KtTokens.REGULAR_STRING_PART import org.jetbrains.kotlin.lexer.KtTokens.RPAR import org.jetbrains.kotlin.KtNodeTypes.SAFE_ACCESS_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.SHORT_STRING_TEMPLATE_ENTRY import org.jetbrains.kotlin.KtNodeTypes.STRING_TEMPLATE import org.jetbrains.kotlin.KtNodeTypes.THEN import org.jetbrains.kotlin.KtNodeTypes.VALUE_ARGUMENT import org.jetbrains.kotlin.KtNodeTypes.VALUE_ARGUMENT_LIST import org.jetbrains.kotlin.KtNodeTypes.VALUE_PARAMETER_LIST import org.jetbrains.kotlin.lexer.KtTokens.WHITE_SPACE import com.saveourtool.diktat.ruleset.utils.isWhiteSpaceWithNewline import io.github.oshai.kotlinlogging.KotlinLogging import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType import org.jetbrains.kotlin.konan.file.File import org.jetbrains.kotlin.psi.KtDotQualifiedExpression import org.jetbrains.kotlin.psi.KtIfExpression import org.jetbrains.kotlin.psi.KtLoopExpression import org.jetbrains.kotlin.psi.psiUtil.parents import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf import org.jetbrains.kotlin.psi.psiUtil.startOffset import org.jetbrains.kotlin.psi.stubs.elements.KtFileElementType import kotlin.contracts.ExperimentalContracts import kotlin.contracts.contract import kotlin.reflect.KCallable import java.util.ArrayDeque as Stack /** * Rule that checks indentation. The following general rules are checked: * 1. Only spaces should be used each indentation is equal to 4 spaces * 2. File should end with new line * Additionally, a set of CustomIndentationChecker objects checks all WHITE_SPACE node if they are exceptions from general rules. * @see CustomIndentationChecker */ @Suppress("LargeClass") class IndentationRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(WRONG_INDENTATION) ) { private val configuration: IndentationConfig by lazy { IndentationConfig(configRules.getRuleConfig(WRONG_INDENTATION)?.configuration ?: emptyMap()) } private lateinit var filePath: String private lateinit var customIndentationCheckers: List private lateinit var overriddenEmitWarn: DiktatErrorEmitter override fun logic(node: ASTNode) { overriddenEmitWarn = configuration.overrideIfRequiredWarnMessage(emitWarn) if (node.elementType == KtFileElementType.INSTANCE) { filePath = node.getFilePath() customIndentationCheckers = listOf( ::AssignmentOperatorChecker, ::ConditionalsAndLoopsWithoutBracesChecker, ::SuperTypeListChecker, ::ValueParameterListChecker, ::ExpressionIndentationChecker, ::DotCallChecker, ::KdocIndentationChecker, ::CustomGettersAndSettersChecker, ::ArrowInWhenChecker ).map { it(configuration) } if (checkIsIndentedWithSpaces(node)) { checkIndentation(node) } else { log.warn { "Not going to check indentation because there are tabs" } } checkNewlineAtEnd(node) } } /** * This method warns if tabs are used in WHITE_SPACE nodes and substitutes them with spaces in fix mode * * @return true if there are no tabs or all of them have been fixed, false otherwise */ @Suppress("FUNCTION_BOOLEAN_PREFIX") private fun checkIsIndentedWithSpaces(node: ASTNode): Boolean { val whiteSpaceNodes: MutableList = mutableListOf() node.getAllLeafsWithSpecificType(WHITE_SPACE, whiteSpaceNodes) whiteSpaceNodes .filter { it.textContains(TAB) } .apply { if (isEmpty()) { return true } } .forEach { whiteSpaceNode -> WRONG_INDENTATION.warnAndFix(configRules, overriddenEmitWarn, isFixMode, "tabs are not allowed for indentation", whiteSpaceNode.startOffset + whiteSpaceNode.text.indexOf(TAB), whiteSpaceNode) { (whiteSpaceNode as LeafPsiElement).rawReplaceWithText(whiteSpaceNode.text.replace(TAB.toString(), configuration.indentationSize.spaces)) } } return isFixMode // true if we changed all tabs to spaces } /** * Checks that file ends with exactly one empty line */ private fun checkNewlineAtEnd(node: ASTNode) { if (configuration.newlineAtEnd) { val lastChild = generateSequence(node) { it.lastChildNode }.last() val numBlankLinesAfter = lastChild.text.count { it == NEWLINE } if (lastChild.elementType != WHITE_SPACE || numBlankLinesAfter != 1) { val warnText = if (lastChild.elementType != WHITE_SPACE || numBlankLinesAfter == 0) "no newline" else "too many blank lines" val fileName = filePath.substringAfterLast(File.separator) // In case, when last child is newline, visually user will see blank line at the end of file, // however, the text length does not consider it, since it's blank and line appeared only because of `\n` // But ktlint synthetically increase length in aim to have ability to point to this line, so in this case // offset will be `node.textLength`, otherwise we will point to the last symbol, i.e `node.textLength - 1` val offset = if (lastChild.isMultilineWhitespace()) node.textLength else node.textLength - 1 WRONG_INDENTATION.warnAndFix(configRules, overriddenEmitWarn, isFixMode, "$warnText at the end of file $fileName", offset, node) { if (lastChild.elementType != WHITE_SPACE) { node.addChild(PsiWhiteSpaceImpl(NEWLINE.toString()), null) } else { lastChild.leaveOnlyOneNewLine() } } } } } /** * Traverses the tree, keeping track of regular and exceptional indentations */ private fun checkIndentation(node: ASTNode) = with(IndentContext(configuration)) { node.visit { astNode -> checkAndReset(astNode) val indentationIncrement = astNode.getIndentationIncrement() if (indentationIncrement.isNonZero()) { storeIncrementingToken(astNode.elementType, indentationIncrement) } else if (astNode.getIndentationDecrement().isNonZero() && !astNode.treePrev.isMultilineWhitespace()) { // if decreasing token is after WHITE_SPACE with \n, indents are corrected in visitWhiteSpace method this -= astNode.elementType } else if (astNode.isMultilineWhitespace() && astNode.treeNext != null) { // we check only WHITE_SPACE nodes with newlines, other than the last line in file; correctness of newlines should be checked elsewhere visitWhiteSpace(astNode) } } } @Suppress("ForbiddenComment") private fun IndentContext.visitWhiteSpace(astNode: ASTNode) { require(astNode.isMultilineWhitespace()) { "The node is $astNode while a multi-line $WHITE_SPACE expected" } maybeIncrement() val whiteSpace = astNode.psi as PsiWhiteSpace if (astNode.treeNext.getIndentationDecrement().isNonZero()) { // if newline is followed by closing token, it should already be indented less this -= astNode.treeNext.elementType } val indentError = IndentationError(indentation, astNode.text.lastIndent()) val checkResult = customIndentationCheckers.firstNotNullOfOrNull { it.checkNode(whiteSpace, indentError) } val expectedIndent = checkResult?.expectedIndent ?: indentError.expected if (checkResult?.adjustNext == true && astNode.parents().none { it.elementType == LONG_STRING_TEMPLATE_ENTRY }) { val exceptionInitiatorNode = astNode.getExceptionalIndentInitiator() addException(exceptionInitiatorNode, expectedIndent - indentError.expected, checkResult.includeLastChild) } if (astNode.treeParent.elementType == LONG_STRING_TEMPLATE_ENTRY && astNode.treeNext.elementType != LONG_TEMPLATE_ENTRY_END) { addException(astNode.treeParent, SINGLE.level() * configuration.indentationSize, false) } val alignedOpeningAndClosingQuotes = hasAlignedOpeningAndClosingQuotes(astNode, indentError.actual) if ((checkResult?.isCorrect != true && expectedIndent != indentError.actual) || !alignedOpeningAndClosingQuotes) { val warnText = if (!alignedOpeningAndClosingQuotes) { "the same number of indents to the opening and closing quotes was expected" } else { "expected $expectedIndent but was ${indentError.actual}" } WRONG_INDENTATION.warnAndFix(configRules, overriddenEmitWarn, isFixMode, warnText, whiteSpace.startOffset + whiteSpace.text.lastIndexOf(NEWLINE) + 1, whiteSpace.node) { checkStringLiteral(whiteSpace, expectedIndent, indentError.actual) whiteSpace.node.indentBy(expectedIndent) } } } /** * Checks if it is a triple-quoted string template with * [trimIndent()][String.trimIndent] or [trimMargin(...)][String.trimMargin] * function. */ private fun checkStringLiteral( whiteSpace: PsiWhiteSpace, expectedIndent: Int, actualIndent: Int ) { val nextNodeDot = whiteSpace.node.treeNext.getNextDotExpression() if (nextNodeDot != null && nextNodeDot.elementType == DOT_QUALIFIED_EXPRESSION && nextNodeDot.firstChildNode.elementType == STRING_TEMPLATE && nextNodeDot.firstChildNode.text.startsWith("\"\"\"") && nextNodeDot.findChildByType(CALL_EXPRESSION).isTrimIndentOrMarginCall()) { fixStringLiteral(nextNodeDot.firstChildNode, expectedIndent, actualIndent) } } /** * Indents each [entry][LITERAL_STRING_TEMPLATE_ENTRY] in a (potentially, * multi-line) triple-quoted [string template][STRING_TEMPLATE]. * * String templates usually have the following structure: * * * `STRING_TEMPLATE` * * `OPEN_QUOTE` * * `LITERAL_STRING_TEMPLATE_ENTRY` * * `REGULAR_STRING_PART` * * … * * `LITERAL_STRING_TEMPLATE_ENTRY` * * `REGULAR_STRING_PART` * * `CLOSING_QUOTE` * * @param stringTemplate the string template. * @see STRING_TEMPLATE * @see LITERAL_STRING_TEMPLATE_ENTRY */ @Suppress("LOCAL_VARIABLE_EARLY_DECLARATION") private fun fixStringLiteral( stringTemplate: ASTNode, expectedIndent: Int, actualIndent: Int ) { val templateEntries = stringTemplate.getAllChildrenWithType(LITERAL_STRING_TEMPLATE_ENTRY) val templateEntriesLastIndex = templateEntries.size - 1 var templateEntryFollowingNewline = false templateEntries.forEachIndexed { index, templateEntry -> val text = templateEntry.text when { text.contains(NEWLINE) -> { /* * Set the flag. */ templateEntryFollowingNewline = true /* * In real-life cases observed, whenever a `LITERAL_STRING_TEMPLATE_ENTRY` * _contains_ a newline character, it is _exactly_ a newline character. */ check(text.length == 1) { val escapedText = text.replace(NEWLINE.toString(), "\\n") "A LITERAL_STRING_TEMPLATE_ENTRY at index $index contains extra characters in addition to the newline, " + "entry: \"$escapedText\", " + "string template: ${stringTemplate.text}" } } /* * This is the last string template fragment which is usually followed * with the closing `"""` and the `.trimIndent()` or `.trimMargin(...)` call. */ index == templateEntriesLastIndex -> { val lastRegularStringPart = templateEntries.last().firstChildNode as LeafPsiElement lastRegularStringPart.checkRegularStringPart().apply { val textWithoutIndent = text.trimStart() rawReplaceWithText(expectedIndent.spaces + textWithoutIndent) } } /* * Either this is the very first string template entry, or an * entry which immediately follows the newline. */ index == 0 || templateEntryFollowingNewline -> { fixFirstTemplateEntries( templateEntry, expectedIndentation = expectedIndent, actualIndentation = actualIndent) /* * Re-set the flag. */ templateEntryFollowingNewline = false } } } } /** * Modifies [templateEntry] by correcting its indentation level. * * This method can be used to fix all [lines][LITERAL_STRING_TEMPLATE_ENTRY] * of a [string template][STRING_TEMPLATE] except for the last one. * * Also, it considers `$foo` insertions in a string. * * @param templateEntry a [LITERAL_STRING_TEMPLATE_ENTRY] node. * @param expectedIndentation the expected indentation level, as returned by * [IndentationError.expected]. * @param actualIndentation the actual indentation level, as returned by * [IndentationError.actual]. */ private fun fixFirstTemplateEntries( templateEntry: ASTNode, expectedIndentation: Int, actualIndentation: Int ) { require(templateEntry.elementType == LITERAL_STRING_TEMPLATE_ENTRY) { "The elementType of this node is ${templateEntry.elementType} while $LITERAL_STRING_TEMPLATE_ENTRY expected" } /* * Quite possible, do nothing in this case. */ if (expectedIndentation == actualIndentation) { return } withIndentationConfig(configuration) { /* * A `REGULAR_STRING_PART`. */ val regularStringPart = templateEntry.firstChildNode as LeafPsiElement val regularStringPartText = regularStringPart.checkRegularStringPart().text // shift of the node depending on its initial string template indentation val nodeStartIndent = (regularStringPartText.leadingSpaceCount() - actualIndentation - SINGLE).zeroIfNegative() val isPrevStringTemplate = templateEntry.treePrev.elementType in stringLiteralTokens val isNextStringTemplate = templateEntry.treeNext.elementType in stringLiteralTokens val correctedText = when { isPrevStringTemplate -> when { isNextStringTemplate -> regularStringPartText // if string template is before literal_string else -> regularStringPartText.trimEnd() } // if string template is after literal_string // or if there is no string template in literal_string else -> (expectedIndentation + SINGLE + nodeStartIndent).spaces + regularStringPartText.trimStart() } regularStringPart.rawReplaceWithText(correctedText) } } private fun ASTNode.getExceptionalIndentInitiator() = treeParent.let { parent -> when (parent.psi) { // fixme: custom logic for determining exceptional indent initiator, should be moved elsewhere // get the topmost expression to keep extended indent for the whole chain of dot call expressions is KtDotQualifiedExpression -> parents().takeWhile { it.elementType == DOT_QUALIFIED_EXPRESSION || it.elementType == SAFE_ACCESS_EXPRESSION }.last() is KtIfExpression -> parent.findChildByType(THEN) ?: parent.findChildByType(ELSE) ?: parent is KtLoopExpression -> (parent.psi as KtLoopExpression).body?.node ?: parent else -> parent } } /** * @return the amount by which the indentation should be incremented * once this node is encountered (may be [none][NONE]). * @see ASTNode.getIndentationDecrement */ private fun ASTNode.getIndentationIncrement(): IndentationAmount = when (elementType) { /* * A special case of an opening parenthesis which *may* or *may not* * increment the indentation. */ LPAR -> getParenthesisIndentationChange() in increasingTokens -> SINGLE else -> NONE } /** * @return the amount by which the indentation should be decremented * once this node is encountered (may be [none][NONE]). * @see ASTNode.getIndentationIncrement */ private fun ASTNode.getIndentationDecrement(): IndentationAmount = when (elementType) { /* * A special case of a closing parenthesis which *may* or *may not* * increment the indentation. */ RPAR -> getParenthesisIndentationChange() in decreasingTokens -> SINGLE else -> NONE } /** * Parentheses always affect indentation when they're a part of a * [VALUE_PARAMETER_LIST] (formal arguments) or a [VALUE_ARGUMENT_LIST] * (effective function call arguments). * * When they're children of a [PARENTHESIZED] (often inside a * [BINARY_EXPRESSION]), contribute to the indentation depending on * whether there's a newline after the opening parenthesis. * * @receiver an opening or a closing parenthesis. * @return the amount by which the indentation should be incremented * (after [LPAR]) or decremented (after [RPAR]). The returned value * may well be [NONE], meaning the indentation level should be * preserved. * @see BINARY_EXPRESSION * @see PARENTHESIZED * @see VALUE_ARGUMENT_LIST * @see VALUE_PARAMETER_LIST */ private fun ASTNode.getParenthesisIndentationChange(): IndentationAmount { require(elementType in arrayOf(LPAR, RPAR)) { elementType.toString() } return when (treeParent.elementType) { PARENTHESIZED -> when (elementType) { /* * `LPAR` inside a binary expression only contributes to the * indentation if it's immediately followed by a newline. */ LPAR -> when { treeNext.isWhiteSpaceWithNewline() -> IndentationAmount.valueOf(configuration.extendedIndentAfterOperators) else -> NONE } /* * `RPAR` inside a binary expression affects the indentation * only if its matching `LPAR` node does so. */ else -> { val openingParenthesis = elementType.braceMatchOrNull()?.let { braceMatch -> treeParent.findChildByType(braceMatch) } openingParenthesis?.getParenthesisIndentationChange() ?: NONE } } /* * Either a control-flow statement (one of IF, WHEN, FOR or * DO_WHILE), a function declaration (VALUE_PARAMETER_LIST or * PROPERTY_ACCESSOR), or a function call (VALUE_ARGUMENT_LIST). */ else -> SINGLE } } /** * Holds a mutable state needed to calculate the indentation and keep track * of exceptions. * * Tokens from [increasingTokens] are stored in stack [activeTokens]. When [WHITE_SPACE] with line break is encountered, * if stack is not empty, indentation is increased. When token from [decreasingTokens] is encountered, it's counterpart is removed * from stack. If there has been a [WHITE_SPACE] with line break between them, indentation is decreased. * * @see IndentationConfigAware */ private class IndentContext(config: IndentationConfig) : IndentationAware, IndentationConfigAware by IndentationConfigAware(config) { private var regularIndent = 0 private val exceptionalIndents: MutableList = mutableListOf() /** * The stack of element types (either [WHITE_SPACE] or any of * [increasingTokens]) along with the indentation changes the * corresponding elements induce. * * [WHITE_SPACE] is always accompanied by [no indentation change][NONE]. */ private val activeTokens: Stack = Stack() /** * @return full current indentation. */ @Suppress( "CUSTOM_GETTERS_SETTERS", "WRONG_NAME_OF_VARIABLE_INSIDE_ACCESSOR", // #1464 ) override val indentation: Int get() = regularIndent + exceptionalIndents.sumOf(ExceptionalIndent::indentation) /** * Pushes [token] onto the [stack][activeTokens], but doesn't increment * the indentation. The indentation is incremented separately, see * [maybeIncrement]. * * A call to this method **may or may not** be followed by a single call * to [maybeIncrement]. * * @param token a token that caused indentation increment, any of * [increasingTokens] (e.g.: an [opening brace][LPAR]). * @param increment the indentation increment (must be non-zero). * @see maybeIncrement */ fun storeIncrementingToken(token: IElementType, increment: IndentationAmount) { require(token in increasingTokens) { "The token is $token while any of $increasingTokens expected" } require(increment.isNonZero()) { "The indentation increment is zero" } activeTokens.push(token to increment) } /** * Increments the indentation if a multi-line [WHITE_SPACE] is * encountered after an opening brace. * * A call to this method **always** has a preceding call to * [storeIncrementingToken]. * * @see minusAssign */ fun maybeIncrement() { val headOrNull: IndentedElementType? = activeTokens.peek() check(headOrNull == null || headOrNull.type == WHITE_SPACE || headOrNull.type in increasingTokens) { "The head of the stack is $headOrNull while only $WHITE_SPACE or any of $increasingTokens expected" } if (headOrNull != null && headOrNull.type != WHITE_SPACE) { regularIndent += headOrNull.indentationChange activeTokens.push(WHITE_SPACE to NONE) } } /** * Pops tokens from the [stack][activeTokens] and decrements the * indentation accordingly. * * @param token a token that caused indentation decrement, any of * [decreasingTokens] (e.g.: a [closing brace][RPAR]). * @see maybeIncrement */ operator fun minusAssign(token: IElementType) { require(token in decreasingTokens) { "The token is $token while any of $decreasingTokens expected" } if (activeTokens.peek()?.type == WHITE_SPACE) { /*- * In practice, there's always only a single `WHITE_SPACE` * element type (representing the newline) pushed onto the stack * after an opening brace (`LPAR` & friends), so it needs to be * popped only once. * * Still, preserving the logic for compatibility. */ while (activeTokens.peek()?.type == WHITE_SPACE) { activeTokens.pop() } /*- * If an opening brace (`LPAR` etc.) was followed by a newline, * this has led to the indentation being increased. * * Now, let's decrease it back to the original value. */ val headOrNull: IndentedElementType? = activeTokens.peek() if (headOrNull != null && headOrNull.type == token.braceMatchOrNull()) { regularIndent -= headOrNull.indentationChange } } /* * In practice, the predicate is always `true` (provided braces are * balanced) and can be replaced with a `check()` call. */ val headOrNull: IndentedElementType? = activeTokens.peek() if (headOrNull != null && headOrNull.type == token.braceMatchOrNull()) { /* * Pop the matching opening brace. */ activeTokens.pop() } } /** * @param initiator a node that caused exceptional indentation * @param indentation an additional indentation. * @param includeLastChild whether the last child node should be included in the range affected by exceptional indentation * @return true if add exception in exceptionalIndents */ fun addException( initiator: ASTNode, indentation: Int, includeLastChild: Boolean ) = exceptionalIndents.add(ExceptionalIndent(initiator, indentation, includeLastChild)) /** * @param astNode the node which is used to determine whether exceptional indents are still active * @return boolean result */ fun checkAndReset(astNode: ASTNode) = exceptionalIndents.retainAll { it.isActive(astNode) } /** * @property initiator a node that caused exceptional indentation * @property indentation an additional indentation. * @property includeLastChild whether the last child node should be included in the range affected by exceptional indentation */ private data class ExceptionalIndent( val initiator: ASTNode, override val indentation: Int, val includeLastChild: Boolean = true ) : IndentationAware { /** * Checks whether this exceptional indentation is still active. This is a hypotheses that exceptional indentation will end * outside of node where it appeared, e.g. when an expression after assignment operator is over. * * @param currentNode the current node during AST traversal * @return boolean result */ fun isActive(currentNode: ASTNode): Boolean = currentNode.psi.parentsWithSelf.any { it.node == initiator } && (includeLastChild || currentNode.treeNext != initiator.lastChildNode) } } companion object { private val log = KotlinLogging.logger {} const val NAME_ID = "indentation" private val increasingTokens: Set = linkedSetOf(LPAR, LBRACE, LBRACKET, LONG_TEMPLATE_ENTRY_START) private val decreasingTokens: Set = linkedSetOf(RPAR, RBRACE, RBRACKET, LONG_TEMPLATE_ENTRY_END) /** * This is essentially a bi-map, which allows to look up a closing brace * type by an opening brace type, or vice versa. */ private val matchingTokens = (increasingTokens.asSequence() zip decreasingTokens.asSequence()).flatMap { (opening, closing) -> sequenceOf(opening to closing, closing to opening) }.toMap() private val stringLiteralTokens = listOf(SHORT_STRING_TEMPLATE_ENTRY, LONG_STRING_TEMPLATE_ENTRY) private val knownTrimFunctionPatterns = sequenceOf(String::trimIndent, String::trimMargin) .map(KCallable::name) .toSet() /** * @return a string which consists of `N` [space][SPACE] characters. */ @Suppress("CUSTOM_GETTERS_SETTERS") private val Int.spaces: String get() = SPACE.toString().repeat(n = this) /** * @return `true` if this is a [whitespace][WHITE_SPACE] node containing * a [newline][NEWLINE], `false` otherwise. */ private fun ASTNode.isMultilineWhitespace(): Boolean = elementType == WHITE_SPACE && textContains(NEWLINE) @OptIn(ExperimentalContracts::class) private fun ASTNode?.isMultilineStringTemplate(): Boolean { contract { returns(true) implies (this@isMultilineStringTemplate != null) } this ?: return false return elementType == STRING_TEMPLATE && getAllChildrenWithType(LITERAL_STRING_TEMPLATE_ENTRY).any { entry -> entry.textContains(NEWLINE) } } /** * @return `true` if this is a [String.trimIndent] or [String.trimMargin] * call, `false` otherwise. */ @OptIn(ExperimentalContracts::class) private fun ASTNode?.isTrimIndentOrMarginCall(): Boolean { contract { returns(true) implies (this@isTrimIndentOrMarginCall != null) } this ?: return false require(elementType == CALL_EXPRESSION) { "The elementType of this node is $elementType while $CALL_EXPRESSION expected" } val referenceExpression = firstChildNode ?: return false if (referenceExpression.elementType != REFERENCE_EXPRESSION) { return false } val identifier = referenceExpression.firstChildNode ?: return false if (identifier.elementType != IDENTIFIER) { return false } return identifier.text in knownTrimFunctionPatterns } private fun ASTNode.getNextDotExpression(): ASTNode? = when (elementType) { DOT_QUALIFIED_EXPRESSION -> this else -> getFirstChildWithType(DOT_QUALIFIED_EXPRESSION) } /** * @return the matching closing brace type for this opening brace type, * or vice versa. */ private fun IElementType.braceMatchOrNull(): IElementType? = matchingTokens[this] /** * Checks this [REGULAR_STRING_PART] child of a [LITERAL_STRING_TEMPLATE_ENTRY]. * * @return this `REGULAR_STRING_PART` PSI element. */ private fun LeafPsiElement.checkRegularStringPart(): LeafPsiElement { val lastRegularStringPartType = elementType check(lastRegularStringPartType == REGULAR_STRING_PART) { "Unexpected type of the 1st child of the string template entry, " + "expected: $REGULAR_STRING_PART, " + "actual: $lastRegularStringPartType, " + "string template: ${parent.parent.text}" } return this } /** * @return this very integer if non-negative, 0 otherwise. */ private fun Int.zeroIfNegative(): Int = when { this > 0 -> this else -> 0 } /** * Processes fragments like: * * ```kotlin * f( * """ * |foobar * """.trimMargin() * ) * ``` * * @param whitespace the whitespace node between an [LPAR] and the * `trimIndent()`- or `trimMargin()`- terminated string template, which is * an effective argument of a function call. The string template is * expected to begin on a separate line (otherwise, there'll be no * whitespace in-between). * @return `true` if the opening and the closing quotes of the string * template are aligned, `false` otherwise. */ private fun hasAlignedOpeningAndClosingQuotes(whitespace: ASTNode, expectedIndent: Int): Boolean { require(whitespace.isMultilineWhitespace()) { "The node is $whitespace while a multi-line $WHITE_SPACE expected" } /* * Here, we expect that `nextNode` is a VALUE_ARGUMENT which contains * the dot-qualified expression (`STRING_TEMPLATE.trimIndent()` or * `STRING_TEMPLATE.trimMargin()`). */ val nextFunctionArgument = whitespace.treeNext if (nextFunctionArgument.elementType == VALUE_ARGUMENT) { val memberOrExtensionCall = nextFunctionArgument.getNextDotExpression() /* * Limit allowed member or extension calls to `trimIndent()` and * `trimMargin()`. */ if (memberOrExtensionCall != null && memberOrExtensionCall.getFirstChildWithType(CALL_EXPRESSION).isTrimIndentOrMarginCall()) { val stringTemplate = memberOrExtensionCall.getFirstChildWithType(STRING_TEMPLATE) /* * Limit the logic to multi-line string templates only (the * opening and closing quotes of a single-line template are, * obviously, always mis-aligned). */ if (stringTemplate != null && stringTemplate.isMultilineStringTemplate()) { val closingQuoteIndent = stringTemplate.getFirstChildWithType(CLOSING_QUOTE) ?.treePrev ?.text ?.length ?: -1 return expectedIndent == closingQuoteIndent } } } return true } } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/files/IndentedElementType.kt ================================================ @file:Suppress("HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE") package com.saveourtool.diktat.ruleset.rules.chapter3.files import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType /** * @return the element type. */ @Suppress("CUSTOM_GETTERS_SETTERS") internal val IndentedElementType.type: IElementType get() = first /** * @return the indentation change. */ @Suppress("CUSTOM_GETTERS_SETTERS") internal val IndentedElementType.indentationChange: IndentationAmount get() = second /** * An [IElementType] along with the indentation change it induces. */ internal typealias IndentedElementType = Pair ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/files/NewlinesRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter3.files import com.saveourtool.diktat.common.config.rules.RuleConfiguration import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.common.config.rules.getRuleConfig import com.saveourtool.diktat.ruleset.constants.Warnings.COMPLEX_EXPRESSION import com.saveourtool.diktat.ruleset.constants.Warnings.WRONG_NEWLINES import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.appendNewlineMergingWhiteSpace import com.saveourtool.diktat.ruleset.utils.changeWhiteSpaceOnNewline import com.saveourtool.diktat.ruleset.utils.emptyBlockList import com.saveourtool.diktat.ruleset.utils.findAllDescendantsWithSpecificType import com.saveourtool.diktat.ruleset.utils.findAllNodesWithCondition import com.saveourtool.diktat.ruleset.utils.findParentNodeWithSpecificType import com.saveourtool.diktat.ruleset.utils.getAllChildrenWithType import com.saveourtool.diktat.ruleset.utils.getFilePath import com.saveourtool.diktat.ruleset.utils.getFirstChildWithType import com.saveourtool.diktat.ruleset.utils.getIdentifierName import com.saveourtool.diktat.ruleset.utils.getRootNode import com.saveourtool.diktat.ruleset.utils.hasParent import com.saveourtool.diktat.ruleset.utils.isBeginByNewline import com.saveourtool.diktat.ruleset.utils.isFollowedByNewline import com.saveourtool.diktat.ruleset.utils.isGradleScript import com.saveourtool.diktat.ruleset.utils.isSingleLineIfElse import com.saveourtool.diktat.ruleset.utils.isWhiteSpaceWithNewline import com.saveourtool.diktat.ruleset.utils.leaveOnlyOneNewLine import com.saveourtool.diktat.ruleset.utils.nextCodeSibling import com.saveourtool.diktat.ruleset.utils.parent import com.saveourtool.diktat.ruleset.utils.prevCodeSibling import io.github.oshai.kotlinlogging.KotlinLogging import org.jetbrains.kotlin.KtNodeTypes.BINARY_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.BLOCK import org.jetbrains.kotlin.KtNodeTypes.CALLABLE_REFERENCE_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.CALL_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.CLASS import org.jetbrains.kotlin.KtNodeTypes.CONDITION import org.jetbrains.kotlin.KtNodeTypes.DOT_QUALIFIED_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.FUN import org.jetbrains.kotlin.KtNodeTypes.FUNCTION_LITERAL import org.jetbrains.kotlin.KtNodeTypes.FUNCTION_TYPE import org.jetbrains.kotlin.KtNodeTypes.FUNCTION_TYPE_RECEIVER import org.jetbrains.kotlin.KtNodeTypes.IF import org.jetbrains.kotlin.KtNodeTypes.IMPORT_DIRECTIVE import org.jetbrains.kotlin.KtNodeTypes.LAMBDA_ARGUMENT import org.jetbrains.kotlin.KtNodeTypes.OPERATION_REFERENCE import org.jetbrains.kotlin.KtNodeTypes.PACKAGE_DIRECTIVE import org.jetbrains.kotlin.KtNodeTypes.POSTFIX_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.PRIMARY_CONSTRUCTOR import org.jetbrains.kotlin.KtNodeTypes.REFERENCE_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.RETURN import org.jetbrains.kotlin.KtNodeTypes.SAFE_ACCESS_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.SECONDARY_CONSTRUCTOR import org.jetbrains.kotlin.KtNodeTypes.SUPER_TYPE_CALL_ENTRY import org.jetbrains.kotlin.KtNodeTypes.SUPER_TYPE_ENTRY import org.jetbrains.kotlin.KtNodeTypes.SUPER_TYPE_LIST import org.jetbrains.kotlin.KtNodeTypes.VALUE_ARGUMENT import org.jetbrains.kotlin.KtNodeTypes.VALUE_ARGUMENT_LIST import org.jetbrains.kotlin.KtNodeTypes.VALUE_PARAMETER import org.jetbrains.kotlin.KtNodeTypes.VALUE_PARAMETER_LIST import org.jetbrains.kotlin.KtNodeTypes.WHEN import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.PsiElement import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType import org.jetbrains.kotlin.com.intellij.psi.tree.TokenSet import org.jetbrains.kotlin.kdoc.lexer.KDocTokens.KDOC import org.jetbrains.kotlin.lexer.KtTokens.ANDAND import org.jetbrains.kotlin.lexer.KtTokens.ARROW import org.jetbrains.kotlin.lexer.KtTokens.BLOCK_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.COLON import org.jetbrains.kotlin.lexer.KtTokens.COLONCOLON import org.jetbrains.kotlin.lexer.KtTokens.COMMA import org.jetbrains.kotlin.lexer.KtTokens.DIV import org.jetbrains.kotlin.lexer.KtTokens.DIVEQ import org.jetbrains.kotlin.lexer.KtTokens.DOT import org.jetbrains.kotlin.lexer.KtTokens.ELVIS import org.jetbrains.kotlin.lexer.KtTokens.EOL_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.EQ import org.jetbrains.kotlin.lexer.KtTokens.IDENTIFIER import org.jetbrains.kotlin.lexer.KtTokens.LPAR import org.jetbrains.kotlin.lexer.KtTokens.MINUS import org.jetbrains.kotlin.lexer.KtTokens.MINUSEQ import org.jetbrains.kotlin.lexer.KtTokens.MUL import org.jetbrains.kotlin.lexer.KtTokens.MULTEQ import org.jetbrains.kotlin.lexer.KtTokens.OROR import org.jetbrains.kotlin.lexer.KtTokens.PLUS import org.jetbrains.kotlin.lexer.KtTokens.PLUSEQ import org.jetbrains.kotlin.lexer.KtTokens.RETURN_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.RPAR import org.jetbrains.kotlin.lexer.KtTokens.SAFE_ACCESS import org.jetbrains.kotlin.lexer.KtTokens.WHITE_SPACE import org.jetbrains.kotlin.psi.KtBinaryExpression import org.jetbrains.kotlin.psi.KtNamedFunction import org.jetbrains.kotlin.psi.KtParameterList import org.jetbrains.kotlin.psi.KtReferenceExpression import org.jetbrains.kotlin.psi.KtSuperTypeList import org.jetbrains.kotlin.psi.KtValueArgumentList import org.jetbrains.kotlin.psi.psiUtil.anyDescendantOfType import org.jetbrains.kotlin.psi.psiUtil.children import org.jetbrains.kotlin.psi.psiUtil.parents import org.jetbrains.kotlin.psi.psiUtil.siblings private typealias ListOfList = MutableList> /** * Rule that checks line break styles: * 1. Checks that some operators are followed by newline, while others are prepended by it * 2. Statements that follow `!!` behave in the same way * 3. Forces functional style of chained dot call expressions with exception * 4. Checks that newline is placed after assignment operator, not before * 5. Ensures that function or constructor name isn't separated from `(` by space or newline * 6. Ensures that in multiline lambda newline follows arrow or, in case of lambda without explicit parameters, opening brace * 7. Checks that functions with single `return` are simplified to functions with expression body * 8. Parameter or argument lists and supertype lists that have more than 2 elements should be separated by newlines * 9. Complex expression inside condition replaced with new variable */ @Suppress("ForbiddenComment") class NewlinesRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(COMPLEX_EXPRESSION, WRONG_NEWLINES) ) { private val configuration by lazy { NewlinesRuleConfiguration(configRules.getRuleConfig(WRONG_NEWLINES)?.configuration ?: emptyMap()) } override fun logic(node: ASTNode) { when (node.elementType) { OPERATION_REFERENCE, EQ -> handleOperatorWithLineBreakAfter(node) // this logic regulates indentation with elements - so that the symbol and the subsequent expression are on the same line in lineBreakBeforeOperators -> handleOperatorWithLineBreakBefore(node) LPAR -> handleOpeningParentheses(node) COMMA -> handleComma(node) COLON -> handleColon(node) BLOCK -> handleLambdaBody(node) RETURN -> handleReturnStatement(node) SUPER_TYPE_LIST, VALUE_PARAMETER_LIST, VALUE_ARGUMENT_LIST -> handleList(node) // this logic splits long expressions into multiple lines DOT_QUALIFIED_EXPRESSION, SAFE_ACCESS_EXPRESSION, POSTFIX_EXPRESSION -> handDotQualifiedAndSafeAccessExpression(node) else -> { } } } @Suppress("GENERIC_VARIABLE_WRONG_DECLARATION", "MagicNumber") private fun handDotQualifiedAndSafeAccessExpression(node: ASTNode) { val listParentTypesNoFix = listOf(PACKAGE_DIRECTIVE, IMPORT_DIRECTIVE, VALUE_PARAMETER_LIST, VALUE_ARGUMENT_LIST, DOT_QUALIFIED_EXPRESSION, SAFE_ACCESS_EXPRESSION, POSTFIX_EXPRESSION) if (isNotFindParentNodeWithSpecificManyType(node, listParentTypesNoFix)) { val listDot = node.findAllNodesWithCondition( withSelf = true, excludeChildrenCondition = { !isDotQuaOrSafeAccessOrPostfixExpression(it) } ) { isDotQuaOrSafeAccessOrPostfixExpression(it) && it.elementType != POSTFIX_EXPRESSION }.reversed() if (listDot.size > configuration.maxCallsInOneLine) { val without = listDot.filterIndexed { index, it -> val nodeBeforeDotOrSafeAccess = it.findChildByType(DOT)?.treePrev ?: it.findChildByType(SAFE_ACCESS)?.treePrev val firstElem = it.firstChildNode val isTextContainsParenthesized = isTextContainsFunctionCall(firstElem) val isNotWhiteSpaceBeforeDotOrSafeAccessContainNewLine = nodeBeforeDotOrSafeAccess?.elementType != WHITE_SPACE || nodeBeforeDotOrSafeAccess?.let { it.elementType == WHITE_SPACE || it.textContains('\n') } == false isTextContainsParenthesized && (index > 0) && isNotWhiteSpaceBeforeDotOrSafeAccessContainNewLine } if (without.isNotEmpty()) { WRONG_NEWLINES.warnAndFix(configRules, emitWarn, isFixMode, "wrong split long `dot qualified expression` or `safe access expression`", node.startOffset, node) { fixDotQualifiedExpression(without) } } } } } /** * Return false, if you find parent with types in list else return true */ private fun isNotFindParentNodeWithSpecificManyType(node: ASTNode, list: List): Boolean { list.forEach { elem -> node.findParentNodeWithSpecificType(elem)?.let { return false } } return true } /** * Fix Dot Qualified Expression and Safe Access Expression - * 1) Append new White Space node before second and subsequent node Dot or Safe Access * in Dot Qualified Expression? Safe Access Expression and Postfix Expression * 2) If before first Dot or Safe Access node stay White Space node with \n - remove this node */ private fun fixDotQualifiedExpression(list: List) { list.forEach { astNode -> val dotNode = astNode.getFirstChildWithType(DOT) ?: astNode.getFirstChildWithType(SAFE_ACCESS) val nodeBeforeDot = dotNode?.treePrev astNode.appendNewlineMergingWhiteSpace(nodeBeforeDot, dotNode) } } private fun isDotQuaOrSafeAccessOrPostfixExpression(node: ASTNode) = node.elementType == DOT_QUALIFIED_EXPRESSION || node.elementType == SAFE_ACCESS_EXPRESSION || node.elementType == POSTFIX_EXPRESSION private fun handleOperatorWithLineBreakAfter(node: ASTNode) { // [node] should be either EQ or OPERATION_REFERENCE which has single child if (node.elementType != EQ && node.firstChildNode.elementType !in lineBreakAfterOperators && !node.isInfixCall()) { return } // We need to check newline only if prevCodeSibling exists. It can be not the case for unary operators, which are placed // at the beginning of the line. if (node.prevCodeSibling()?.isFollowedByNewline() == true) { WRONG_NEWLINES.warnAndFix(configRules, emitWarn, isFixMode, "should break a line after and not before ${node.text}", node.startOffset, node) { node.run { treeParent.removeChild(treePrev) if (!isFollowedByNewline()) { treeParent.appendNewlineMergingWhiteSpace(treeNext.takeIf { it.elementType == WHITE_SPACE }, treeNext) } } } } } @Suppress("ComplexMethod", "TOO_LONG_FUNCTION") private fun handleOperatorWithLineBreakBefore(node: ASTNode) { if (node.isDotFromPackageOrImport()) { return } val isIncorrect = (if (node.elementType == ELVIS) node.treeParent else node).run { if (isInvalidCallsChain(dropLeadingProperties = true)) { if (node.isInParentheses()) { checkForComplexExpression(node) } val isSingleLineIfElse = parent(true) { it.elementType == IF }?.isSingleLineIfElse() ?: false // to follow functional style these operators should be started by newline (isFollowedByNewline() || !isBeginByNewline()) && !isSingleLineIfElse && (!isFirstCall() || !isMultilineLambda(treeParent)) } else { if (isInvalidCallsChain(dropLeadingProperties = false) && node.isInParentheses()) { checkForComplexExpression(node) } // unless statement is simple and on single line, these operators cannot have newline after isFollowedByNewline() && !isSingleDotStatementOnSingleLine() } } if (isIncorrect || node.isElvisCorrect()) { val freeText = if (node.isInvalidCallsChain() || node.isElvisCorrect()) { "should follow functional style at ${node.text}" } else { "should break a line before and not after ${node.text}" } WRONG_NEWLINES.warnAndFix(configRules, emitWarn, isFixMode, freeText, node.startOffset, node) { node.selfOrOperationReferenceParent().run { if (!isBeginByNewline()) { // prepend newline treeParent.appendNewlineMergingWhiteSpace(treePrev.takeIf { it.elementType == WHITE_SPACE }, this) } if (isFollowedByNewline()) { // remove newline after parent(false) { it.treeNext != null }?.let { it.treeParent.removeChild(it.treeNext) } } } } } } private fun checkForComplexExpression(node: ASTNode) { if (node.getRootNode().getFilePath().isGradleScript()) { // this inspection is softened for gradle scripts, see https://github.com/saveourtool/diktat/issues/1148 return } COMPLEX_EXPRESSION.warn(configRules, emitWarn, node.text, node.startOffset, node) } private fun handleOpeningParentheses(node: ASTNode) { val parent = node.treeParent if (parent.elementType in listOf(VALUE_ARGUMENT_LIST, VALUE_PARAMETER_LIST)) { val prevWhiteSpace = node .parent(false) { it.treePrev != null } ?.treePrev ?.takeIf { it.elementType == WHITE_SPACE } val isNotAnonymous = parent.treeParent.elementType in listOf(CALL_EXPRESSION, PRIMARY_CONSTRUCTOR, SECONDARY_CONSTRUCTOR, FUN) if (prevWhiteSpace != null && isNotAnonymous) { WRONG_NEWLINES.warnAndFix(configRules, emitWarn, isFixMode, "opening parentheses should not be separated from constructor or function name", node.startOffset, node) { prevWhiteSpace.treeParent.removeChild(prevWhiteSpace) } } } } /** * Check that newline is not placed before a comma */ private fun handleComma(node: ASTNode) { val prevNewLine = node .parent(false) { it.treePrev != null } ?.treePrev ?.takeIf { it.elementType == WHITE_SPACE && it.text.contains("\n") } prevNewLine?.let { WRONG_NEWLINES.warnAndFix(configRules, emitWarn, isFixMode, "newline should be placed only after comma", node.startOffset, node) { it.treeParent.removeChild(it) } } } /** * Check that newline is not placed before or after a colon */ private fun handleColon(node: ASTNode) { val prevNewLine = node .parent(false) { it.treePrev != null } ?.treePrev ?.takeIf { it.elementType == WHITE_SPACE && it.text.contains("\n") } prevNewLine?.let { whiteSpace -> WRONG_NEWLINES.warnAndFix(configRules, emitWarn, isFixMode, "newline shouldn't be placed before colon", node.startOffset, node) { whiteSpace.treeParent.removeChild(whiteSpace) if (node.hasParent(VALUE_PARAMETER_LIST)) { // If inside the list of arguments the rule for a new line before the colon has worked, // then we delete the new line on both sides of the colon whiteSpace.treeParent?.let { val nextNewLine = node .parent(false) { it.treeNext != null } ?.treeNext ?.takeIf { it.elementType == WHITE_SPACE && it.text.contains("\n") } nextNewLine?.let { it.treeParent.addChild(PsiWhiteSpaceImpl(" "), it) it.treeParent.removeChild(it) } } } } } } @Suppress("TOO_LONG_FUNCTION") private fun handleLambdaBody(node: ASTNode) { if (node.treeParent.elementType == FUNCTION_LITERAL) { val isSingleLineLambda = node.treeParent .text .lines() .size == 1 val arrowNode = node.siblings(false).find { it.elementType == ARROW } if (!isSingleLineLambda && arrowNode != null) { // lambda with explicit arguments val newlinesBeforeArrow = arrowNode .siblings(false) .filter { it.isNewLineNode() } .toList() if (newlinesBeforeArrow.isNotEmpty() || !arrowNode.isFollowedByNewline()) { WRONG_NEWLINES.warnAndFix(configRules, emitWarn, isFixMode, "in lambda with several lines in body newline should be placed after an arrow", arrowNode.startOffset, arrowNode) { // fixme: replacement logic can be sophisticated for better appearance? newlinesBeforeArrow.forEach { it.treeParent.replaceChild(it, PsiWhiteSpaceImpl(" ")) } arrowNode.treeNext.takeIf { it.elementType == WHITE_SPACE }?.leaveOnlyOneNewLine() } } } else if (!isSingleLineLambda && arrowNode == null) { // lambda without arguments val lbraceNode = node.treeParent.firstChildNode if (!lbraceNode.isFollowedByNewline()) { WRONG_NEWLINES.warnAndFix(configRules, emitWarn, isFixMode, "in lambda with several lines in body newline should be placed after an opening brace", lbraceNode.startOffset, lbraceNode) { lbraceNode.treeNext.let { if (it.elementType == WHITE_SPACE) { it.leaveOnlyOneNewLine() } else { it.treeParent.addChild(PsiWhiteSpaceImpl("\n"), it) } } } } } } } @Suppress("UnsafeCallOnNullableType", "AVOID_NULL_CHECKS") private fun handleReturnStatement(node: ASTNode) { val blockNode = node.treeParent.takeIf { it.elementType == BLOCK && it.treeParent.elementType == FUN } val returnsUnit = node.children().count() == 1 // the only child is RETURN_KEYWORD val hasMultipleReturn = node.findAllDescendantsWithSpecificType(RETURN_KEYWORD).count() > 1 if (blockNode == null || returnsUnit || hasMultipleReturn) { // function is either already with expression body or definitely can't be converted to it // or the function has more than one keyword `return` inside it return } blockNode .children() .filterNot { it.elementType in emptyBlockList } .toList() .takeIf { it.size == 1 } ?.also { WRONG_NEWLINES.warnAndFix(configRules, emitWarn, isFixMode, "functions with single return statement should be simplified to expression body", node.startOffset, node) { val funNode = blockNode.treeParent val returnType = (funNode.psi as? KtNamedFunction)?.typeReference?.node val expression = node.findChildByType(RETURN_KEYWORD)!!.nextCodeSibling()!! val childBlockNode = funNode.findChildByType(BLOCK) funNode.apply { if (returnType != null) { removeRange(returnType.treeNext, null) addChild(PsiWhiteSpaceImpl(" "), null) } else if (childBlockNode != null) { removeChild(childBlockNode) } addChild(LeafPsiElement(EQ, "="), null) addChild(PsiWhiteSpaceImpl(" "), null) addChild(expression.clone() as ASTNode, null) } } } } /** * Checks that members of [VALUE_PARAMETER_LIST] (list of function parameters at declaration site) are separated with newlines. * Also checks that entries of [SUPER_TYPE_LIST] are separated by newlines. */ @Suppress("ComplexMethod") private fun handleList(node: ASTNode) { if (node.elementType == VALUE_PARAMETER_LIST) { // do not check list parameter in lambda node.findParentNodeWithSpecificType(LAMBDA_ARGUMENT)?.let { return } // do not check other value lists if (node.treeParent.elementType.let { it == FUNCTION_TYPE || it == FUNCTION_TYPE_RECEIVER }) { return } } if (node.elementType == VALUE_ARGUMENT_LIST && node.siblings(forward = false).any { it.elementType == REFERENCE_EXPRESSION }) { // check that it is not function invocation, but only supertype constructor calls return } val (numEntries, entryType) = when (node.elementType) { VALUE_PARAMETER_LIST -> (node.psi as KtParameterList).parameters.size to "value parameters" SUPER_TYPE_LIST -> (node.psi as KtSuperTypeList).entries.size to "supertype list entries" VALUE_ARGUMENT_LIST -> (node.psi as KtValueArgumentList).arguments.size to "value arguments" else -> { log.warn { "Unexpected node element type ${node.elementType}" } return } } if (numEntries > configuration.maxParametersInOneLine) { when (node.elementType) { VALUE_PARAMETER_LIST -> handleFirstValue(node, VALUE_PARAMETER, "first parameter should be placed on a separate line " + "or all other parameters should be aligned with it in declaration of <${node.getParentIdentifier()}>") VALUE_ARGUMENT_LIST -> handleFirstValue(node, VALUE_ARGUMENT, "first value argument (%s) should be placed on the new line " + "or all other parameters should be aligned with it") else -> { } } handleValueParameterList(node, entryType) } } private fun handleFirstValue(node: ASTNode, filterType: IElementType, warnText: String ) = node .children() .takeWhile { !it.textContains('\n') } .filter { it.elementType == filterType } .toList() .takeIf { it.size > 1 } ?.let { list -> val freeText = if (filterType == VALUE_ARGUMENT) { warnText.format(list.first().text) } else { warnText } WRONG_NEWLINES.warnAndFix(configRules, emitWarn, isFixMode, freeText, node.startOffset, node) { list.first().treePrev?.let { node.appendNewlineMergingWhiteSpace( list.first() .treePrev .takeIf { it.elementType == WHITE_SPACE }, list.first() ) } } } /** * Check that super classes are on separate lines (if there are three or more) * * @return true if there are less than three super classes or if all of them are on separate lines */ private fun isCorrectSuperTypeList(valueParameterList: List): Boolean { val superTypeList = valueParameterList.filter { it.elementType in listOf(SUPER_TYPE_CALL_ENTRY, SUPER_TYPE_ENTRY) } if (superTypeList.size <= 2) { return true } val classDefinitionNode = valueParameterList[0].treeParent?.treeParent val colonNodes = classDefinitionNode?.getAllChildrenWithType(COLON) val newlineBeforeSuperTypeList = colonNodes?.find {colonNode -> val newlineNode = colonNode.treeNext val superClassType = newlineNode?.treeNext newlineNode?.text?.count { it == '\n' } == 1 && superClassType?.elementType == SUPER_TYPE_LIST } // list elements are correct if they are sequence of (COMMA, '\n', supertype) var areElementsCorrect = true var valueParameter = valueParameterList[0].treeNext while (valueParameter != null) { val newlineNode = valueParameter.treeNext val superClassType = valueParameter.treeNext?.treeNext if (valueParameter.elementType != COMMA || newlineNode?.text?.count { it == '\n' } != 1 || superClassType?.elementType != SUPER_TYPE_ENTRY) { areElementsCorrect = false break } valueParameter = superClassType?.treeNext } return newlineBeforeSuperTypeList != null && areElementsCorrect } private fun setSuperClassesOnSeparateLines( node: ASTNode, valueParameterList: List, warnText: String, colonNodes: List? ) { WRONG_NEWLINES.warnAndFix( configRules, emitWarn, isFixMode, warnText, node.startOffset, node ) { valueParameterList.forEach { superClassNode -> val commaNode = superClassNode.treeNext val whiteSpaceNode = commaNode?.treeNext // put super classes on separate lines if (superClassNode.elementType in listOf(SUPER_TYPE_CALL_ENTRY, SUPER_TYPE_ENTRY) && commaNode?.text == "," && whiteSpaceNode?.elementType == WHITE_SPACE ) { commaNode.changeWhiteSpaceOnNewline(whiteSpaceNode, commaNode) } // add newline before the first element of super class list val colonNodeBeforeList = colonNodes?.find { colonNode -> colonNode.treeNext.text.count { it == '\n' } == 0 && colonNode.treeNext.treeNext.elementType == SUPER_TYPE_LIST } colonNodeBeforeList?.changeWhiteSpaceOnNewline(colonNodeBeforeList.treeNext, colonNodeBeforeList) } } } private fun fixInvalidCommas(node: ASTNode, warnText: String) { node.children() .filter { val isNewLineNext = it.treeNext?.isNewLineNode() ?: false val isNewLinePrev = it.treePrev?.isNewLineNode() ?: false (it.elementType == COMMA && !isNewLineNext) || // Move RPAR to the new line (it.elementType == RPAR && it.treePrev?.elementType != COMMA && !isNewLinePrev) } .toList() .takeIf { it.isNotEmpty() } ?.let { invalidCommas -> WRONG_NEWLINES.warnAndFix( configRules, emitWarn, isFixMode, warnText, node.startOffset, node ) { invalidCommas.forEach { commaOrRpar -> val nextWhiteSpace = commaOrRpar.treeNext?.takeIf { it.elementType == WHITE_SPACE } if (commaOrRpar.elementType == COMMA) { nextWhiteSpace?.treeNext?.let { commaOrRpar.appendNewlineMergingWhiteSpace(nextWhiteSpace, nextWhiteSpace.treeNext) } ?: commaOrRpar.treeNext?.treeParent?.appendNewlineMergingWhiteSpace(nextWhiteSpace, commaOrRpar.treeNext) } else { commaOrRpar.treeParent?.appendNewlineMergingWhiteSpace(nextWhiteSpace, commaOrRpar) } } } } } private fun handleValueParameterList(node: ASTNode, entryType: String) { val valueParameterList = node.children().toList() val warnText = node.getParentIdentifier()?.let { "$entryType should be placed on different lines in declaration of <${node.getParentIdentifier()}>" } ?: "$entryType should be placed on different lines" val classDefinitionNode = valueParameterList[0].treeParent?.treeParent val colonNodes = classDefinitionNode?.getAllChildrenWithType(COLON) if (!isCorrectSuperTypeList(valueParameterList)) { setSuperClassesOnSeparateLines(node, valueParameterList, warnText, colonNodes) } else { fixInvalidCommas(node, warnText) } } private fun ASTNode.isNewLineNode(): Boolean = this.run { elementType == WHITE_SPACE && textContains('\n') } @Suppress("UnsafeCallOnNullableType") private fun ASTNode.getParentIdentifier() = when (treeParent.elementType) { PRIMARY_CONSTRUCTOR -> treeParent.treeParent SECONDARY_CONSTRUCTOR -> parent(CLASS)!! else -> treeParent } .getIdentifierName()?.text private fun ASTNode.getOrderedCallExpressions(psi: PsiElement, result: MutableList) { // if statements here have the only right order - don't change it if (psi.children.isNotEmpty() && !psi.isFirstChildElementType(DOT_QUALIFIED_EXPRESSION) && !psi.isFirstChildElementType(SAFE_ACCESS_EXPRESSION)) { val firstChild = psi.firstChild if (firstChild.isFirstChildElementType(DOT_QUALIFIED_EXPRESSION) || firstChild.isFirstChildElementType(SAFE_ACCESS_EXPRESSION)) { getOrderedCallExpressions(firstChild.firstChild, result) } result.add(firstChild.node .siblings(true) .dropWhile { it.elementType in dropChainValues } .first() // node treeNext is ".", "?.", "!!", "::" ) } else if (psi.children.isNotEmpty()) { getOrderedCallExpressions(psi.firstChild, result) result.add(psi.firstChild .node .siblings(true) .dropWhile { it.elementType in dropChainValues } .first() // node treeNext is ".", "?.", "!!", "::" ) } } private fun KtBinaryExpression.dotCalls(right: Boolean = true) = (if (right) this.right else this.left) ?.node ?.takeIf { it.elementType == DOT_QUALIFIED_EXPRESSION } ?.findChildByType(DOT) ?.getCallChain() private fun ASTNode.isElvisCorrect(): Boolean { if (this.elementType != ELVIS) { return false } val binaryExpression = (this.treeParent.treeParent.psi as KtBinaryExpression) val leftDotCalls = binaryExpression.dotCalls(false) val rightDotCalls = binaryExpression.dotCalls() return (leftDotCalls?.size ?: 0) + (rightDotCalls?.size ?: 0) > configuration.maxCallsInOneLine && !this.isBeginByNewline() } /** * This function is needed because many operators are represented as a single child of [OPERATION_REFERENCE] node * e.g. [ANDAND] is a single child of [OPERATION_REFERENCE] */ private fun ASTNode.selfOrOperationReferenceParent() = treeParent.takeIf { it.elementType == OPERATION_REFERENCE } ?: this private fun ASTNode.isSingleDotStatementOnSingleLine() = parents() .takeWhile { it.elementType in expressionTypes } .singleOrNull() ?.let { it.text.lines().count() == 1 } ?: false // fixme: there could be other cases when dot means something else private fun ASTNode.isDotFromPackageOrImport() = elementType == DOT && parent(true) { it.elementType == IMPORT_DIRECTIVE || it.elementType == PACKAGE_DIRECTIVE } != null private fun PsiElement.isFirstChildElementType(elementType: IElementType) = this.firstChild.node.elementType == elementType /** * This method collects chain calls and checks it * * @return true - if there is error, and false if there is no error */ private fun ASTNode.isInvalidCallsChain(dropLeadingProperties: Boolean = true) = getCallChain(dropLeadingProperties)?.isNotValidCalls(this) ?: false private fun ASTNode.getCallChain(dropLeadingProperties: Boolean = true): List? { val parentExpressionList = getParentExpressions() .lastOrNull() ?.run { mutableListOf().also { getOrderedCallExpressions(psi, it) } } return if (dropLeadingProperties) { // fixme: we can't distinguish fully qualified names (like java.lang) from chain of property accesses (like list.size) for now parentExpressionList?.dropWhile { !isTextContainsFunctionCall(it.treeParent) } } else { parentExpressionList } } private fun isTextContainsFunctionCall(node: ASTNode): Boolean = node.textContains('(') || node.textContains('{') private fun List.isNotValidCalls(node: ASTNode): Boolean { if (this.size == 1) { return false } val callsByNewLine: ListOfList = mutableListOf() val callsInOneNewLine: MutableList = mutableListOf() this.forEach { astNode -> if (astNode.treePrev.isFollowedByNewline() || astNode.treePrev.isWhiteSpaceWithNewline()) { callsByNewLine.add(callsInOneNewLine.toMutableList()) callsInOneNewLine.clear() } callsInOneNewLine.add(astNode) if (astNode.treePrev.elementType == POSTFIX_EXPRESSION && !astNode.treePrev.isFollowedByNewline() && configuration.maxCallsInOneLine == 1) { return true } } callsByNewLine.add(callsInOneNewLine) return (callsByNewLine.find { it.contains(node) } ?: return false) .indexOf(node) + 1 > configuration.maxCallsInOneLine } /** * taking all expressions inside complex expression until we reach lambda arguments */ private fun ASTNode.getParentExpressions() = parents().takeWhile { it.elementType in chainExpressionTypes && it.elementType != LAMBDA_ARGUMENT } private fun isMultilineLambda(node: ASTNode): Boolean = (node.findAllDescendantsWithSpecificType(LAMBDA_ARGUMENT) .firstOrNull() ?.text ?.count { it == '\n' } ?: -1) > 0 /** * Getting the first call expression in call chain */ private fun ASTNode.isFirstCall() = getParentExpressions() .lastOrNull() ?.run { val firstCallee = mutableListOf().also { getOrderedCallExpressions(psi, it) }.first() findAllDescendantsWithSpecificType(firstCallee.elementType, false).first() === this@isFirstCall } ?: false /** * This method should be called on OPERATION_REFERENCE in the middle of BINARY_EXPRESSION */ private fun ASTNode.isInfixCall() = elementType == OPERATION_REFERENCE && firstChildNode.elementType == IDENTIFIER && treeParent.elementType == BINARY_EXPRESSION /** * This method checks that complex expression should be replace with new variable */ private fun ASTNode.isInParentheses() = parent { it.elementType == DOT_QUALIFIED_EXPRESSION || it.elementType == SAFE_ACCESS_EXPRESSION } ?.treeParent ?.elementType ?.let { it in parenthesesTypes } ?: false /** * [RuleConfiguration] for newlines placement */ private class NewlinesRuleConfiguration(config: Map) : RuleConfiguration(config) { /** * If the number of parameters on one line is more than this threshold, all parameters should be placed on separate lines. */ val maxParametersInOneLine = config["maxParametersInOneLine"]?.toInt() ?: 2 val maxCallsInOneLine = config["maxCallsInOneLine"]?.toInt() ?: MAX_CALLS_IN_ONE_LINE } companion object { private val log = KotlinLogging.logger {} const val MAX_CALLS_IN_ONE_LINE = 3 const val NAME_ID = "newlines" // fixme: these token sets can be not full, need to add new once as corresponding cases are discovered. // error is raised if these operators are prepended by newline private val lineBreakAfterOperators = TokenSet.create(ANDAND, OROR, PLUS, PLUSEQ, MINUS, MINUSEQ, MUL, MULTEQ, DIV, DIVEQ) // error is raised if these operators are followed by newline private val lineBreakBeforeOperators = TokenSet.create(DOT, SAFE_ACCESS, ELVIS, COLONCOLON) private val expressionTypes = TokenSet.create(DOT_QUALIFIED_EXPRESSION, SAFE_ACCESS_EXPRESSION, CALLABLE_REFERENCE_EXPRESSION, BINARY_EXPRESSION) private val chainExpressionTypes = TokenSet.create(DOT_QUALIFIED_EXPRESSION, SAFE_ACCESS_EXPRESSION) private val dropChainValues = TokenSet.create(EOL_COMMENT, WHITE_SPACE, BLOCK_COMMENT, KDOC) private val parenthesesTypes = TokenSet.create(CONDITION, WHEN, VALUE_ARGUMENT) } } /** * Checks whether [this] function is recursive, i.e. calls itself inside from it's body * * @return true if function is recursive, false otherwise */ fun KtNamedFunction.isRecursive() = bodyBlockExpression ?.statements?.any { statement -> statement.anyDescendantOfType { it.text == this@isRecursive.name } } ?: false ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/files/SemicolonsRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter3.files import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.REDUNDANT_SEMICOLON import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.extractLineOfText import com.saveourtool.diktat.ruleset.utils.isEol import org.jetbrains.kotlin.KtNodeTypes.ENUM_ENTRY import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.lexer.KtTokens.SEMICOLON /** * Rule that checks usage of semicolons at the end of line */ @Suppress("ForbiddenComment") class SemicolonsRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(REDUNDANT_SEMICOLON) ) { override fun logic(node: ASTNode) { if (node.elementType == SEMICOLON) { handleSemicolon(node) } } /** * Check that EOL semicolon is used only in enums */ private fun handleSemicolon(node: ASTNode) { if (node.isEol() && node.treeParent.elementType != ENUM_ENTRY) { // semicolon at the end of line which is not part of enum members declarations REDUNDANT_SEMICOLON.warnAndFix(configRules, emitWarn, isFixMode, node.extractLineOfText(), node.startOffset, node) { node.treeParent.removeChild(node) } } } companion object { const val NAME_ID = "semicolon" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/files/TopLevelOrderRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter3.files import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.TOP_LEVEL_ORDER import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.* import com.saveourtool.diktat.ruleset.utils.isPartOfComment import org.jetbrains.kotlin.KtNodeTypes.CLASS import org.jetbrains.kotlin.KtNodeTypes.FUN import org.jetbrains.kotlin.KtNodeTypes.IMPORT_LIST import org.jetbrains.kotlin.KtNodeTypes.OBJECT_DECLARATION import org.jetbrains.kotlin.KtNodeTypes.PROPERTY import org.jetbrains.kotlin.KtNodeTypes.TYPEALIAS import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.lexer.KtTokens.INTERNAL_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.OVERRIDE_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.PRIVATE_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.PROTECTED_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.WHITE_SPACE import org.jetbrains.kotlin.psi.KtFunction import org.jetbrains.kotlin.psi.psiUtil.isExtensionDeclaration import org.jetbrains.kotlin.psi.psiUtil.siblings import org.jetbrains.kotlin.psi.stubs.elements.KtFileElementType /** * Rule that checks order in top level */ class TopLevelOrderRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(TOP_LEVEL_ORDER), ) { override fun logic(node: ASTNode) { if (node.elementType == KtFileElementType.INSTANCE) { checkNode(node) } } @Suppress("UnsafeCallOnNullableType") private fun checkNode(node: ASTNode) { val children = node.getChildren(null) val initialElementsOrder = children.filter { it.elementType in sortedType } if (initialElementsOrder.isEmpty()) { return } val properties = Properties(children.filter { it.elementType == PROPERTY }).sortElements() val functions = children.filter { it.elementType == FUN } val typealiases = children.filter { it.elementType == TYPEALIAS } val classes = children.filter { it.elementType == CLASS || it.elementType == OBJECT_DECLARATION } val sortedElementsWithTrailingNonCodeNodes = Blocks(properties, typealiases, functions, classes).sortElements().map { astNode -> astNode to astNode.siblings(false).takeWhile { it.elementType == WHITE_SPACE || it.isPartOfComment() }.toList() } val lastNonSortedChildren = initialElementsOrder.last().siblings(true).toList() sortedElementsWithTrailingNonCodeNodes.filterIndexed { index, pair -> initialElementsOrder[index] != pair.first } .forEach { listOfChildren -> val wrongNode = listOfChildren.first TOP_LEVEL_ORDER.warnAndFix(configRules, emitWarn, isFixMode, wrongNode.text, wrongNode.startOffset, wrongNode) { node.removeRange(node.findChildByType(IMPORT_LIST)!!.treeNext, node.lastChildNode) node.removeChild(node.lastChildNode) sortedElementsWithTrailingNonCodeNodes.map { (sortedNode, sortedNodePrevSibling) -> sortedNodePrevSibling.reversed().map { node.addChild(it, null) } node.addChild(sortedNode, null) } lastNonSortedChildren.map { node.addChild(it, null) } } } } /** * Interface for classes to collect child and sort them */ interface Elements { /** * Method to sort children * * @return sorted mutable list */ fun sortElements(): MutableList } /** * Class containing different groups of properties in file * * @param properties */ private data class Properties(private val properties: List) : Elements { override fun sortElements(): MutableList { val constValProperties = properties.filter { it.isConstant() } val valProperties = properties.filter { it.isValProperty() && !it.isConstant() } val lateinitProperties = properties.filter { it.isLateInit() } val varProperties = properties.filter { it.isVarProperty() && !it.isLateInit() } return listOf(constValProperties, valProperties, lateinitProperties, varProperties).flatten().toMutableList() } } /** * Class containing different children in file * * @param properties * @param typealiases * @param functions * @param classes */ private data class Blocks( private val properties: List, private val typealiases: List, private val functions: List, private val classes: List ) : Elements { override fun sortElements(): MutableList { val (extensionFun, nonExtensionFun) = functions.partition { (it.psi as KtFunction).isExtensionDeclaration() } return (properties + listOf(typealiases, classes, extensionFun, nonExtensionFun).flatMap { nodes -> val (privatePart, notPrivatePart) = nodes.partition { it.hasModifier(PRIVATE_KEYWORD) } val (protectedPart, notProtectedPart) = notPrivatePart.partition { it.hasModifier(PROTECTED_KEYWORD) || it.hasModifier(OVERRIDE_KEYWORD) } val (internalPart, publicPart) = notProtectedPart.partition { it.hasModifier(INTERNAL_KEYWORD) } listOf(publicPart, internalPart, protectedPart, privatePart).flatten() }).toMutableList() } } companion object { const val NAME_ID = "top-level-order" /** * List of children that should be sort */ val sortedType = listOf(PROPERTY, FUN, CLASS, OBJECT_DECLARATION, TYPEALIAS) } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/files/WhiteSpaceRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter3.files import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.common.config.rules.getRuleConfig import com.saveourtool.diktat.ruleset.constants.Warnings.LONG_LINE import com.saveourtool.diktat.ruleset.constants.Warnings.WRONG_INDENTATION import com.saveourtool.diktat.ruleset.constants.Warnings.WRONG_WHITESPACE import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.rules.chapter3.LineLength import com.saveourtool.diktat.ruleset.utils.appendNewlineMergingWhiteSpace import com.saveourtool.diktat.ruleset.utils.calculateLineColByOffset import com.saveourtool.diktat.ruleset.utils.findParentNodeWithSpecificType import com.saveourtool.diktat.ruleset.utils.hasChildOfType import com.saveourtool.diktat.ruleset.utils.isPartOfComment import com.saveourtool.diktat.ruleset.utils.isWhiteSpace import com.saveourtool.diktat.ruleset.utils.nextCodeLeaf import com.saveourtool.diktat.ruleset.utils.parent import com.saveourtool.diktat.ruleset.utils.prevSibling import io.github.oshai.kotlinlogging.KotlinLogging import org.jetbrains.kotlin.KtNodeTypes.ANNOTATION_ENTRY import org.jetbrains.kotlin.KtNodeTypes.BINARY_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.BLOCK import org.jetbrains.kotlin.KtNodeTypes.CALLABLE_REFERENCE_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.CALL_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.CLASS import org.jetbrains.kotlin.KtNodeTypes.COLLECTION_LITERAL_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.CONSTRUCTOR_DELEGATION_CALL import org.jetbrains.kotlin.KtNodeTypes.FUN import org.jetbrains.kotlin.KtNodeTypes.FUNCTION_LITERAL import org.jetbrains.kotlin.KtNodeTypes.LAMBDA_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.NULLABLE_TYPE import org.jetbrains.kotlin.KtNodeTypes.OBJECT_DECLARATION import org.jetbrains.kotlin.KtNodeTypes.OPERATION_REFERENCE import org.jetbrains.kotlin.KtNodeTypes.POSTFIX_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.PRIMARY_CONSTRUCTOR import org.jetbrains.kotlin.KtNodeTypes.PROPERTY import org.jetbrains.kotlin.KtNodeTypes.SECONDARY_CONSTRUCTOR import org.jetbrains.kotlin.KtNodeTypes.TYPE_CONSTRAINT import org.jetbrains.kotlin.KtNodeTypes.TYPE_PARAMETER import org.jetbrains.kotlin.KtNodeTypes.TYPE_PARAMETER_LIST import org.jetbrains.kotlin.KtNodeTypes.VALUE_ARGUMENT import org.jetbrains.kotlin.KtNodeTypes.VALUE_ARGUMENT_LIST import org.jetbrains.kotlin.KtNodeTypes.VALUE_PARAMETER import org.jetbrains.kotlin.KtNodeTypes.VALUE_PARAMETER_LIST import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafElement import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl import org.jetbrains.kotlin.com.intellij.psi.tree.TokenSet import org.jetbrains.kotlin.lexer.KtTokens.ARROW import org.jetbrains.kotlin.lexer.KtTokens.CATCH_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.COLON import org.jetbrains.kotlin.lexer.KtTokens.COLONCOLON import org.jetbrains.kotlin.lexer.KtTokens.COMMA import org.jetbrains.kotlin.lexer.KtTokens.CONSTRUCTOR_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.DOT import org.jetbrains.kotlin.lexer.KtTokens.DO_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.ELSE_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.ELVIS import org.jetbrains.kotlin.lexer.KtTokens.EQ import org.jetbrains.kotlin.lexer.KtTokens.EXCLEXCL import org.jetbrains.kotlin.lexer.KtTokens.FINALLY_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.FOR_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.GT import org.jetbrains.kotlin.lexer.KtTokens.IF_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.INIT_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.LBRACE import org.jetbrains.kotlin.lexer.KtTokens.LBRACKET import org.jetbrains.kotlin.lexer.KtTokens.LPAR import org.jetbrains.kotlin.lexer.KtTokens.LT import org.jetbrains.kotlin.lexer.KtTokens.QUEST import org.jetbrains.kotlin.lexer.KtTokens.RANGE import org.jetbrains.kotlin.lexer.KtTokens.RBRACE import org.jetbrains.kotlin.lexer.KtTokens.RBRACKET import org.jetbrains.kotlin.lexer.KtTokens.RPAR import org.jetbrains.kotlin.lexer.KtTokens.SAFE_ACCESS import org.jetbrains.kotlin.lexer.KtTokens.SEMICOLON import org.jetbrains.kotlin.lexer.KtTokens.TRY_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.WHEN_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.WHILE_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.WHITE_SPACE import org.jetbrains.kotlin.psi.KtBinaryExpression import org.jetbrains.kotlin.psi.KtPostfixExpression import org.jetbrains.kotlin.psi.KtPrefixExpression import org.jetbrains.kotlin.psi.psiUtil.parents import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf import org.jetbrains.kotlin.psi.stubs.elements.KtFileElementType /** * This rule checks usage of whitespaces for horizontal code separation * 1. There should be single space between keyword and (, unless keyword is `constructor` * 2. There should be single space between keyword and { * 3. There should be single space before any {, unless lambda inside parentheses of argument list * 4. Binary operators should be surrounded by whitespaces, excluding :: and . * 5. Spaces should be used after `,`, `:`, `;` (except cases when those symbols are in the end of line). * There should be no whitespaces in the end of line. * 6. There should be only one space between identifier and it's type, if type is nullable there should be no spaces before `?` * 7. There should be no space before `[` * 8. There should be no space between a method or constructor name (both at declaration and at call site) and a parenthesis. * 9. There should be no space after `(`, `[` and `<` (in templates); no space before `)`, `]`, `>` (in templates) * 10. There should be no spaces between prefix/postfix operator (like `!!` or `++`) and it's operand */ @Suppress("ForbiddenComment") class WhiteSpaceRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(WRONG_WHITESPACE, LONG_LINE, WRONG_INDENTATION) ) { private val configuration by lazy { LineLength.LineLengthConfiguration( configRules.getRuleConfig(LONG_LINE)?.configuration ?: emptyMap() ) } private lateinit var positionByOffset: (Int) -> Pair @Suppress("ComplexMethod") override fun logic(node: ASTNode) { when (node.elementType) { // keywords CONSTRUCTOR_KEYWORD -> handleConstructor(node) in keywordsWithSpaceAfter -> handleKeywordWithParOrBrace(node) // operators and operator-like symbols DOT, ARROW, SAFE_ACCESS, EQ -> handleBinaryOperator(node) OPERATION_REFERENCE -> handleOperator(node) COLON -> handleColon(node) COLONCOLON -> handleColonColon(node) COMMA, SEMICOLON -> handleToken(node, 0, 1) QUEST -> if (node.treeParent.elementType == NULLABLE_TYPE) { handleToken(node, 0, null) } // braces and other symbols LBRACE -> handleLbrace(node) RBRACE -> handleRbrace(node) LBRACKET -> handleLbracket(node) LPAR -> handleLpar(node) RPAR, RBRACKET -> handleToken(node, 0, null) GT, LT -> handleGtOrLt(node) // white space WHITE_SPACE -> handleEolWhiteSpace(node) else -> { } } } private fun handleLbracket(node: ASTNode) = if (node.treeParent.elementType == COLLECTION_LITERAL_EXPRESSION) { handleToken(node, 1, 0) } else { handleToken(node, 0, 0) } private fun handleConstructor(node: ASTNode) { if (node.treeNext.numWhiteSpaces()?.let { it > 0 } == true) { // there is either whitespace or newline after constructor keyword WRONG_WHITESPACE.warnAndFix(configRules, emitWarn, isFixMode, "keyword '${node.text}' should not be separated from " + "'(' with a whitespace", node.startOffset, node) { node.treeParent.removeChild(node.treeNext) } } } @Suppress("UnsafeCallOnNullableType") private fun handleKeywordWithParOrBrace(node: ASTNode) { if (node.treeNext.numWhiteSpaces() != 1) { // there is either not single whitespace or newline after keyword val nextCodeLeaf = node.nextCodeLeaf()!! if (nextCodeLeaf.elementType != LPAR && nextCodeLeaf.elementType != LBRACE && node.treeNext.textContains('\n')) { // statement after keyword doesn't have braces and starts at new line, e.g. `if (condition) foo()\n else\n bar()` return } WRONG_WHITESPACE.warnAndFix(configRules, emitWarn, isFixMode, "keyword '${node.text}' should be separated from " + "'${nextCodeLeaf.text}' with a whitespace", nextCodeLeaf.startOffset, nextCodeLeaf) { node.leaveSingleWhiteSpace() } } } private fun handleRbrace(node: ASTNode) { if (node.treeParent.elementType == FUNCTION_LITERAL && !node.treePrev.isWhiteSpace() && node.treePrev.elementType == BLOCK && node.treePrev.text.isNotEmpty()) { WRONG_WHITESPACE.warnAndFix(configRules, emitWarn, isFixMode, "there should be a whitespace before }", node.startOffset, node) { node.treeParent.addChild(PsiWhiteSpaceImpl(" "), node) } } } /** * This method covers all other opening braces, not covered in [handleKeywordWithParOrBrace]. */ @Suppress("UnsafeCallOnNullableType") private fun handleLbrace(node: ASTNode) { // `{` can't be the very first symbol in the file, so `!!` should be safe val whitespaceOrPrevNode = node.selfOrParentsTreePrev()!! val isFromLambdaAsArgument = node .parents() .take(NUM_PARENTS_FOR_LAMBDA) .toList() .takeIf { it.size == NUM_PARENTS_FOR_LAMBDA } ?.let { it[0].elementType == FUNCTION_LITERAL && it[1].elementType == LAMBDA_EXPRESSION && it[2].elementType == VALUE_ARGUMENT && // lambda is not passed as a named argument !it[2].hasChildOfType(EQ) } ?: false val prevNode = whitespaceOrPrevNode.let { if (it.elementType == WHITE_SPACE) it.treePrev else it } val numWhiteSpace = whitespaceOrPrevNode.numWhiteSpaces() handleWhiteSpaceBeforeLeftBrace(node, isFromLambdaAsArgument, numWhiteSpace, whitespaceOrPrevNode, prevNode) handleWhiteSpaceAfterLeftBrace(node) } private fun handleWhiteSpaceBeforeLeftBrace(node: ASTNode, isFromLambdaAsArgument: Boolean, numWhiteSpace: Int?, whitespaceOrPrevNode: ASTNode, prevNode: ASTNode ) { // note: the conditions in the following `if`s cannot be collapsed into simple conjunctions if (isFromLambdaAsArgument) { @Suppress("PARAMETER_NAME_IN_OUTER_LAMBDA") val isFirstArgument = node .parent { it.elementType == VALUE_ARGUMENT } .let { it?.prevSibling { prevNode -> prevNode.elementType == COMMA } == null } // Handling this case: `foo({ it.bar() }, 2, 3)` if (numWhiteSpace != 0 && isFirstArgument) { WRONG_WHITESPACE.warnAndFix(configRules, emitWarn, isFixMode, "there should be no whitespace before '{' of lambda" + " inside argument list", node.startOffset, node) { whitespaceOrPrevNode.treeParent.removeChild(whitespaceOrPrevNode) } } } else if (prevNode.elementType !in keywordsWithSpaceAfter && numWhiteSpace != 1) { val hasOnlyWhiteSpaceBefore = whitespaceOrPrevNode.elementType == WHITE_SPACE && whitespaceOrPrevNode.textContains('\n') if (!hasOnlyWhiteSpaceBefore) { WRONG_WHITESPACE.warnAndFix(configRules, emitWarn, isFixMode, "there should be a whitespace before '{'", node.startOffset, node) { prevNode.leaveSingleWhiteSpace() } } } } private fun handleWhiteSpaceAfterLeftBrace(node: ASTNode) { if (node.treeParent.elementType == FUNCTION_LITERAL && !node.treeNext.isWhiteSpace() && node.treeNext.elementType == BLOCK && node.treeNext.text.isNotEmpty()) { WRONG_WHITESPACE.warnAndFix(configRules, emitWarn, isFixMode, "there should be a whitespace after {", node.startOffset, node) { node.treeParent.addChild(PsiWhiteSpaceImpl(" "), node.treeNext) } } } private fun handleColonColon(node: ASTNode) { if (node.treeParent.elementType == CALLABLE_REFERENCE_EXPRESSION) { if (node.treeParent.firstChildNode != node) { // callable reference has receiver and shouldn't have any spaces around handleBinaryOperator(node) } else { // callable reference doesn't have receiver, it should stick to reference name and spaces before are determined // by other cases of this rule handleToken(node, null, 0) } } } private fun handleColon(node: ASTNode) { when (node.treeParent.elementType) { CLASS, SECONDARY_CONSTRUCTOR, TYPE_CONSTRAINT, TYPE_PARAMETER, OBJECT_DECLARATION -> handleBinaryOperator(node) VALUE_PARAMETER, PROPERTY, FUN -> handleToken(node, 0, 1) ANNOTATION_ENTRY -> handleToken(node, 0, 0) // e.g. @param:JsonProperty // fixme: find examples or delete this line else -> log.warn { "Colon with treeParent.elementType=${node.treeParent.elementType}, not handled by WhiteSpaceRule" } } } private fun handleOperator(node: ASTNode) { when (node.treeParent.psi) { is KtPrefixExpression -> handleToken(node, null, 0) is KtPostfixExpression -> handleToken(node, 0, null) is KtBinaryExpression -> handleBinaryOperator(node) else -> { } } } private fun handleBinaryOperator(node: ASTNode) { val operatorNode = if (node.elementType == OPERATION_REFERENCE) node.firstChildNode else node if (node.elementType == EQ && node.treeParent.elementType == OPERATION_REFERENCE) { return } if (node.elementType == OPERATION_REFERENCE && node.treeParent.elementType.let { it == BINARY_EXPRESSION || it == POSTFIX_EXPRESSION || it == PROPERTY } || node.elementType != OPERATION_REFERENCE) { val requiredNumSpaces = if (operatorNode.elementType in operatorsWithNoWhitespace) 0 else 1 handleToken(node, requiredNumSpaces, requiredNumSpaces) } } @Suppress("UnsafeCallOnNullableType") private fun handleToken( node: ASTNode, requiredSpacesBefore: Int?, requiredSpacesAfter: Int? ) { require(requiredSpacesBefore != null || requiredSpacesAfter != null) { "requiredSpacesBefore=$requiredSpacesBefore and requiredSpacesAfter=$requiredSpacesAfter, but at least one should not be null" } val spacesBefore = node.selfOrParentsTreePrev()!!.numWhiteSpaces() // calculate actual spaces after but only if requiredSpacesAfter is not null, otherwise we won't check it val spacesAfter = requiredSpacesAfter?.let { _ -> // for `!!` and possibly other postfix expressions treeNext and treeParent.treeNext can be null // upper levels are already outside of the expression this token belongs to, so we won't check them (node.treeNext ?: node.treeParent.treeNext) ?.numWhiteSpaces() } val isErrorBefore = requiredSpacesBefore != null && spacesBefore != null && spacesBefore != requiredSpacesBefore val isErrorAfter = requiredSpacesAfter != null && spacesAfter != null && spacesAfter != requiredSpacesAfter if (isErrorBefore || isErrorAfter) { val freeText = "${node.text} should have" + getDescription(requiredSpacesBefore != null, requiredSpacesAfter != null, requiredSpacesBefore, requiredSpacesAfter) + ", but has" + getDescription(isErrorBefore, isErrorAfter, spacesBefore, spacesAfter) WRONG_WHITESPACE.warnAndFix(configRules, emitWarn, isFixMode, freeText, node.startOffset, node) { node.fixSpaceAround(requiredSpacesBefore, requiredSpacesAfter) } } } private fun handleEolWhiteSpace(node: ASTNode) { val hasSpaces = node.text.substringBefore('\n').contains(' ') // the second condition corresponds to the last line of file val isEol = node.textContains('\n') || node.psi.parentsWithSelf.all { it.nextSibling == null } if (hasSpaces && isEol) { WRONG_WHITESPACE.warnAndFix(configRules, emitWarn, isFixMode, "there should be no spaces in the end of line", node.startOffset, node) { (node as LeafElement).rawReplaceWithText(node.text.trimStart(' ')) } } } @Suppress("UnsafeCallOnNullableType") private fun handleLpar(node: ASTNode) { when { node.treeParent.treeParent.elementType == SECONDARY_CONSTRUCTOR -> { // there is separate handler for 'constructor' keyword to provide custom warning message return } node.nextCodeLeaf()!!.elementType == LBRACE -> { // there is separate handler for lambda expression inside parenthesis return } node.treeParent.treeParent.elementType == ANNOTATION_ENTRY -> handleToken(node.treeParent, 0, null) else -> { } } val isDeclaration = node.treeParent.elementType == VALUE_PARAMETER_LIST && node.treeParent .treeParent .elementType .let { it == PRIMARY_CONSTRUCTOR || it == FUN || it == CALL_EXPRESSION } val isCall = node.treeParent.elementType == VALUE_ARGUMENT_LIST && node.treeParent .treeParent .elementType .let { it == CONSTRUCTOR_DELEGATION_CALL || it == CALL_EXPRESSION } if (isDeclaration || isCall) { handleToken(node, 0, 0) } else { handleToken(node, null, 0) } } private fun handleGtOrLt(node: ASTNode) { if (node.treeParent == TYPE_PARAMETER_LIST) { handleToken( node, if (node.elementType == GT) 0 else null, if (node.elementType == GT) null else 0 ) } } @Suppress("UnsafeCallOnNullableType") private fun ASTNode.isNeedNewLineInOperatorReferences(): Boolean { positionByOffset = this.findParentNodeWithSpecificType(KtFileElementType.INSTANCE)!!.calculateLineColByOffset() val offset = positionByOffset(this.startOffset).second return offset + this.text.length >= configuration.lineLength } @Suppress("UnsafeCallOnNullableType") private fun ASTNode.fixSpaceAround(requiredSpacesBefore: Int?, requiredSpacesAfter: Int?) { if (requiredSpacesBefore == 1) { selfOrParentsTreePrev()?.let { if (it.elementType == WHITE_SPACE) it.treePrev else it }?.leaveSingleWhiteSpace() if (this.isNeedNewLineInOperatorReferences() && this.firstChildNode.elementType == ELVIS) { this.treePrev.let { this.treeParent.appendNewlineMergingWhiteSpace(it, it) } } } else if (requiredSpacesBefore == 0) { selfOrParentsTreePrev()?.removeIfWhiteSpace() } if (requiredSpacesAfter == 1) { leaveSingleWhiteSpace() if (this.isNeedNewLineInOperatorReferences() && this.firstChildNode.elementType != ELVIS) { this.treeNext.let { this.treeParent.appendNewlineMergingWhiteSpace(it, it) } } } else if (requiredSpacesAfter == 0) { // for `!!` and possibly other postfix expressions treeNext can be null (treeNext ?: treeParent.treeNext).removeIfWhiteSpace() } } /** * Function that returns `treePrev` of this node, or if this.treePrev is null, `treePrev` of first parent node that has it */ private fun ASTNode.selfOrParentsTreePrev() = parent(false) { it.treePrev != null }?.treePrev /** * This method counts spaces in this node. Null is returned in following cases: * * if it is WHITE_SPACE with a newline * * if the next node is a comment, because spaces around comments are checked elsewhere. * * If this node is not a WHITE_SPACE, 0 is returned because there are zero white spaces. */ private fun ASTNode.numWhiteSpaces(): Int? = if (elementType != WHITE_SPACE) { 0 } else { // this can happen, e.g. in lambdas after an arrow, where block can be not surrounded by braces // treeNext may not have children ( {_, _ -> }) val isBlockStartingWithComment = treeNext.elementType == BLOCK && treeNext.firstChildNode?.isPartOfComment() == true if (textContains('\n') || treeNext.isPartOfComment() || isBlockStartingWithComment) null else text.count { it == ' ' } } private fun ASTNode.leaveSingleWhiteSpace() { if (treeNext.elementType == WHITE_SPACE) { (treeNext as LeafElement).rawReplaceWithText(" ") } else { treeParent.addChild(PsiWhiteSpaceImpl(" "), treeNext) } } private fun ASTNode.removeIfWhiteSpace() = takeIf { it.elementType == WHITE_SPACE && !it.textContains('\n') } ?.let { it.treeParent.removeChild(it) } private fun getDescription(shouldBefore: Boolean, shouldAfter: Boolean, before: Int?, after: Int? ): String = if (shouldBefore && shouldAfter) { " $before space(s) before and $after space(s) after" } else if (shouldBefore && !shouldAfter) { " $before space(s) before" } else if (shouldAfter) { " $after space(s) after" } else { "" } companion object { private val log = KotlinLogging.logger {} const val NAME_ID = "horizontal-whitespace" // this is the number of parent nodes needed to check if this node is lambda from argument list private const val NUM_PARENTS_FOR_LAMBDA = 3 private val keywordsWithSpaceAfter = TokenSet.create( // these keywords are followed by { ELSE_KEYWORD, TRY_KEYWORD, DO_KEYWORD, FINALLY_KEYWORD, INIT_KEYWORD, // these keywords are followed by ( FOR_KEYWORD, IF_KEYWORD, WHILE_KEYWORD, CATCH_KEYWORD, // these keywords can be followed by either { or ( WHEN_KEYWORD ) val operatorsWithNoWhitespace = TokenSet.create(DOT, RANGE, COLONCOLON, SAFE_ACCESS, EXCLEXCL) } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter3/identifiers/LocalVariablesRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter3.identifiers import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.LOCAL_VARIABLE_EARLY_DECLARATION import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.containsOnlyConstants import com.saveourtool.diktat.ruleset.utils.getDeclarationScope import com.saveourtool.diktat.ruleset.utils.getLineNumber import com.saveourtool.diktat.ruleset.utils.isPartOfComment import com.saveourtool.diktat.ruleset.utils.lastLineNumber import com.saveourtool.diktat.ruleset.utils.numNewLines import com.saveourtool.diktat.ruleset.utils.search.findAllVariablesWithUsages import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.PsiElement import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace import org.jetbrains.kotlin.lexer.KtTokens.SEMICOLON import org.jetbrains.kotlin.lexer.KtTokens.WHITE_SPACE import org.jetbrains.kotlin.psi.KtBlockExpression import org.jetbrains.kotlin.psi.KtCallExpression import org.jetbrains.kotlin.psi.KtNameReferenceExpression import org.jetbrains.kotlin.psi.KtProperty import org.jetbrains.kotlin.psi.psiUtil.getParentOfType import org.jetbrains.kotlin.psi.psiUtil.parents import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf import org.jetbrains.kotlin.psi.psiUtil.referenceExpression import org.jetbrains.kotlin.psi.psiUtil.siblings import org.jetbrains.kotlin.psi.psiUtil.startOffset import org.jetbrains.kotlin.psi.stubs.elements.KtFileElementType /** * This rule checks that local variables are declared close to the point where they are first used. * Current algorithm assumes that scopes are always `BLOCK`s. * 1. Warns if there are statements between variable declaration and it's first usage * 2. It is allowed to declare variables in outer scope compared to usage scope. It could be useful to store state, e.g. between loop iterations. * * Current limitations due to usage of AST only: * * Only properties without initialization or initialized with expressions based on constants are supported. * * Properties initialized with constructor calls cannot be distinguished from method call and are no supported. */ class LocalVariablesRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(LOCAL_VARIABLE_EARLY_DECLARATION) ) { override fun logic(node: ASTNode) { if (node.elementType == KtFileElementType.INSTANCE) { // collect all local properties and associate with corresponding references val propertiesToUsages = collectLocalPropertiesWithUsages(node) // find all usages which include more than one property val multiPropertyUsages = groupPropertiesByUsages(propertiesToUsages) multiPropertyUsages .forEach { (statement, properties) -> handleConsecutiveDeclarations(statement, properties) } propertiesToUsages .filterNot { it.key in multiPropertyUsages.values.flatten() } .forEach { handleLocalProperty(it.key, it.value) } } } private fun collectLocalPropertiesWithUsages(node: ASTNode) = node .findAllVariablesWithUsages { propertyNode -> propertyNode.isLocal && propertyNode.name != null && propertyNode.parent is KtBlockExpression && (propertyNode.isVar && propertyNode.initializer == null || (propertyNode.initializer?.containsOnlyConstants() ?: false) || (propertyNode.initializer as? KtCallExpression).isWhitelistedMethod()) } .filterNot { it.value.isEmpty() } @Suppress("TYPE_ALIAS") private fun groupPropertiesByUsages(propertiesToUsages: Map>) = propertiesToUsages .mapValues { (property, usages) -> getFirstUsageStatementOrBlock(usages, property.getDeclarationScope()) } .map { it.value to it.key } .groupByTo(mutableMapOf(), { it.first }) { it.second } .filter { it.value.size > 1 } .toMap>() @Suppress("UnsafeCallOnNullableType") private fun handleLocalProperty(property: KtProperty, usages: List) { val declarationScope = property.getDeclarationScope() val firstUsageStatementLine = getFirstUsageStatementOrBlock(usages, declarationScope).node.getLineNumber() val firstUsage = usages.minByOrNull { it.node.getLineNumber() }!! // should skip val and var before it's statement val offset = property .siblings(forward = true, withItself = false) .takeWhile { it != getFirstUsageStatementOrBlock(usages, declarationScope) } .filter { it is KtProperty } .count() checkLineNumbers(property, firstUsageStatementLine, firstUsageLine = firstUsage.node.getLineNumber(), offset = offset) } /** * Check declarations, for which the properties are used on the same line. * If properties are used for the first time in the same statement, then they can be declared on consecutive lines * with maybe empty lines in between. */ @Suppress("TOO_LONG_FUNCTION") private fun handleConsecutiveDeclarations(statement: PsiElement, properties: List) { val numLinesAfterLastProp = properties .last() .node .treeNext .takeIf { it.elementType == WHITE_SPACE } ?.let { // minus one is needed to except \n after property it.numNewLines() - 1 } ?: 0 val sortedProperties = properties.sortedBy { it.node.getLineNumber() } // need to check that properties are declared consecutively with only maybe empty lines sortedProperties .zip( (properties.size - 1 downTo 0).map { index -> val siblings = sortedProperties[properties.lastIndex - index].siblings(forward = true, withItself = false) // Also we need to count number of comments to skip. See `should skip comments` test // For the last property we don't need to count, because they will be counted in checkLineNumbers // We count number of comments beginning from next property val numberOfComments = siblings .takeWhile { it != statement } .dropWhile { it !is KtProperty } .filter { it.node.isPartOfComment() } .count() // We should also skip all vars that were not included in properties list, but they are between statement and current property val numberOfVarWithInitializer = siblings .takeWhile { it != statement } .filter { it is KtProperty && it !in properties } .count() // If it is not last property we should consider number on new lines after last property in list if (index != 0) { index + numLinesAfterLastProp + numberOfComments + numberOfVarWithInitializer } else { index + numberOfComments + numberOfVarWithInitializer } } ) .forEach { (property, offset) -> checkLineNumbers(property, statement.node.getLineNumber(), offset) } } @Suppress("UnsafeCallOnNullableType") private fun checkLineNumbers( property: KtProperty, firstUsageStatementLine: Int, offset: Int = 0, firstUsageLine: Int? = null ) { val numLinesToSkip = property .siblings(forward = true, withItself = false) .takeWhile { it is PsiWhiteSpace || it.node.elementType == SEMICOLON || it.node.isPartOfComment() } .let { siblings -> siblings .last() .node .lastLineNumber() - siblings .first() .node .getLineNumber() - 1 } if (firstUsageStatementLine - numLinesToSkip != property.node.lastLineNumber() + 1 + offset) { LOCAL_VARIABLE_EARLY_DECLARATION.warn(configRules, emitWarn, warnMessage(property.name!!, property.node.getLineNumber(), firstUsageLine ?: firstUsageStatementLine), property.startOffset, property.node) } } /** * Returns the [KtBlockExpression] with which a property should be compared. * If the usage is in nested block, compared to declaration, then statement from declaration scope, which contains block * with usage, is returned. * * @return either the line on which the property is used if it is first used in the same scope, or the block in the same scope as declaration */ @Suppress("UnsafeCallOnNullableType", "GENERIC_VARIABLE_WRONG_DECLARATION") private fun getFirstUsageStatementOrBlock(usages: List, declarationScope: KtBlockExpression?): PsiElement { val firstUsage = usages.minByOrNull { it.node.getLineNumber() }!! val firstUsageScope = firstUsage.getParentOfType(true) return if (firstUsageScope == declarationScope) { // property is first used in the same scope where it is declared, we check line of statement where it is first used firstUsage .parents .find { it.parent == declarationScope }!! } else { // first usage is in deeper block compared to declaration, need to check how close is declaration to the first line of the block usages.minByOrNull { it.node.getLineNumber() }!! .parentsWithSelf .find { it.parent == declarationScope }!! } } private fun KtCallExpression?.isWhitelistedMethod() = this?.run { // `referenceExpression()` can return something different than just a name, e.g. when function returns a function: // `foo()()` `referenceExpression()` will be a `KtCallExpression` as well (referenceExpression() as? KtNameReferenceExpression)?.getReferencedName() in functionInitializers && valueArguments.isEmpty() } ?: false private fun warnMessage( name: String, declared: Int, used: Int ) = "<$name> is declared on line <$declared> and is used for the first time on line <$used>" companion object { const val NAME_ID = "local-variables" private val functionInitializers = listOf( "emptyList", "emptySet", "emptyMap", "emptyArray", "emptySequence", "listOf", "setOf", "mapOf", "arrayOf", "arrayListOf", "mutableListOf", "mutableSetOf", "mutableMapOf", "linkedMapOf", "linkedSetOf" ) } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter4/ImmutableValNoVarRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter4 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.SAY_NO_TO_VAR import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.search.findAllVariablesWithAssignments import com.saveourtool.diktat.ruleset.utils.search.findAllVariablesWithUsages import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.psi.KtBlockExpression import org.jetbrains.kotlin.psi.KtLambdaExpression import org.jetbrains.kotlin.psi.KtLoopExpression import org.jetbrains.kotlin.psi.psiUtil.getParentOfType import org.jetbrains.kotlin.psi.stubs.elements.KtFileElementType /** * Variables with `val` modifier - are immutable (read-only). * Usage of such variables instead of `var` variables increases robustness and readability of code, * because `var` variables can be reassigned several times in the business logic. Of course, in some scenarios with loops or accumulators only `var`s can be used and are allowed. * FixMe: here we should also raise warnings for a reassignment of a var (if var has no assignments except in declaration - it can be final) */ class ImmutableValNoVarRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(SAY_NO_TO_VAR) ) { override fun logic(node: ASTNode) { if (node.elementType == KtFileElementType.INSTANCE) { // we will raise warning for cases when var property has no assignments val varNoAssignments = node .findAllVariablesWithAssignments { it.name != null && it.isVar } .filter { it.value.isEmpty() } varNoAssignments.forEach { (_, _) -> // FixMe: raise another warning and fix the code (change to val) for variables without assignment } // we can force to be immutable only variables that are from local context (not from class and not from file-level) val usages = node .findAllVariablesWithUsages { it.isLocal && it.name != null && it.parent is KtBlockExpression && it.isVar } .filter { !varNoAssignments.containsKey(it.key) } usages.forEach { (property, usages) -> val usedInAccumulators = usages.any { it.getParentOfType(true) != null || it.getParentOfType(true) != null } if (!usedInAccumulators) { SAY_NO_TO_VAR.warn(configRules, emitWarn, property.text, property.node.startOffset, property.node) } } return } } companion object { const val NAME_ID = "no-var-rule" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter4/NullChecksRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter4 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.AVOID_NULL_CHECKS import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.* import com.saveourtool.diktat.ruleset.utils.parent import org.jetbrains.kotlin.KtNodeTypes.BINARY_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.BLOCK import org.jetbrains.kotlin.KtNodeTypes.BREAK import org.jetbrains.kotlin.KtNodeTypes.CALL_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.CLASS_INITIALIZER import org.jetbrains.kotlin.KtNodeTypes.CONDITION import org.jetbrains.kotlin.KtNodeTypes.ELSE import org.jetbrains.kotlin.KtNodeTypes.IF import org.jetbrains.kotlin.KtNodeTypes.NULL import org.jetbrains.kotlin.KtNodeTypes.REFERENCE_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.THEN import org.jetbrains.kotlin.KtNodeTypes.VALUE_ARGUMENT import org.jetbrains.kotlin.KtNodeTypes.VALUE_ARGUMENT_LIST import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.lexer.KtTokens.IF_KEYWORD import org.jetbrains.kotlin.psi.KtBinaryExpression import org.jetbrains.kotlin.psi.KtBlockExpression import org.jetbrains.kotlin.psi.KtIfExpression import org.jetbrains.kotlin.psi.KtPsiUtil import org.jetbrains.kotlin.psi.psiUtil.blockExpressionsOrSingle import org.jetbrains.kotlin.psi.psiUtil.parents typealias ThenAndElseLines = Pair?, List?> /** * This rule check and fixes explicit null checks (explicit comparison with `null`) * There are several code-structures that can be used in Kotlin to avoid null-checks. For example: `?:`, `.let {}`, `.also {}`, e.t.c */ class NullChecksRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(AVOID_NULL_CHECKS) ) { override fun logic(node: ASTNode) { if (node.elementType == CONDITION) { node.parent(IF)?.let { // excluding complex cases with else-if statements, because they look better with explicit null-check if (!isComplexIfStatement(it)) { // this can be autofixed as the condition stays in simple if-statement conditionInIfStatement(node) } } } if (node.elementType == BINARY_EXPRESSION) { // `condition` case is already checked above, so no need to check it once again node.parent(CONDITION) ?: run { // only warning here, because autofix in other statements (like) lambda (or value) can break the code nullCheckInOtherStatements(node) } } } /** * checks that if-statement is a complex condition * You can name a statement - "complex if-statement" if it has other if in the else branch (else-if structure) */ private fun isComplexIfStatement(parentIf: ASTNode): Boolean { val parentIfPsi = parentIf.psi require(parentIfPsi is KtIfExpression) return (parentIfPsi.`else` ?.node ?.firstChildNode ?.elementType == IF_KEYWORD) } private fun conditionInIfStatement(node: ASTNode) { node.findAllDescendantsWithSpecificType(BINARY_EXPRESSION).forEach { binaryExprNode -> val condition = (binaryExprNode.psi as KtBinaryExpression) if (isNullCheckBinaryExpression(condition)) { val isEqualToNull = when (condition.operationToken) { // `==` and `===` comparison can be fixed with `?:` operator KtTokens.EQEQ, KtTokens.EQEQEQ -> true // `!=` and `!==` comparison can be fixed with `.let/also` operators KtTokens.EXCLEQ, KtTokens.EXCLEQEQEQ -> false else -> return } val (_, elseCodeLines) = getThenAndElseLines(node, isEqualToNull) val (numberOfStatementsInElseBlock, isAssignmentInNewElseBlock) = getInfoAboutElseBlock(node, isEqualToNull) // if `if-else` block inside `init` or 'run', 'with', 'apply' blocks and there is more than one statement inside 'else' block, then // we don't have to make any fixes, because this leads to kotlin compilation error after adding 'run' block instead of 'else' block // read https://youtrack.jetbrains.com/issue/KT-64174 for more information if (shouldBeWarned(node, elseCodeLines, numberOfStatementsInElseBlock, isAssignmentInNewElseBlock)) { warnAndFixOnNullCheck( condition, canBeAutoFixed(node, isEqualToNull), "use '.let/.also/?:/e.t.c' instead of ${condition.text}" ) { fixNullInIfCondition(node, condition, isEqualToNull) } } } } } /** * Checks whether it is necessary to warn about null-check */ private fun shouldBeWarned( condition: ASTNode, elseCodeLines: List?, numberOfStatementsInElseBlock: Int, isAssignment: Boolean, ): Boolean = when { // else { "null"/empty } -> "" isNullOrEmptyElseBlock(elseCodeLines) -> true // else { bar() } -> ?: bar() isOnlyOneNonAssignmentStatementInElseBlock(numberOfStatementsInElseBlock, isAssignment) -> true // else { ... } -> ?: run { ... } else -> isNotInsideWrongBlock(condition) } private fun isOnlyOneNonAssignmentStatementInElseBlock(numberOfStatementsInElseBlock: Int, isAssignment: Boolean) = numberOfStatementsInElseBlock == 1 && !isAssignment private fun isNullOrEmptyElseBlock(elseCodeLines: List?) = elseCodeLines == null || elseCodeLines.singleOrNull() == "null" private fun isNotInsideWrongBlock(condition: ASTNode): Boolean = condition.parents().none { it.elementType == CLASS_INITIALIZER || (it.elementType == CALL_EXPRESSION && it.findChildByType(REFERENCE_EXPRESSION)?.text in listOf("run", "with", "apply")) } /** * Checks whether null-check can be auto fixed */ @Suppress("UnsafeCallOnNullableType") private fun canBeAutoFixed(condition: ASTNode, isEqualToNull: Boolean): Boolean { // Handle cases with `break` word in blocks val typePair = if (isEqualToNull) { (ELSE to THEN) } else { (THEN to ELSE) } val isBlockInIfWithBreak = condition.getBreakNodeFromIf(typePair.first) val isOneLineBlockInIfWithBreak = condition .treeParent .findChildByType(typePair.second) ?.let { it.findChildByType(BLOCK) ?: it } ?.let { astNode -> astNode.hasChildOfType(BREAK) && (astNode.psi as? KtBlockExpression)?.statements?.size != 1 } ?: false return (!isBlockInIfWithBreak && !isOneLineBlockInIfWithBreak) } @Suppress("UnsafeCallOnNullableType", "TOO_LONG_FUNCTION") private fun fixNullInIfCondition( condition: ASTNode, binaryExpression: KtBinaryExpression, isEqualToNull: Boolean ) { val variableName = binaryExpression.left!!.text val (thenCodeLines, elseCodeLines) = getThenAndElseLines(condition, isEqualToNull) val (numberOfStatementsInElseBlock, isAssignmentInNewElseBlock) = getInfoAboutElseBlock(condition, isEqualToNull) val elseEditedCodeLines = getEditedElseCodeLines(elseCodeLines, numberOfStatementsInElseBlock, isAssignmentInNewElseBlock) val thenEditedCodeLines = getEditedThenCodeLines(variableName, thenCodeLines, elseEditedCodeLines) val newTextForReplacement = "$thenEditedCodeLines $elseEditedCodeLines" val newNodeForReplacement = KotlinParser().createNode(newTextForReplacement) val ifNode = condition.treeParent ifNode.treeParent.replaceChild(ifNode, newNodeForReplacement) } private fun getThenAndElseLines(condition: ASTNode, isEqualToNull: Boolean): ThenAndElseLines { val thenFromExistingCode = condition.extractLinesFromBlock(THEN) val elseFromExistingCode = condition.extractLinesFromBlock(ELSE) // if (a == null) { foo() } else { bar() } -> if (a != null) { bar() } else { foo() } val thenCodeLines = if (isEqualToNull) { elseFromExistingCode } else { thenFromExistingCode } val elseCodeLines = if (isEqualToNull) { thenFromExistingCode } else { elseFromExistingCode } return Pair(thenCodeLines, elseCodeLines) } private fun getInfoAboutElseBlock(condition: ASTNode, isEqualToNull: Boolean) = ((condition.treeParent.psi as? KtIfExpression) ?.let { if (isEqualToNull) { it.then } else { it.`else` } } ?.blockExpressionsOrSingle() ?.let { elements -> elements.count() to elements.any { element -> KtPsiUtil.isAssignment(element) } } ?: Pair(0, false)) @Suppress("UnsafeCallOnNullableType") private fun getEditedElseCodeLines( elseCodeLines: List?, numberOfStatementsInElseBlock: Int, isAssignment: Boolean, ): String = when { // else { "null"/empty } -> "" isNullOrEmptyElseBlock(elseCodeLines) -> "" // else { bar() } -> ?: bar() isOnlyOneNonAssignmentStatementInElseBlock(numberOfStatementsInElseBlock, isAssignment) -> "?: ${elseCodeLines!!.joinToString(postfix = "\n", separator = "\n")}" // else { ... } -> ?: run { ... } else -> getDefaultCaseElseCodeLines(elseCodeLines!!) } @Suppress("UnsafeCallOnNullableType") private fun getEditedThenCodeLines( variableName: String, thenCodeLines: List?, elseEditedCodeLines: String ): String = when { // if (a != null) { } -> a ?: editedElse (thenCodeLines.isNullOrEmpty() && elseEditedCodeLines.isNotEmpty()) || // if (a != null) { a } else { ... } -> a ?: editedElse (thenCodeLines?.singleOrNull() == variableName && elseEditedCodeLines.isNotEmpty()) -> variableName // if (a != null) { a.foo() } -> a?.foo() thenCodeLines?.singleOrNull()?.startsWith("$variableName.") ?: false -> "$variableName?${thenCodeLines?.firstOrNull()!!.removePrefix(variableName)}" // if (a != null) { break } -> a?.let { ... } // if (a != null) { foo() } -> a?.let { ... } else -> getDefaultCaseThenCodeLines(variableName, thenCodeLines) } private fun getDefaultCaseThenCodeLines(variableName: String, thenCodeLines: List?): String = "$variableName?.let {${thenCodeLines?.joinToString(prefix = "\n", postfix = "\n", separator = "\n")}}" private fun getDefaultCaseElseCodeLines(elseCodeLines: List): String = "?: run {${elseCodeLines.joinToString(prefix = "\n", postfix = "\n", separator = "\n")}}" @Suppress("COMMENT_WHITE_SPACE", "UnsafeCallOnNullableType") private fun nullCheckInOtherStatements(binaryExprNode: ASTNode) { val condition = (binaryExprNode.psi as KtBinaryExpression) if (isNullCheckBinaryExpression(condition)) { // require(a != null) is incorrect, Kotlin has special method: `requireNotNull` - need to use it instead // hierarchy is the following: // require(a != null) // / \ // REFERENCE_EXPRESSION VALUE_ARGUMENT_LIST // | | // IDENTIFIER(require) VALUE_ARGUMENT val parent = binaryExprNode.treeParent if (parent != null && parent.elementType == VALUE_ARGUMENT) { val grandParent = parent.treeParent if (grandParent != null && grandParent.elementType == VALUE_ARGUMENT_LIST && isRequireFun(grandParent.treePrev)) { @Suppress("COLLAPSE_IF_STATEMENTS") if (listOf(KtTokens.EXCLEQ, KtTokens.EXCLEQEQEQ).contains(condition.operationToken)) { warnAndFixOnNullCheck( condition, true, "use 'requireNotNull' instead of require(${condition.text})" ) { val variableName = (binaryExprNode.psi as KtBinaryExpression).left!!.text val newMethod = KotlinParser().createNode("requireNotNull($variableName)") grandParent.treeParent.treeParent.addChild(newMethod, grandParent.treeParent) grandParent.treeParent.treeParent.removeChild(grandParent.treeParent) } } } } } } private fun ASTNode.getBreakNodeFromIf(type: IElementType) = this .treeParent .findChildByType(type) ?.let { it.findChildByType(BLOCK) ?: it } ?.findAllNodesWithCondition { it.elementType == BREAK } ?.isNotEmpty() ?: false private fun ASTNode.extractLinesFromBlock(type: IElementType): List? = treeParent .getFirstChildWithType(type) ?.text ?.trim('{', '}') ?.split("\n") ?.filter { it.isNotBlank() } ?.map { it.trim() } ?.toList() @Suppress("UnsafeCallOnNullableType") private fun isNullCheckBinaryExpression(condition: KtBinaryExpression): Boolean = // check that binary expression has `null` as right or left operand setOf(condition.right, condition.left).map { it!!.node.elementType }.contains(NULL) && // checks that it is the comparison condition setOf(KtTokens.EQEQ, KtTokens.EQEQEQ, KtTokens.EXCLEQ, KtTokens.EXCLEQEQEQ) .contains(condition.operationToken) && // no need to raise warning or fix null checks in complex expressions !condition.isComplexCondition() && !condition.isInLambda() /** * checks if condition is a complex expression. For example: * (a == 5) - is not a complex condition, but (a == 5 && b != 6) is a complex condition */ private fun KtBinaryExpression.isComplexCondition(): Boolean { // KtBinaryExpression is complex if it has a parent that is also a binary expression return this.parent is KtBinaryExpression } /** * Expression could be used in lambda: * if (a.any { it == null }) */ private fun KtBinaryExpression.isInLambda(): Boolean { // KtBinaryExpression is in lambda if it has a parent that is a block expression return this.parent is KtBlockExpression } private fun warnAndFixOnNullCheck( condition: KtBinaryExpression, canBeAutoFixed: Boolean, freeText: String, autofix: () -> Unit ) { AVOID_NULL_CHECKS.warnOnlyOrWarnAndFix( configRules, emitWarn, freeText, condition.node.startOffset, condition.node, canBeAutoFixed, isFixMode, ) { autofix() } } private fun isRequireFun(referenceExpression: ASTNode) = referenceExpression.elementType == REFERENCE_EXPRESSION && referenceExpression.firstChildNode.text == "require" companion object { const val NAME_ID = "null-checks" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter4/SmartCastRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter4 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.SMART_CAST_NEEDED import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.KotlinParser import com.saveourtool.diktat.ruleset.utils.findAllDescendantsWithSpecificType import com.saveourtool.diktat.ruleset.utils.findParentNodeWithSpecificType import com.saveourtool.diktat.ruleset.utils.getFirstChildWithType import com.saveourtool.diktat.ruleset.utils.hasParent import com.saveourtool.diktat.ruleset.utils.search.findAllVariablesWithUsages import org.jetbrains.kotlin.KtNodeTypes.BINARY_WITH_TYPE import org.jetbrains.kotlin.KtNodeTypes.BLOCK import org.jetbrains.kotlin.KtNodeTypes.DOT_QUALIFIED_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.ELSE import org.jetbrains.kotlin.KtNodeTypes.FLOAT_CONSTANT import org.jetbrains.kotlin.KtNodeTypes.IF import org.jetbrains.kotlin.KtNodeTypes.INTEGER_CONSTANT import org.jetbrains.kotlin.KtNodeTypes.IS_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.REFERENCE_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.STRING_TEMPLATE import org.jetbrains.kotlin.KtNodeTypes.THEN import org.jetbrains.kotlin.KtNodeTypes.WHEN import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.psi.KtBlockExpression import org.jetbrains.kotlin.psi.KtNameReferenceExpression import org.jetbrains.kotlin.psi.KtProperty import org.jetbrains.kotlin.psi.psiUtil.isAncestor import org.jetbrains.kotlin.psi.psiUtil.parents import org.jetbrains.kotlin.psi.stubs.elements.KtFileElementType /** * Rule that detects redundant explicit casts */ class SmartCastRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(SMART_CAST_NEEDED) ) { override fun logic(node: ASTNode) { if (node.elementType == KtFileElementType.INSTANCE) { val usages = collectLocalPropertiesWithUsages(node) val properMap = collectReferenceList(usages) handleProp(properMap) } if (node.elementType == WHEN) { // Rule is simplified after https://github.com/saveourtool/diktat/issues/1168 return } } // Divide in is and as expr @Suppress("TYPE_ALIAS") private fun handleProp(propMap: Map>) { propMap.forEach { (property, references) -> val isExpr: MutableList = mutableListOf() val asExpr: MutableList = mutableListOf() references.forEach { if (it.node.hasParent(IS_EXPRESSION)) { isExpr.add(it) } else if (it.node.hasParent(BINARY_WITH_TYPE) && it.node.treeParent.text.contains(KtTokens.AS_KEYWORD.value)) { asExpr.add(it) } } val groups = groupIsAndAsExpr(isExpr, asExpr, property) if (groups.isNotEmpty()) { handleGroups(groups) } } } /** * If condition == is then we are looking for then block * If condition == !is then we are looking for else block */ @Suppress("NestedBlockDepth", "TYPE_ALIAS") private fun handleGroups(groups: Map>) { groups.keys.forEach { key -> val parentText = key.node.treeParent.text if (parentText.contains(" is ")) { groups.getValue(key).forEach { asCall -> if (asCall.node.hasParent(THEN)) { raiseWarning(asCall.node) } } } else if (parentText.contains(" !is ")) { groups.getValue(key).forEach { asCall -> if (asCall.node.hasParent(ELSE)) { raiseWarning(asCall.node) } } } } } @Suppress("UnsafeCallOnNullableType") private fun raiseWarning(asCall: ASTNode) { SMART_CAST_NEEDED.warnAndFix(configRules, emitWarn, isFixMode, asCall.treeParent.text, asCall.startOffset, asCall) { val dotExpr = asCall.findParentNodeWithSpecificType(DOT_QUALIFIED_EXPRESSION) val afterDotPart = dotExpr?.text?.split(".")?.get(1) val text = afterDotPart?.let { "${asCall.text}.$afterDotPart" } ?: asCall.text (dotExpr ?: asCall.treeParent).treeParent.addChild(KotlinParser().createNode(text), (dotExpr ?: asCall.treeParent)) (dotExpr ?: asCall.treeParent).treeParent.removeChild((dotExpr ?: asCall.treeParent)) } } /** * Groups is and as expressions, so that they are used in same if block */ @Suppress("TYPE_ALIAS") private fun groupIsAndAsExpr(isExpr: List, asExpr: List, prop: KtProperty ): Map> { if (isExpr.isEmpty() && asExpr.isNotEmpty()) { handleZeroIsCase(asExpr, prop) return emptyMap() } val groupedExprs: MutableMap> = mutableMapOf() isExpr.forEach { ref -> val list: MutableList = mutableListOf() asExpr.forEach { asCall -> if (asCall.node.findParentNodeWithSpecificType(IF) == ref.node.findParentNodeWithSpecificType(IF)) { list.add(asCall) } } groupedExprs[ref] = list } return groupedExprs } @Suppress("UnsafeCallOnNullableType") private fun getPropertyType(prop: KtProperty): String? { when (prop.initializer?.node?.elementType) { STRING_TEMPLATE -> return "String" INTEGER_CONSTANT -> return "Int" else -> { } } if (prop.initializer?.node?.elementType == FLOAT_CONSTANT) { if (prop.initializer?.text!!.endsWith("f")) { return "Float" } return "Double" } return null } @Suppress("UnsafeCallOnNullableType") private fun handleZeroIsCase(asExpr: List, prop: KtProperty) { val propType = getPropertyType(prop) ?: return asExpr .map { it.node } .forEach { if (it.treeParent.text.endsWith(propType)) { raiseWarning(it) } } } private fun handleThenBlock(then: ASTNode, blocks: List) { val thenBlock = then.findChildByType(BLOCK) thenBlock?.let { // Find all as expressions that are inside this current block val asList = thenBlock .findAllDescendantsWithSpecificType(BINARY_WITH_TYPE) .filter { it.text.contains(" as ") && it.findParentNodeWithSpecificType(BLOCK) == thenBlock } .filterNot { (it.getFirstChildWithType(REFERENCE_EXPRESSION)?.psi as KtNameReferenceExpression).getLocalDeclaration() != null } checkAsExpressions(asList, blocks) } ?: run { val asList = then.findAllDescendantsWithSpecificType(BINARY_WITH_TYPE).filter { it.text.contains(KtTokens.AS_KEYWORD.value) } checkAsExpressions(asList, blocks) } } @Suppress("UnsafeCallOnNullableType") private fun checkAsExpressions(asList: List, blocks: List) { val asExpr: MutableList = mutableListOf() @Suppress("PARAMETER_NAME_IN_OUTER_LAMBDA") asList.forEach { val split = it.text.split("as").map { part -> part.trim() } asExpr.add(AsExpressions(split[0], split[1], it)) } val exprToChange = asExpr.filter { asCall -> blocks.any { isExpr -> isExpr.identifier == asCall.identifier && isExpr.type == asCall.type } } if (exprToChange.isNotEmpty()) { exprToChange.forEach { asCall -> SMART_CAST_NEEDED.warnAndFix(configRules, emitWarn, isFixMode, "${asCall.identifier} as ${asCall.type}", asCall.node.startOffset, asCall.node) { val dotExpr = asCall.node.findParentNodeWithSpecificType(DOT_QUALIFIED_EXPRESSION)!! val afterDotPart = dotExpr.text.split(".")[1] val text = "${asCall.identifier}.$afterDotPart" dotExpr.treeParent.addChild(KotlinParser().createNode(text), dotExpr) dotExpr.treeParent.removeChild(dotExpr) } } } } private fun KtNameReferenceExpression.getLocalDeclaration(): KtProperty? = parents .mapNotNull { it as? KtBlockExpression } .first() .let { blockExpression -> blockExpression .statements .takeWhile { !it.isAncestor(this, true) } .mapNotNull { it as? KtProperty } .find { it.isLocal && it.hasInitializer() && it.name?.equals(getReferencedName()) ?: false } } private fun collectLocalPropertiesWithUsages(node: ASTNode) = node .findAllVariablesWithUsages { propertyNode -> propertyNode.name != null } /** * Gets references, which contains is or as keywords * * @return Map of property and list of expressions */ @Suppress("TYPE_ALIAS") private fun collectReferenceList(propertiesToUsages: Map>): Map> = propertiesToUsages.mapValues { (_, value) -> value.filter { entry -> entry.parent.node.elementType == IS_EXPRESSION || entry.parent.node.elementType == BINARY_WITH_TYPE } } /** * @property identifier a reference that is cast * @property type a type to which the reference is being cast * @property node a node that holds the entire expression */ data class AsExpressions( val identifier: String, val type: String, val node: ASTNode ) /** * @property identifier a reference that is checked * @property type a type with which the reference is being compared */ data class IsExpressions(val identifier: String, val type: String) companion object { const val NAME_ID = "smart-cast-rule" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter4/TypeAliasRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter4 import com.saveourtool.diktat.common.config.rules.RuleConfiguration import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.common.config.rules.getRuleConfig import com.saveourtool.diktat.ruleset.constants.Warnings.TYPE_ALIAS import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.* import org.jetbrains.kotlin.KtNodeTypes.SUPER_TYPE_LIST import org.jetbrains.kotlin.KtNodeTypes.TYPEALIAS import org.jetbrains.kotlin.KtNodeTypes.TYPE_REFERENCE import org.jetbrains.kotlin.KtNodeTypes.VALUE_PARAMETER import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.lexer.KtTokens.LT import org.jetbrains.kotlin.psi.psiUtil.parents /** * This rule checks if variable has long type reference and two or more nested generics. * Length type reference can be configured */ class TypeAliasRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(TYPE_ALIAS) ) { override fun logic(node: ASTNode) { if (node.elementType == TYPE_REFERENCE && node .parents() .map { it.elementType } .none { it == SUPER_TYPE_LIST || it == TYPEALIAS }) { checkTypeReference(node, TypeAliasConfiguration(configRules.getRuleConfig(TYPE_ALIAS)?.configuration ?: emptyMap())) } } /** * Check properties for nested generics. Count LT for generic types and VALUE_PARAMETER for functional types */ private fun checkTypeReference(node: ASTNode, config: TypeAliasConfiguration) { if (node.textLength > config.typeReferenceLength) { @Suppress("COLLAPSE_IF_STATEMENTS") if (node.findAllDescendantsWithSpecificType(LT).size > 1 || node.findAllDescendantsWithSpecificType(VALUE_PARAMETER).size > 1) { TYPE_ALIAS.warn(configRules, emitWarn, "too long type reference", node.startOffset, node) } } } /** * [RuleConfiguration] about using type aliases instead of complex types */ class TypeAliasConfiguration(config: Map) : RuleConfiguration(config) { /** * Maximum length of a type before suggesting to use typealias */ val typeReferenceLength = config["typeReferenceLength"]?.toIntOrNull() ?: TYPE_REFERENCE_MAX_LENGTH } companion object { const val NAME_ID = "type-alias" const val TYPE_REFERENCE_MAX_LENGTH = 25 } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter4/VariableGenericTypeDeclarationRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter4 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.GENERIC_VARIABLE_WRONG_DECLARATION import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.getAllChildrenWithType import org.jetbrains.kotlin.KtNodeTypes.CALL_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.DOT_QUALIFIED_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.PROPERTY import org.jetbrains.kotlin.KtNodeTypes.TYPE_ARGUMENT_LIST import org.jetbrains.kotlin.KtNodeTypes.VALUE_PARAMETER import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.psi.KtCallExpression import org.jetbrains.kotlin.psi.KtParameter import org.jetbrains.kotlin.psi.KtProperty /** * This Rule checks if declaration of a generic variable is appropriate. * Not recommended: val myVariable: Map = emptyMap() or val myVariable = emptyMap() * Recommended: val myVariable: Map = emptyMap() */ @Suppress("ForbiddenComment") // FIXME: we now don't have access to return types, so we can perform this check only if explicit type is present, but should be able also if it's not. class VariableGenericTypeDeclarationRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(GENERIC_VARIABLE_WRONG_DECLARATION) ) { override fun logic(node: ASTNode) { when (node.elementType) { PROPERTY, VALUE_PARAMETER -> handleProperty(node) else -> { } } } @Suppress("UnsafeCallOnNullableType", "AVOID_NULL_CHECKS") private fun handleProperty(node: ASTNode) { val callExpr = node.findChildByType(CALL_EXPRESSION) ?: node .findChildByType(DOT_QUALIFIED_EXPRESSION) ?.getAllChildrenWithType(CALL_EXPRESSION) ?.lastOrNull() val rightSide = (callExpr?.psi as? KtCallExpression)?.typeArgumentList?.arguments val leftSide = if (node.elementType == PROPERTY) { (node.psi as? KtProperty) ?.typeReference ?.typeElement ?.typeArgumentsAsTypes } else { (node.psi as? KtParameter) ?.typeReference ?.typeElement ?.typeArgumentsAsTypes } // Allow cases with wild card types; `*` interprets as `null` in list of types if (leftSide?.any { it == null } == true) { return } if (rightSide != null && leftSide != null && rightSide.size == leftSide.size && rightSide.zip(leftSide).all { (first, second) -> first.text == second.text }) { GENERIC_VARIABLE_WRONG_DECLARATION.warnAndFix(configRules, emitWarn, isFixMode, "type arguments are unnecessary in ${callExpr.text}", node.startOffset, node) { callExpr.removeChild(callExpr.findChildByType(TYPE_ARGUMENT_LIST)!!) } } if (leftSide == null && rightSide != null) { GENERIC_VARIABLE_WRONG_DECLARATION.warn(configRules, emitWarn, node.text, node.startOffset, node) } } companion object { const val NAME_ID = "variable-generic-type" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter4/calculations/AccurateCalculationsRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter4.calculations import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.FLOAT_IN_ACCURATE_CALCULATIONS import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.findLocalDeclaration import com.saveourtool.diktat.ruleset.utils.getFunctionName import org.jetbrains.kotlin.KtNodeTypes import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.PsiElement import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.psi.KtBinaryExpression import org.jetbrains.kotlin.psi.KtCallExpression import org.jetbrains.kotlin.psi.KtDotQualifiedExpression import org.jetbrains.kotlin.psi.KtExpression import org.jetbrains.kotlin.psi.KtNameReferenceExpression import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf import org.jetbrains.kotlin.psi.psiUtil.startOffset /** * Rule that checks that floating-point numbers are not used for accurate calculations * 1. Checks that floating-point numbers are not used in arithmetic binary expressions * Exception: allows arithmetic operations only when absolute value of result is immediately used in comparison * Fixme: detect variables by type, not only floating-point literals */ class AccurateCalculationsRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(FLOAT_IN_ACCURATE_CALCULATIONS) ) { private fun KtCallExpression?.isAbsOfFloat() = this ?.run { (calleeExpression as? KtNameReferenceExpression) ?.getReferencedName() ?.equals("abs") ?.and( valueArguments .singleOrNull() ?.getArgumentExpression() ?.isFloatingPoint() ?: false) ?: false } ?: false @Suppress("PARAMETER_NAME_IN_OUTER_LAMBDA") private fun KtDotQualifiedExpression.isComparisonWithAbs() = takeIf { it.selectorExpression.run { this is KtCallExpression && getFunctionName() in comparisonFunctions } } ?.run { (selectorExpression as KtCallExpression) .valueArguments .singleOrNull() ?.let { it.getArgumentExpression() as? KtCallExpression } ?.isAbsOfFloat() ?: false || (receiverExpression as? KtCallExpression).isAbsOfFloat() } ?: false private fun KtBinaryExpression.isComparisonWithAbs() = takeIf { it.operationToken in comparisonOperators } ?.run { left as? KtCallExpression ?: right as? KtCallExpression } ?.run { calleeExpression as? KtNameReferenceExpression } ?.getReferencedName() ?.equals("abs") ?: false private fun isComparisonWithAbs(psiElement: PsiElement) = when (psiElement) { is KtBinaryExpression -> psiElement.isComparisonWithAbs() is KtDotQualifiedExpression -> psiElement.isComparisonWithAbs() else -> false } private fun checkFloatValue(floatValue: PsiElement?, expression: KtExpression) { floatValue?.let { // float value is used in comparison FLOAT_IN_ACCURATE_CALCULATIONS.warn(configRules, emitWarn, "float value of <${it.text}> used in arithmetic expression in ${expression.text}", expression.startOffset, expression.node) } } private fun handleFunction(expression: KtDotQualifiedExpression) = expression .takeIf { it.selectorExpression is KtCallExpression } ?.run { receiverExpression to selectorExpression as KtCallExpression } ?.takeIf { it.second.getFunctionName() in arithmeticOperationsFunctions } ?.takeUnless { expression.parentsWithSelf.any(::isComparisonWithAbs) } ?.let { (receiverExpression, selectorExpression) -> val floatValue = receiverExpression.takeIf { it.isFloatingPoint() } ?: selectorExpression .valueArguments .find { it.getArgumentExpression()?.isFloatingPoint() ?: false } checkFloatValue(floatValue, expression) } @Suppress("UnsafeCallOnNullableType") private fun handleBinaryExpression(expression: KtBinaryExpression) = expression .takeIf { it.operationToken in arithmeticOperationTokens } ?.takeUnless { it.parentsWithSelf.any(::isComparisonWithAbs) } ?.run { // !! is safe because `KtBinaryExpression#left` is annotated `Nullable IfNotParsed` val floatValue = left!!.takeIf { it.isFloatingPoint() } ?: right!!.takeIf { it.isFloatingPoint() } checkFloatValue(floatValue, this) } /** * @param node */ override fun logic(node: ASTNode) { when (val psi = node.psi) { is KtBinaryExpression -> handleBinaryExpression(psi) is KtDotQualifiedExpression -> handleFunction(psi) else -> return } } companion object { const val NAME_ID = "accurate-calculations" private val arithmeticOperationTokens = listOf(KtTokens.PLUS, KtTokens.PLUSEQ, KtTokens.PLUSPLUS, KtTokens.MINUS, KtTokens.MINUSEQ, KtTokens.MINUSMINUS, KtTokens.MUL, KtTokens.MULTEQ, KtTokens.DIV, KtTokens.DIVEQ, KtTokens.PERC, KtTokens.PERCEQ, KtTokens.GT, KtTokens.LT, KtTokens.LTEQ, KtTokens.GTEQ, KtTokens.EQEQ ) private val comparisonOperators = listOf(KtTokens.LT, KtTokens.LTEQ, KtTokens.GT, KtTokens.GTEQ) private val arithmeticOperationsFunctions = listOf("equals", "compareTo") private val comparisonFunctions = listOf("compareTo") } } @Suppress("UnsafeCallOnNullableType") private fun PsiElement.isFloatingPoint(): Boolean = node.elementType == KtTokens.FLOAT_LITERAL || node.elementType == KtNodeTypes.FLOAT_CONSTANT || ((this as? KtNameReferenceExpression) ?.findLocalDeclaration() ?.initializer ?.node ?.run { elementType == KtTokens.FLOAT_LITERAL || elementType == KtNodeTypes.FLOAT_CONSTANT } ?: false) || ((this as? KtBinaryExpression) ?.run { left!!.isFloatingPoint() && right!!.isFloatingPoint() } ?: false) ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter5/AsyncAndSyncRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter5 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.RUN_BLOCKING_INSIDE_ASYNC import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.hasChildOfType import com.saveourtool.diktat.ruleset.utils.parent import org.jetbrains.kotlin.KtNodeTypes.CALL_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.FUN import org.jetbrains.kotlin.KtNodeTypes.LAMBDA_ARGUMENT import org.jetbrains.kotlin.KtNodeTypes.REFERENCE_EXPRESSION import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.psi.KtFunction import org.jetbrains.kotlin.psi.psiUtil.hasSuspendModifier /** * This rule finds if using runBlocking in asynchronous code */ class AsyncAndSyncRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(RUN_BLOCKING_INSIDE_ASYNC) ) { private val asyncList = listOf("async", "launch") override fun logic(node: ASTNode) { if (node.isRunBlocking()) { checkRunBlocking(node) } } private fun checkRunBlocking(node: ASTNode) { node.parent { it.isAsync() || it.isSuspend() }?.let { RUN_BLOCKING_INSIDE_ASYNC.warn(configRules, emitWarn, node.text, node.startOffset, node) } } private fun ASTNode.isAsync() = this.elementType == CALL_EXPRESSION && this.findChildByType(REFERENCE_EXPRESSION)?.text in asyncList private fun ASTNode.isSuspend() = this.elementType == FUN && (this.psi as KtFunction).modifierList?.hasSuspendModifier() ?: false private fun ASTNode.isRunBlocking() = this.elementType == REFERENCE_EXPRESSION && this.text == "runBlocking" && this.treeParent.hasChildOfType(LAMBDA_ARGUMENT) companion object { const val NAME_ID = "sync-in-async" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter5/AvoidNestedFunctionsRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter5 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.AVOID_NESTED_FUNCTIONS import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.findAllDescendantsWithSpecificType import com.saveourtool.diktat.ruleset.utils.getFirstChildWithType import com.saveourtool.diktat.ruleset.utils.hasChildOfType import com.saveourtool.diktat.ruleset.utils.hasParent import com.saveourtool.diktat.ruleset.utils.isAnonymousFunction import com.saveourtool.diktat.ruleset.utils.isWhiteSpaceWithNewline import org.jetbrains.kotlin.KtNodeTypes.CLASS_BODY import org.jetbrains.kotlin.KtNodeTypes.FUN import org.jetbrains.kotlin.KtNodeTypes.MODIFIER_LIST import org.jetbrains.kotlin.KtNodeTypes.PROPERTY import org.jetbrains.kotlin.KtNodeTypes.REFERENCE_EXPRESSION import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType import org.jetbrains.kotlin.lexer.KtTokens.IDENTIFIER import org.jetbrains.kotlin.psi.KtFunction import org.jetbrains.kotlin.psi.psiUtil.parents /** * This rule checks for nested functions and warns if it finds any. */ class AvoidNestedFunctionsRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(AVOID_NESTED_FUNCTIONS) ) { override fun logic(node: ASTNode) { if (node.elementType == FUN) { handleNestedFunctions(node) } } // FixMe: need to detect all properties, which local function is using and add them to params of this function @Suppress("UnsafeCallOnNullableType") private fun handleNestedFunctions(node: ASTNode) { if (isNestedFunction(node)) { val funcName = node.getFirstChildWithType(IDENTIFIER)!!.text AVOID_NESTED_FUNCTIONS.warnOnlyOrWarnAndFix(configRules, emitWarn, "fun $funcName", node.startOffset, node, shouldBeAutoCorrected = checkFunctionReferences(node), isFixMode) { // We take last nested function, then add and remove child from bottom to top val lastFunc = node.findAllDescendantsWithSpecificType(FUN).last() val funcSeq = lastFunc .parents() .filter { it.elementType == FUN } .toMutableList() funcSeq.add(0, lastFunc) val firstFunc = funcSeq.last() funcSeq.dropLast(1).forEachIndexed { index, it -> val parent = funcSeq[index + 1] if (it.treePrev.isWhiteSpaceWithNewline()) { parent.removeChild(it.treePrev) } firstFunc.treeParent.addChild(it.clone() as ASTNode, firstFunc) firstFunc.treeParent.addChild(PsiWhiteSpaceImpl("\n"), firstFunc) parent.removeChild(it) } } } } private fun isNestedFunction(node: ASTNode): Boolean = node.hasParent(FUN) && node.hasFunParentUntil(CLASS_BODY) && !node.hasChildOfType(MODIFIER_LIST) && !node.isAnonymousFunction() private fun ASTNode.hasFunParentUntil(stopNode: IElementType): Boolean = parents() .asSequence() .takeWhile { it.elementType != stopNode } .any { it.elementType == FUN } /** * Checks if local function has no usage of outside properties */ @Suppress("UnsafeCallOnNullableType", "FUNCTION_BOOLEAN_PREFIX") private fun checkFunctionReferences(func: ASTNode): Boolean { val localProperties: MutableList = mutableListOf() localProperties.addAll(func.findAllDescendantsWithSpecificType(PROPERTY)) val propertiesNames: List = mutableListOf().apply { addAll(localProperties.map { it.getFirstChildWithType(IDENTIFIER)!!.text }) addAll(getParameterNames(func)) } .toList() return func.findAllDescendantsWithSpecificType(REFERENCE_EXPRESSION).all { propertiesNames.contains(it.text) } } /** * Collects all function parameters' names * * @return List of names */ @Suppress("UnsafeCallOnNullableType") private fun getParameterNames(node: ASTNode): List = (node.psi as KtFunction).valueParameters.map { it.name!! } companion object { const val NAME_ID = "avoid-nested-functions" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter5/CheckInverseMethodRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter5 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.INVERSE_FUNCTION_PREFERRED import com.saveourtool.diktat.ruleset.rules.DiktatRule import org.jetbrains.kotlin.KtNodeTypes.CALL_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.OPERATION_REFERENCE import org.jetbrains.kotlin.KtNodeTypes.REFERENCE_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.VALUE_ARGUMENT_LIST import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.CompositeElement import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement import org.jetbrains.kotlin.lexer.KtTokens.BLOCK_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.IDENTIFIER import org.jetbrains.kotlin.lexer.KtTokens.LPAR import org.jetbrains.kotlin.lexer.KtTokens.RPAR import org.jetbrains.kotlin.lexer.KtTokens.WHITE_SPACE import org.jetbrains.kotlin.psi.psiUtil.siblings /** * This rule checks if inverse method can be used. * For example if there is !isEmpty() on collection call that it changes it to isNotEmpty() */ class CheckInverseMethodRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(INVERSE_FUNCTION_PREFERRED) ) { override fun logic(node: ASTNode) { if (node.elementType == CALL_EXPRESSION && node.text in methodMap.keys) { checkCallExpressionName(node) } } private fun checkCallExpressionName(node: ASTNode) { val operationRef = node .treeParent .siblings(forward = false) .takeWhile { it.elementType in intermediateTokens } .firstOrNull { it.elementType == OPERATION_REFERENCE } if (operationRef?.text == "!") { INVERSE_FUNCTION_PREFERRED.warnAndFix(configRules, emitWarn, isFixMode, "${methodMap[node.text]} instead of !${node.text}", node.startOffset, node) { val callExpression = CompositeElement(CALL_EXPRESSION) val referenceExp = CompositeElement(REFERENCE_EXPRESSION) val argList = CompositeElement(VALUE_ARGUMENT_LIST) node.treeParent.addChild(callExpression, node) callExpression.addChild(referenceExp) callExpression.addChild(argList) referenceExp.addChild(LeafPsiElement(IDENTIFIER, "${methodMap[node.text]}".dropLast(2))) argList.addChild(LeafPsiElement(LPAR, "(")) argList.addChild(LeafPsiElement(RPAR, ")")) node.treeParent.treeParent.removeChild(node.treeParent.treePrev) // removing OPERATION_EXPRESSION - ! node.treeParent.removeChild(node) } } } companion object { const val NAME_ID = "inverse-method" val methodMap = mapOf( "isEmpty()" to "isNotEmpty()", "isBlank()" to "isNotBlank()", "isNotEmpty()" to "isEmpty()", "isNotBlank()" to "isBlank()" ) val intermediateTokens = listOf(WHITE_SPACE, OPERATION_REFERENCE, BLOCK_COMMENT) } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter5/CustomLabel.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter5 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.CUSTOM_LABEL import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.loopType import org.jetbrains.kotlin.KtNodeTypes.BREAK import org.jetbrains.kotlin.KtNodeTypes.CALL_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.CONTINUE import org.jetbrains.kotlin.KtNodeTypes.LABEL_QUALIFIER import org.jetbrains.kotlin.KtNodeTypes.REFERENCE_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.RETURN import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.psi.psiUtil.parents /** * Rule that checks using custom label */ class CustomLabel(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(CUSTOM_LABEL) ) { private val forEachReference = listOf("forEach", "forEachIndexed") private val labels = listOf("@loop", "@forEach", "@forEachIndexed") private val stopWords = listOf(RETURN, BREAK, CONTINUE) override fun logic(node: ASTNode) { if (node.elementType == LABEL_QUALIFIER && node.text !in labels && node.treeParent.elementType in stopWords) { val nestedCount = node.parents().count { it.elementType in loopType || (it.elementType == CALL_EXPRESSION && it.findChildByType(REFERENCE_EXPRESSION)?.text in forEachReference) } if (nestedCount == 1) { CUSTOM_LABEL.warn(configRules, emitWarn, node.text, node.startOffset, node) } } } companion object { const val NAME_ID = "custom-label" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter5/FunctionArgumentsSize.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter5 import com.saveourtool.diktat.common.config.rules.RuleConfiguration import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.common.config.rules.getRuleConfig import com.saveourtool.diktat.ruleset.constants.Warnings.TOO_MANY_PARAMETERS import com.saveourtool.diktat.ruleset.rules.DiktatRule import org.jetbrains.kotlin.KtNodeTypes import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.lexer.KtTokens.IDENTIFIER import org.jetbrains.kotlin.psi.KtFunction /** * Rule that checks that function doesn't contains too many parameters */ class FunctionArgumentsSize(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(TOO_MANY_PARAMETERS) ) { private val configuration: FunctionArgumentsSizeConfiguration by lazy { FunctionArgumentsSizeConfiguration(configRules.getRuleConfig(TOO_MANY_PARAMETERS)?.configuration ?: emptyMap()) } override fun logic(node: ASTNode) { if (node.elementType == KtNodeTypes.FUN) { checkFun(node, configuration.maxParameterSize) } } @Suppress("UnsafeCallOnNullableType") private fun checkFun(node: ASTNode, maxParameterSize: Long) { val parameterListSize = (node.psi as KtFunction).valueParameters.size if (parameterListSize > maxParameterSize) { TOO_MANY_PARAMETERS.warn(configRules, emitWarn, "${node.findChildByType(IDENTIFIER)!!.text} has $parameterListSize, but allowed $maxParameterSize", node.startOffset, node) } } /** * [RuleConfiguration] for maximum number of parameters */ class FunctionArgumentsSizeConfiguration(config: Map) : RuleConfiguration(config) { /** * Maximum allowed number of function parameters */ val maxParameterSize = config["maxParameterListSize"]?.toLongOrNull() ?: MAX_DEFAULT_PARAMETER_SIZE } companion object { const val MAX_DEFAULT_PARAMETER_SIZE = 5L const val NAME_ID = "argument-size" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter5/FunctionLength.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter5 import com.saveourtool.diktat.common.config.rules.RuleConfiguration import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.common.config.rules.getRuleConfig import com.saveourtool.diktat.ruleset.constants.Warnings.TOO_LONG_FUNCTION import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.* import org.jetbrains.kotlin.KtNodeTypes.FUN import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.psi.KtFunction /** * Rule 5.1.1 check function length */ class FunctionLength(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(TOO_LONG_FUNCTION) ) { override fun logic(node: ASTNode) { val configuration = FunctionLengthConfiguration( configRules.getRuleConfig(TOO_LONG_FUNCTION)?.configuration ?: emptyMap() ) if (node.elementType == FUN) { checkFun(node, configuration) } } private fun checkFun(node: ASTNode, configuration: FunctionLengthConfiguration) { val copyNode = if (configuration.isIncludeHeader) { node.clone() as ASTNode } else { ((node.psi as KtFunction) .bodyExpression ?.node ?.clone() ?: return) as ASTNode } val sizeFun = countCodeLines(copyNode) if (sizeFun > configuration.maxFunctionLength) { TOO_LONG_FUNCTION.warn(configRules, emitWarn, "max length is ${configuration.maxFunctionLength}, but you have $sizeFun", node.startOffset, node) } } /** * [RuleConfiguration] for function length */ class FunctionLengthConfiguration(config: Map) : RuleConfiguration(config) { /** * Maximum allowed function length */ val maxFunctionLength = config["maxFunctionLength"]?.toLong() ?: MAX_FUNCTION_LENGTH /** * Whether function header (start of a declaration with parameter list and return type) is counted too */ val isIncludeHeader = config["isIncludeHeader"]?.toBoolean() ?: true } companion object { private const val MAX_FUNCTION_LENGTH = 30L const val NAME_ID = "function-length" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter5/LambdaLengthRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter5 import com.saveourtool.diktat.common.config.rules.RuleConfiguration import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.common.config.rules.getRuleConfig import com.saveourtool.diktat.ruleset.constants.Warnings.TOO_MANY_LINES_IN_LAMBDA import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.* import org.jetbrains.kotlin.KtNodeTypes import org.jetbrains.kotlin.com.intellij.lang.ASTNode /** * Rule 5.2.5 check lambda length without parameters */ class LambdaLengthRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(TOO_MANY_LINES_IN_LAMBDA) ) { private val configuration by lazy { LambdaLengthConfiguration( this.configRules.getRuleConfig(TOO_MANY_LINES_IN_LAMBDA)?.configuration ?: emptyMap() ) } override fun logic(node: ASTNode) { if (node.elementType == KtNodeTypes.LAMBDA_EXPRESSION) { checkLambda(node, configuration) } } private fun checkLambda(node: ASTNode, configuration: LambdaLengthConfiguration) { val sizeLambda = countCodeLines(node) if (sizeLambda > configuration.maxLambdaLength && doesLambdaContainIt(node)) { TOO_MANY_LINES_IN_LAMBDA.warn(configRules, emitWarn, "max length lambda without arguments is ${configuration.maxLambdaLength}, but you have $sizeLambda", node.startOffset, node) } } /** * [RuleConfiguration] for lambda length */ class LambdaLengthConfiguration(config: Map) : RuleConfiguration(config) { /** * Maximum allowed lambda length */ val maxLambdaLength = config["maxLambdaLength"]?.toLong() ?: MAX_LINES_IN_LAMBDA } companion object { private const val MAX_LINES_IN_LAMBDA = 10L const val NAME_ID = "lambda-length" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter5/LambdaParameterOrder.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter5 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.LAMBDA_IS_NOT_LAST_PARAMETER import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.hasChildOfType import org.jetbrains.kotlin.KtNodeTypes import org.jetbrains.kotlin.KtNodeTypes.FUNCTION_TYPE import org.jetbrains.kotlin.KtNodeTypes.NULLABLE_TYPE import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.lexer.KtTokens.IDENTIFIER import org.jetbrains.kotlin.psi.KtFunction import org.jetbrains.kotlin.utils.addToStdlib.ifNotEmpty /** * Rule that checks if parameter with function type is the last in parameter list */ class LambdaParameterOrder(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(LAMBDA_IS_NOT_LAST_PARAMETER) ) { override fun logic(node: ASTNode) { if (node.elementType == KtNodeTypes.FUN) { checkArguments(node) } } @Suppress("UnsafeCallOnNullableType") private fun checkArguments(node: ASTNode) { val funArguments = (node.psi as KtFunction).valueParameters val sortArguments = funArguments .sortedBy { valueParam -> valueParam .typeReference ?.node ?.let { it.findChildByType(NULLABLE_TYPE) ?: it } ?.hasChildOfType(FUNCTION_TYPE) } funArguments.filterIndexed { index, ktParameter -> ktParameter != sortArguments[index] }.ifNotEmpty { LAMBDA_IS_NOT_LAST_PARAMETER.warn(configRules, emitWarn, node.findChildByType(IDENTIFIER)!!.text, first().node.startOffset, node) } } companion object { const val NAME_ID = "lambda-parameter-order" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter5/NestedFunctionBlock.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter5 import com.saveourtool.diktat.common.config.rules.RuleConfiguration import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.common.config.rules.getRuleConfig import com.saveourtool.diktat.ruleset.constants.Warnings.NESTED_BLOCK import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.findAllDescendantsWithSpecificType import com.saveourtool.diktat.ruleset.utils.hasChildOfType import org.jetbrains.kotlin.KtNodeTypes.CLASS import org.jetbrains.kotlin.KtNodeTypes.FUN import org.jetbrains.kotlin.KtNodeTypes.FUNCTION_LITERAL import org.jetbrains.kotlin.KtNodeTypes.OBJECT_DECLARATION import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.lexer.KtTokens.IDENTIFIER import org.jetbrains.kotlin.lexer.KtTokens.LBRACE import org.jetbrains.kotlin.psi.psiUtil.parents /** * Rule 5.1.2 Nested blokcs */ class NestedFunctionBlock(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(NESTED_BLOCK) ) { private val configuration: NestedBlockConfiguration by lazy { NestedBlockConfiguration(configRules.getRuleConfig(NESTED_BLOCK)?.configuration ?: emptyMap()) } override fun logic(node: ASTNode) { if (node.elementType in nullificationType) { countNestedBlocks(node, configuration.maxNestedBlockQuantity) } } private fun countNestedBlocks(node: ASTNode, maxNestedBlockCount: Long) { node.findAllDescendantsWithSpecificType(LBRACE) .reversed() .forEach { lbraceNode -> val blockParent = lbraceNode .parents() .takeWhile { it != node } .takeIf { parentList -> parentList.map { it.elementType }.none { it in nullificationType } } ?.count { it.hasChildOfType(LBRACE) } ?: return if (blockParent > maxNestedBlockCount) { NESTED_BLOCK.warn(configRules, emitWarn, node.findChildByType(IDENTIFIER)?.text ?: node.text, node.startOffset, node) return } } } /** * [RuleConfiguration] for analyzing nested code blocks */ class NestedBlockConfiguration(config: Map) : RuleConfiguration(config) { /** * Maximum number of allowed levels of nested blocks */ val maxNestedBlockQuantity = config["maxNestedBlockQuantity"]?.toLong() ?: MAX_NESTED_BLOCK_COUNT } companion object { private const val MAX_NESTED_BLOCK_COUNT = 4L const val NAME_ID = "nested-block" /** * Nodes of these types reset counter of nested blocks */ private val nullificationType = listOf(CLASS, FUN, OBJECT_DECLARATION, FUNCTION_LITERAL) } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter5/OverloadingArgumentsFunction.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter5 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.WRONG_OVERLOADING_FUNCTION_ARGUMENTS import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.allSiblings import com.saveourtool.diktat.ruleset.utils.findChildAfter import com.saveourtool.diktat.ruleset.utils.findChildBefore import org.jetbrains.kotlin.KtNodeTypes.FUN import org.jetbrains.kotlin.KtNodeTypes.TYPE_REFERENCE import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.lexer.KtTokens.IDENTIFIER import org.jetbrains.kotlin.psi.KtFunction import org.jetbrains.kotlin.psi.KtParameter import org.jetbrains.kotlin.psi.psiUtil.startOffset /** * Rule that suggests to use functions with default parameters instead of multiple overloads */ class OverloadingArgumentsFunction(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(WRONG_OVERLOADING_FUNCTION_ARGUMENTS) ) { override fun logic(node: ASTNode) { if (node.elementType == FUN) { checkFun(node.psi as KtFunction) } } @Suppress("UnsafeCallOnNullableType") private fun checkFun(funPsi: KtFunction) { val allOverloadFunction = funPsi .node .allSiblings(withSelf = false) .asSequence() .filter { it.elementType == FUN } .map { it.psi as KtFunction } .filter { it.isOverloadedBy(funPsi) } .filter { it.hasSameModifiers(funPsi) } .filter { funPsi.node.findChildBefore(IDENTIFIER, TYPE_REFERENCE)?.text == it.node.findChildBefore(IDENTIFIER, TYPE_REFERENCE)?.text } .filter { funPsi.node.findChildAfter(IDENTIFIER, TYPE_REFERENCE)?.text == it.node.findChildAfter(IDENTIFIER, TYPE_REFERENCE)?.text } .toList() if (allOverloadFunction.isNotEmpty()) { WRONG_OVERLOADING_FUNCTION_ARGUMENTS.warn(configRules, emitWarn, funPsi.node.findChildByType(IDENTIFIER)!!.text, funPsi.startOffset, funPsi.node) } } /** * We can raise errors only on those methods that have same modifiers (inline/public/etc.) */ private fun KtFunction.hasSameModifiers(other: KtFunction) = this.getSortedModifiers() == other.getSortedModifiers() private fun KtFunction.getSortedModifiers() = this.modifierList ?.node ?.getChildren(KtTokens.MODIFIER_KEYWORDS) ?.map { it.text } ?.sortedBy { it } /** * we need to compare following things for two functions: * 1) that function arguments go in the same order in both method * 2) that arguments have SAME names (you can think that it is not necessary, * but usually if developer really wants to overload method - he will have same names of arguments) * 3) arguments have same types (obviously) * * So we need to find methods with following arguments: foo(a: Int, b: Int) and foo(a: Int). foo(b: Int) is NOT suitable */ private fun KtFunction.isOverloadedBy(other: KtFunction): Boolean { // no need to process methods with different names if (this.nameIdentifier?.text != other.nameIdentifier?.text) { return false } // if this function has more arguments, than other, then we will compare it on the next iteration cycle (at logic() level) // this hack will help us to point only to one function with smaller number of arguments if (this.valueParameters.size < other.valueParameters.size) { return false } for (i in 0 until other.valueParameters.size) { // all arguments on the same position should match by name and type if (this.valueParameters[i].getFunctionName() != other.valueParameters[i].getFunctionName() || this.valueParameters[i].getFunctionType() != other.valueParameters[i].getFunctionType() ) { return false } } return true } private fun KtParameter.getFunctionName() = this.nameIdentifier?.text private fun KtParameter.getFunctionType() = this.typeReference?.text companion object { const val NAME_ID = "overloading-default-values" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter5/ParameterNameInOuterLambdaRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter5 import com.saveourtool.diktat.common.config.rules.RuleConfiguration import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.common.config.rules.getRuleConfig import com.saveourtool.diktat.ruleset.constants.Warnings.PARAMETER_NAME_IN_OUTER_LAMBDA import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.doesLambdaContainIt import com.saveourtool.diktat.ruleset.utils.findAllDescendantsWithSpecificType import com.saveourtool.diktat.ruleset.utils.hasExplicitIt import com.saveourtool.diktat.ruleset.utils.hasItInLambda import com.saveourtool.diktat.ruleset.utils.hasNoParameters import org.jetbrains.kotlin.KtNodeTypes import org.jetbrains.kotlin.com.intellij.lang.ASTNode /** * Rule 5.2.7 check parameter name in outer lambda */ class ParameterNameInOuterLambdaRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(PARAMETER_NAME_IN_OUTER_LAMBDA) ) { /** * Configuration for the rule ParameterNameInOuterLambda */ private val configuration by lazy { ParameterNameInOuterLambdaConfiguration( configRules.getRuleConfig(PARAMETER_NAME_IN_OUTER_LAMBDA)?.configuration ?: emptyMap()) } override fun logic(node: ASTNode) { if (node.elementType == KtNodeTypes.LAMBDA_EXPRESSION) { checkLambda(node) } } private fun checkLambda(node: ASTNode) { val strictMode = configuration.strictMode val innerLambdaList = node.findAllDescendantsWithSpecificType(KtNodeTypes.LAMBDA_EXPRESSION, false) val hasInnerLambda = innerLambdaList.isNotEmpty() val outerLambdaHasNoParameterName = hasNoParameters(node) || node.hasExplicitIt() val innerLambdasHasNoIt = !hasInnerLambda || innerLambdaList.all { innerLambda -> !hasItInLambda(innerLambda) } if (!strictMode && outerLambdaHasNoParameterName && innerLambdasHasNoIt) { return } if (hasInnerLambda && doesLambdaContainIt(node)) { PARAMETER_NAME_IN_OUTER_LAMBDA.warn( configRules, emitWarn, "lambda without arguments has inner lambda", node.startOffset, node, ) } } /** * ParameterNameInOuterLambdaConfiguration used when we need to allow the usage of 'it' in outer lambda * * @param config - map of strings with configuration options for a Parameter Name In Outer Lambda rule */ class ParameterNameInOuterLambdaConfiguration(config: Map) : RuleConfiguration(config) { /** * Flag (when false) allows to use `it` in outer lambda, if in inner lambdas would be no `it` */ val strictMode = config["strictMode"]?.toBoolean() ?: true } companion object { const val NAME_ID = "parameter-name-in-outer-lambda" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter6/AvoidEmptyPrimaryConstructor.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter6 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.EMPTY_PRIMARY_CONSTRUCTOR import com.saveourtool.diktat.ruleset.rules.DiktatRule import org.jetbrains.kotlin.KtNodeTypes.CLASS import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.psi.KtClass /** * This rule checks if a class has an empty primary constructor. */ class AvoidEmptyPrimaryConstructor(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(EMPTY_PRIMARY_CONSTRUCTOR) ) { override fun logic(node: ASTNode) { if (node.elementType == CLASS) { checkClass(node.psi as KtClass) } } @Suppress("UnsafeCallOnNullableType") private fun checkClass(ktClass: KtClass) { if (ktClass.primaryConstructor?.valueParameters?.isNotEmpty() != false || ktClass.primaryConstructorModifierList != null) { return } EMPTY_PRIMARY_CONSTRUCTOR.warnAndFix(configRules, emitWarn, isFixMode, ktClass.nameIdentifier!!.text, ktClass.node.startOffset, ktClass.node) { ktClass.node.removeChild(ktClass.primaryConstructor!!.node) } } companion object { const val NAME_ID = "avoid-empty-primary-constructor" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter6/AvoidUtilityClass.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter6 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.common.config.rules.getCommonConfiguration import com.saveourtool.diktat.ruleset.constants.Warnings.AVOID_USING_UTILITY_CLASS import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.* import org.jetbrains.kotlin.KtNodeTypes.CLASS import org.jetbrains.kotlin.KtNodeTypes.CLASS_BODY import org.jetbrains.kotlin.KtNodeTypes.FUN import org.jetbrains.kotlin.KtNodeTypes.OBJECT_DECLARATION import org.jetbrains.kotlin.KtNodeTypes.PRIMARY_CONSTRUCTOR import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.kdoc.lexer.KDocTokens.KDOC import org.jetbrains.kotlin.lexer.KtTokens.BLOCK_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.EOL_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.IDENTIFIER import org.jetbrains.kotlin.lexer.KtTokens.LBRACE import org.jetbrains.kotlin.lexer.KtTokens.RBRACE import org.jetbrains.kotlin.lexer.KtTokens.WHITE_SPACE import org.jetbrains.kotlin.psi.psiUtil.children import java.util.Locale /** * Rule 6.4.1 checks that class/object, with a word "util" in its name, has only functions. */ class AvoidUtilityClass(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(AVOID_USING_UTILITY_CLASS) ) { override fun logic(node: ASTNode) { val config = configRules.getCommonConfiguration() val filePath = node.getFilePath() if (!node.hasTestAnnotation() && !isLocatedInTest(filePath.splitPathToDirs(), config.testAnchors)) { @Suppress("COLLAPSE_IF_STATEMENTS") if (node.elementType == OBJECT_DECLARATION || node.elementType == CLASS) { checkClass(node) } } } @Suppress("UnsafeCallOnNullableType", "WRONG_NEWLINES") private fun checkClass(node: ASTNode) { // checks that class/object doesn't contain primary constructor and its identifier doesn't has "utli" if (!node.hasChildOfType(IDENTIFIER) || node.hasChildOfType(PRIMARY_CONSTRUCTOR) || !node.findChildByType(IDENTIFIER)!!.text.lowercase(Locale.getDefault()).contains("util")) { return } node.findChildByType(CLASS_BODY) ?.children() ?.toList() ?.takeIf { childList -> childList.all { it.elementType in utilityClassChildren } } ?.filter { it.elementType == FUN } ?.ifEmpty { return } ?: return AVOID_USING_UTILITY_CLASS.warn(configRules, emitWarn, node.findChildByType(IDENTIFIER)?.text ?: node.text, node.startOffset, node) } companion object { const val NAME_ID = "avoid-utility-class" private val utilityClassChildren = listOf(LBRACE, WHITE_SPACE, FUN, RBRACE, KDOC, EOL_COMMENT, BLOCK_COMMENT, OBJECT_DECLARATION) } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter6/CustomGetterSetterRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter6 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.CUSTOM_GETTERS_SETTERS import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.* import org.jetbrains.kotlin.KtNodeTypes.MODIFIER_LIST import org.jetbrains.kotlin.KtNodeTypes.PROPERTY_ACCESSOR import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.lexer.KtTokens.GET_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.OVERRIDE_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.PRIVATE_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.SET_KEYWORD import org.jetbrains.kotlin.psi.KtProperty /** * Inspection that checks that no custom getters and setters are used for properties. */ class CustomGetterSetterRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(CUSTOM_GETTERS_SETTERS) ) { override fun logic(node: ASTNode) { if (node.elementType == PROPERTY_ACCESSOR) { checkForCustomGetersSetters(node) } } private fun checkForCustomGetersSetters(node: ASTNode) { val getter = node.getFirstChildWithType(GET_KEYWORD) val setter = node.getFirstChildWithType(SET_KEYWORD) val isPrivateSetter = node.getFirstChildWithType(MODIFIER_LIST)?.hasAnyChildOfTypes(PRIVATE_KEYWORD) ?: false val isOverrideGetter = node.treeParent.getFirstChildWithType(MODIFIER_LIST)?.hasAnyChildOfTypes(OVERRIDE_KEYWORD) ?: false // find matching node if (isPairPropertyBackingField(node.treeParent?.psi as? KtProperty, null)) { return } setter?.let { // only private custom setters are allowed if (!isPrivateSetter) { CUSTOM_GETTERS_SETTERS.warn(configRules, emitWarn, setter.text, setter.startOffset, node) } } getter?.let { // only override getter are allowed if (!isOverrideGetter) { CUSTOM_GETTERS_SETTERS.warn(configRules, emitWarn, getter.text, getter.startOffset, node) } } } companion object { const val NAME_ID = "custom-getter-setter" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter6/ExtensionFunctionsInFileRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter6 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.EXTENSION_FUNCTION_WITH_CLASS import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.findAllDescendantsWithSpecificType import com.saveourtool.diktat.ruleset.utils.getAllChildrenWithType import com.saveourtool.diktat.ruleset.utils.getFirstChildWithType import com.saveourtool.diktat.ruleset.utils.prevSibling import org.jetbrains.kotlin.KtNodeTypes.CLASS import org.jetbrains.kotlin.KtNodeTypes.FUN import org.jetbrains.kotlin.KtNodeTypes.TYPE_REFERENCE import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.PsiElement import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement import org.jetbrains.kotlin.lexer.KtModifierKeywordToken import org.jetbrains.kotlin.lexer.KtTokens.EXTERNAL_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.IDENTIFIER import org.jetbrains.kotlin.psi.KtClass import org.jetbrains.kotlin.psi.KtFunction import org.jetbrains.kotlin.psi.psiUtil.allChildren import org.jetbrains.kotlin.psi.stubs.elements.KtFileElementType /** * This rule checks if there are any extension functions for the class in the same file, where it is defined */ class ExtensionFunctionsInFileRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(EXTENSION_FUNCTION_WITH_CLASS) ) { override fun logic(node: ASTNode) { if (node.elementType == KtFileElementType.INSTANCE) { val classNames = collectAllClassNames(node) collectAllExtensionFunctionsWithSameClassName(node, classNames).forEach { fireWarning(it) } } } /** * Collects all class names in the [file], except those with modifiers from * the [ignore list][ignoredModifierTypes]. * * @throws IllegalArgumentException if [file] is not a * [FILE][KtFileElementType.INSTANCE] node. */ private fun collectAllClassNames(file: ASTNode): List { require(file.elementType == KtFileElementType.INSTANCE) val classes = file.findAllDescendantsWithSpecificType(CLASS) return classes.asSequence() .map(ASTNode::getPsi) .filterIsInstance(KtClass::class.java) .filter { clazz -> clazz.modifierTypes().none { modifierType -> modifierType in ignoredModifierTypes } } .map(KtClass::getName) .filterNotNull() .toList() } private fun fireWarning(node: ASTNode) { EXTENSION_FUNCTION_WITH_CLASS.warn(configRules, emitWarn, "fun ${(node.psi as KtFunction).name}", node.startOffset, node) } private fun collectAllExtensionFunctionsWithSameClassName(node: ASTNode, classNames: List): List = node.getAllChildrenWithType(FUN).filter { isExtensionFunctionWithClassName(it, classNames) } @Suppress("UnsafeCallOnNullableType") private fun isExtensionFunctionWithClassName(node: ASTNode, classNames: List): Boolean = node.getFirstChildWithType(IDENTIFIER)!!.prevSibling { it.elementType == TYPE_REFERENCE }?.text in classNames companion object { const val NAME_ID = "extension-functions-class-file" /** * Types of class/interface modifiers which, if present, don't trigger * the warning. * * @since 1.2.5 */ private val ignoredModifierTypes: Array = arrayOf( EXTERNAL_KEYWORD, ) /** * @since 1.2.5 */ private fun KtClass.modifiers(): Sequence = modifierList?.allChildren ?: emptySequence() /** * @since 1.2.5 */ private fun KtClass.modifierTypes(): Sequence = modifiers() .filterIsInstance() .map(LeafPsiElement::getElementType) .filterIsInstance() } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter6/ExtensionFunctionsSameNameRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter6 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.EXTENSION_FUNCTION_SAME_SIGNATURE import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.findAllDescendantsWithSpecificType import com.saveourtool.diktat.ruleset.utils.findChildAfter import com.saveourtool.diktat.ruleset.utils.findChildBefore import com.saveourtool.diktat.ruleset.utils.findLeafWithSpecificType import com.saveourtool.diktat.ruleset.utils.getAllChildrenWithType import com.saveourtool.diktat.ruleset.utils.getFirstChildWithType import com.saveourtool.diktat.ruleset.utils.hasChildOfType import org.jetbrains.kotlin.KtNodeTypes.CLASS import org.jetbrains.kotlin.KtNodeTypes.FUN import org.jetbrains.kotlin.KtNodeTypes.SUPER_TYPE_CALL_ENTRY import org.jetbrains.kotlin.KtNodeTypes.SUPER_TYPE_LIST import org.jetbrains.kotlin.KtNodeTypes.TYPE_REFERENCE import org.jetbrains.kotlin.KtNodeTypes.VALUE_PARAMETER_LIST import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.lexer.KtTokens.COLON import org.jetbrains.kotlin.lexer.KtTokens.DOT import org.jetbrains.kotlin.lexer.KtTokens.IDENTIFIER import org.jetbrains.kotlin.psi.KtClass import org.jetbrains.kotlin.psi.KtNamedFunction import org.jetbrains.kotlin.psi.KtParameterList import org.jetbrains.kotlin.psi.stubs.elements.KtFileElementType internal typealias RelatedClasses = List> internal typealias SimilarSignatures = List> /** * This rule checks if extension functions with the same signature don't have related classes */ class ExtensionFunctionsSameNameRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(EXTENSION_FUNCTION_SAME_SIGNATURE) ) { override fun logic(node: ASTNode) { /* * 1) Collect all classes that extend other classes (collect related classes) * 2) Collect all extension functions with same signature * 3) Check if classes of functions are related */ if (node.elementType == KtFileElementType.INSTANCE) { val relatedClasses = collectAllRelatedClasses(node) val extFunctionsWithSameName = collectAllExtensionFunctions(node) handleFunctions(relatedClasses, extFunctionsWithSameName) } } // Fixme: should find all related classes in project, not only in file @Suppress("UnsafeCallOnNullableType", "TYPE_ALIAS") private fun collectAllRelatedClasses(node: ASTNode): List> { val classListWithInheritance = node .findAllDescendantsWithSpecificType(CLASS) .filterNot { (it.psi as KtClass).isInterface() } .filter { it.hasChildOfType(SUPER_TYPE_LIST) } val pairs: MutableList> = mutableListOf() classListWithInheritance.forEach { classNode -> val callEntries = classNode.findChildByType(SUPER_TYPE_LIST)!!.getAllChildrenWithType(SUPER_TYPE_CALL_ENTRY) callEntries.forEach { entry -> val className = (classNode.psi as KtClass).name!! val entryName = entry.findLeafWithSpecificType(IDENTIFIER)!! pairs.add(Pair(className, entryName.text)) } } return pairs } @Suppress("UnsafeCallOnNullableType", "TYPE_ALIAS") private fun collectAllExtensionFunctions(node: ASTNode): SimilarSignatures { val extensionFunctionList = node.findAllDescendantsWithSpecificType(FUN).filter { it.hasChildOfType(TYPE_REFERENCE) && it.hasChildOfType(DOT) } val distinctFunctionSignatures: MutableMap = mutableMapOf() // maps function signatures on node it is used by val extensionFunctionsPairs: MutableList> = mutableListOf() // pairs extension functions with same signature extensionFunctionList.forEach { func -> val functionName = (func.psi as KtNamedFunction).name!! // List is used to show param names in warning val params = (func.getFirstChildWithType(VALUE_PARAMETER_LIST)!!.psi as KtParameterList).parameters.map { it.name!! } val returnType = func.findChildAfter(COLON, TYPE_REFERENCE)?.text val className = func.findChildBefore(DOT, TYPE_REFERENCE)!!.text val signature = FunctionSignature(functionName, params, returnType) if (distinctFunctionSignatures.contains(signature)) { val secondFuncClassName = distinctFunctionSignatures[signature]!!.findChildBefore(DOT, TYPE_REFERENCE)!!.text extensionFunctionsPairs.add(Pair( ExtensionFunction(secondFuncClassName, signature, distinctFunctionSignatures[signature]!!), ExtensionFunction(className, signature, func))) } else { distinctFunctionSignatures[signature] = func } } return extensionFunctionsPairs } private fun handleFunctions(relatedClasses: RelatedClasses, functions: SimilarSignatures) { functions.forEach { val firstClassName = it.first.className val secondClassName = it.second.className if (relatedClasses.hasRelatedClasses(Pair(firstClassName, secondClassName))) { raiseWarning(it.first.node, it.first, it.second) raiseWarning(it.second.node, it.first, it.second) } } } private fun RelatedClasses.hasRelatedClasses(pair: Pair) = any { it.first == pair.first && it.second == pair.second || it.first == pair.second && it.second == pair.first } private fun raiseWarning( node: ASTNode, firstFunc: ExtensionFunction, secondFunc: ExtensionFunction ) { EXTENSION_FUNCTION_SAME_SIGNATURE.warn(configRules, emitWarn, "$firstFunc and $secondFunc", node.startOffset, node) } /** * Class that represents a function's signature * @property name function name * @property parameters function parameters as strings * @property returnType return type of a function if it is explicitly set */ internal data class FunctionSignature( val name: String, val parameters: List, val returnType: String? ) { override fun toString() = "$name$parameters${returnType?.let { ": $it" } ?: ""}" } /** * Class that represents an extension function * @property className name of receiver class * @property signature a [FunctionSignature] of a function * @property node a [ASTNode] that represents this function */ internal data class ExtensionFunction( val className: String, val signature: FunctionSignature, val node: ASTNode ) { override fun toString() = "fun $className.$signature" } companion object { const val NAME_ID = "extension-functions-same-name" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter6/ImplicitBackingPropertyRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter6 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.NO_CORRESPONDING_PROPERTY import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.findAllDescendantsWithSpecificType import com.saveourtool.diktat.ruleset.utils.getFirstChildWithType import com.saveourtool.diktat.ruleset.utils.hasAnyChildOfTypes import com.saveourtool.diktat.ruleset.utils.hasChildOfType import org.jetbrains.kotlin.KtNodeTypes.BLOCK import org.jetbrains.kotlin.KtNodeTypes.CLASS_BODY import org.jetbrains.kotlin.KtNodeTypes.DOT_QUALIFIED_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.PROPERTY import org.jetbrains.kotlin.KtNodeTypes.PROPERTY_ACCESSOR import org.jetbrains.kotlin.KtNodeTypes.REFERENCE_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.RETURN import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.lexer.KtTokens.GET_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.IDENTIFIER import org.jetbrains.kotlin.lexer.KtTokens.SET_KEYWORD import org.jetbrains.kotlin.psi.KtProperty /** * This rule checks if there is a backing property for field with property accessors, in case they don't use field keyword */ class ImplicitBackingPropertyRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(NO_CORRESPONDING_PROPERTY) ) { override fun logic(node: ASTNode) { if (node.elementType == CLASS_BODY) { findAllProperties(node) } } @Suppress("UnsafeCallOnNullableType") private fun findAllProperties(node: ASTNode) { val properties = node.getChildren(null).filter { it.elementType == PROPERTY } val propsWithBackSymbol = properties .filter { it.getFirstChildWithType(IDENTIFIER)!!.text.startsWith("_") } .map { it.getFirstChildWithType(IDENTIFIER)!!.text } properties.filter { it.hasAnyChildOfTypes(PROPERTY_ACCESSOR) }.forEach { validateAccessors(it, propsWithBackSymbol) } } private fun validateAccessors(node: ASTNode, propsWithBackSymbol: List) { val accessors = node.findAllDescendantsWithSpecificType(PROPERTY_ACCESSOR).filter { it.hasChildOfType(BLOCK) } // exclude get with expression body accessors.filter { it.hasChildOfType(GET_KEYWORD) }.forEach { handleGetAccessors(it, node, propsWithBackSymbol) } accessors.filter { it.hasChildOfType(SET_KEYWORD) }.forEach { handleSetAccessors(it, node, propsWithBackSymbol) } } @Suppress("UnsafeCallOnNullableType") private fun handleGetAccessors( accessor: ASTNode, node: ASTNode, propsWithBackSymbol: List ) { val refExprs = accessor .findAllDescendantsWithSpecificType(RETURN) .filterNot { it.hasChildOfType(DOT_QUALIFIED_EXPRESSION) } .flatMap { it.findAllDescendantsWithSpecificType(REFERENCE_EXPRESSION) } val localProps = accessor .findAllDescendantsWithSpecificType(PROPERTY) .map { (it.psi as KtProperty).name!! } // If refExprs is empty then we assume that it returns some constant if (refExprs.isNotEmpty()) { handleReferenceExpressions(node, refExprs, propsWithBackSymbol, localProps) } } @Suppress("UnsafeCallOnNullableType") private fun handleSetAccessors( accessor: ASTNode, node: ASTNode, propsWithBackSymbol: List ) { val refExprs = accessor.findAllDescendantsWithSpecificType(REFERENCE_EXPRESSION) // In set we don't check for local properties. At least one reference expression should contain field or _prop if (refExprs.isNotEmpty()) { handleReferenceExpressions(node, refExprs, propsWithBackSymbol, null) } } @Suppress("UnsafeCallOnNullableType") private fun handleReferenceExpressions(node: ASTNode, expressions: List, backingPropertiesNames: List, localProperties: List? ) { if (expressions.none { backingPropertiesNames.contains(it.text) || it.text == "field" || localProperties?.contains(it.text) == true }) { raiseWarning(node, node.getFirstChildWithType(IDENTIFIER)!!.text) } } private fun raiseWarning(node: ASTNode, propName: String) { NO_CORRESPONDING_PROPERTY.warn(configRules, emitWarn, "$propName has no corresponding property with name _$propName", node.startOffset, node) } companion object { const val NAME_ID = "implicit-backing-property" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter6/PropertyAccessorFields.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter6 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.WRONG_NAME_OF_VARIABLE_INSIDE_ACCESSOR import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.findAllDescendantsWithSpecificType import com.saveourtool.diktat.ruleset.utils.isGoingAfter import org.jetbrains.kotlin.KtNodeTypes.BLOCK import org.jetbrains.kotlin.KtNodeTypes.CALL_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.DOT_QUALIFIED_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.PROPERTY import org.jetbrains.kotlin.KtNodeTypes.PROPERTY_ACCESSOR import org.jetbrains.kotlin.KtNodeTypes.REFERENCE_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.THIS_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.TYPE_REFERENCE import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.tree.TokenSet import org.jetbrains.kotlin.lexer.KtTokens.IDENTIFIER import org.jetbrains.kotlin.psi.KtProperty /** * Rule check that never use the name of a variable in the custom getter or setter */ class PropertyAccessorFields(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(WRONG_NAME_OF_VARIABLE_INSIDE_ACCESSOR) ) { override fun logic(node: ASTNode) { if (node.elementType == PROPERTY_ACCESSOR) { checkPropertyAccessor(node) } } // fixme should use shadow-check when it will be done private fun checkPropertyAccessor(node: ASTNode) { val leftValue = node.treeParent.findChildByType(IDENTIFIER) ?: return val isNotExtensionProperty = leftValue.treePrev?.treePrev?.elementType != TYPE_REFERENCE val firstReferenceWithSameName = node .findAllDescendantsWithSpecificType(REFERENCE_EXPRESSION) .mapNotNull { it.findChildByType(IDENTIFIER) } .firstOrNull { it.text == leftValue.text && (it.treeParent.treeParent.elementType != DOT_QUALIFIED_EXPRESSION || it.treeParent.treeParent.firstChildNode.elementType == THIS_EXPRESSION) } val isContainLocalVarSameName = node .findChildByType(BLOCK) ?.getChildren(TokenSet.create(PROPERTY)) ?.filter { (it.psi as KtProperty).nameIdentifier?.text == leftValue.text } ?.none { firstReferenceWithSameName?.isGoingAfter(it) ?: false } ?: true val isNotCallExpression = firstReferenceWithSameName?.treeParent?.treeParent?.elementType != CALL_EXPRESSION if (firstReferenceWithSameName != null && isContainLocalVarSameName && isNotCallExpression && isNotExtensionProperty) { WRONG_NAME_OF_VARIABLE_INSIDE_ACCESSOR.warn(configRules, emitWarn, node.text, node.startOffset, node) } } companion object { const val NAME_ID = "getter-setter-fields" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter6/RunInScript.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter6 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.RUN_IN_SCRIPT import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.* import com.saveourtool.diktat.util.isKotlinScript import org.jetbrains.kotlin.KtNodeTypes.CALL_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.DOT_QUALIFIED_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.LAMBDA_ARGUMENT import org.jetbrains.kotlin.KtNodeTypes.LAMBDA_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.PARENTHESIZED import org.jetbrains.kotlin.KtNodeTypes.SCRIPT_INITIALIZER import org.jetbrains.kotlin.KtNodeTypes.VALUE_ARGUMENT import org.jetbrains.kotlin.KtNodeTypes.VALUE_ARGUMENT_LIST import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.CompositeElement /** * Rule that checks if kts script contains other functions except run code * In .kts files allow use only property declaration, function, classes, and code inside `run` block * In gradle.kts files allow to call binary expression with EQ, expression and dot qualified expression in addition to everything used in .kts files */ class RunInScript( configRules: List, ) : DiktatRule( id = NAME_ID, configRules = configRules, inspections = listOf(RUN_IN_SCRIPT), ) { override fun logic(node: ASTNode) { if (node.elementType == SCRIPT_INITIALIZER && node.getFilePath().isKotlinScript()) { if (node.getFilePath().isGradleScript()) { checkGradleNode(node) } else { checkScript(node) } } } private fun checkGradleNode(node: ASTNode) { val astNode = if (node.hasEqBinaryExpression()) { return } else { when (node.firstChildNode.elementType) { PARENTHESIZED -> node.firstChildNode else -> node } } if (!astNode.hasChildOfType(CALL_EXPRESSION) && !astNode.hasChildOfType(DOT_QUALIFIED_EXPRESSION)) { warnRunInScript(astNode) } } private fun checkScript(node: ASTNode) { val isLambdaArgument = node.firstChildNode.hasChildOfType(LAMBDA_ARGUMENT) val isLambdaInsideValueArgument = node.firstChildNode .findChildByType(VALUE_ARGUMENT_LIST) ?.findChildByType(VALUE_ARGUMENT) ?.findChildByType(LAMBDA_EXPRESSION) != null if (!isLambdaArgument && !isLambdaInsideValueArgument) { warnRunInScript(node) } } private fun warnRunInScript(node: ASTNode) { RUN_IN_SCRIPT.warnAndFix(configRules, emitWarn, isFixMode, node.text, node.startOffset, node) { if (node.firstChildNode.elementType != DOT_QUALIFIED_EXPRESSION) { val parent = node.treeParent val newNode = KotlinParser().createNode("run {\n ${node.text}\n} \n") val newScript = CompositeElement(SCRIPT_INITIALIZER) parent.addChild(newScript, node) newScript.addChild(newNode) parent.removeChild(node) } } } companion object { const val NAME_ID = "run-script" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter6/TrivialPropertyAccessors.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter6 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.findAllDescendantsWithSpecificType import com.saveourtool.diktat.ruleset.utils.getFirstChildWithType import com.saveourtool.diktat.ruleset.utils.getIdentifierName import com.saveourtool.diktat.ruleset.utils.hasChildOfType import com.saveourtool.diktat.ruleset.utils.isWhiteSpace import org.jetbrains.kotlin.KtNodeTypes.BINARY_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.BLOCK import org.jetbrains.kotlin.KtNodeTypes.PROPERTY_ACCESSOR import org.jetbrains.kotlin.KtNodeTypes.REFERENCE_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.VALUE_PARAMETER_LIST import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.lexer.KtTokens.BLOCK_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.EOL_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.LBRACE import org.jetbrains.kotlin.lexer.KtTokens.RBRACE import org.jetbrains.kotlin.lexer.KtTokens.WHITE_SPACE import org.jetbrains.kotlin.psi.KtBinaryExpression import org.jetbrains.kotlin.psi.KtPropertyAccessor /** * This rule checks if there are any trivial getters and setters and, if so, deletes them */ class TrivialPropertyAccessors(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED) ) { override fun logic(node: ASTNode) { if (node.elementType == PROPERTY_ACCESSOR) { handlePropertyAccessors(node) } } private fun handlePropertyAccessors(node: ASTNode) { if ((node.psi as KtPropertyAccessor).isGetter) { handleGetAccessor(node) } else { handleSetAccessor(node) } } @Suppress("UnsafeCallOnNullableType") private fun handleSetAccessor(node: ASTNode) { val valueParamName = node .getFirstChildWithType(VALUE_PARAMETER_LIST) ?.firstChildNode ?.getIdentifierName() ?.text if (node.hasChildOfType(BLOCK) && !valueParamName.isNullOrEmpty()) { val block = node.getFirstChildWithType(BLOCK)!! val blockChildren = block.getChildren(null).filter { it.elementType !in excessChildrenTypes } if (blockChildren.size == 1 && blockChildren.first().elementType == BINARY_EXPRESSION && (blockChildren.first().psi as KtBinaryExpression).left?.text == "field" && (blockChildren.first().psi as KtBinaryExpression).right?.text == valueParamName ) { raiseWarning(node) } } } @Suppress("UnsafeCallOnNullableType") private fun handleGetAccessor(node: ASTNode) { // It handles both cases: get() = ... and get() { return ... } val references = node.findAllDescendantsWithSpecificType(REFERENCE_EXPRESSION) if (references.singleOrNull()?.text == "field") { raiseWarning(node) } else if (node.getChildren(null).size == ONE_CHILD_IN_ARRAY) { raiseWarning(node) } } private fun raiseWarning(node: ASTNode) { TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED.warnAndFix(configRules, emitWarn, isFixMode, node.text, node.startOffset, node) { val property = (node.psi as KtPropertyAccessor).property.node if (node.treePrev.isWhiteSpace()) { property.removeChild(node.treePrev) } property.removeChild(node) } } companion object { const val NAME_ID = "trivial-property-accessors" private const val ONE_CHILD_IN_ARRAY = 1 private val excessChildrenTypes = listOf(LBRACE, RBRACE, WHITE_SPACE, EOL_COMMENT, BLOCK_COMMENT) } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter6/UseLastIndex.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter6 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.* import org.jetbrains.kotlin.KtNodeTypes.BINARY_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.DOT_QUALIFIED_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.INTEGER_CONSTANT import org.jetbrains.kotlin.KtNodeTypes.OPERATION_REFERENCE import org.jetbrains.kotlin.KtNodeTypes.REFERENCE_EXPRESSION import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.psi.psiUtil.children /** * This rule checks if there use property length with operation - 1 and fix this on lastIndex */ class UseLastIndex(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(Warnings.USE_LAST_INDEX) ) { override fun logic(node: ASTNode) { if (node.elementType == BINARY_EXPRESSION) { changeRight(node) } } private fun changeRight(node: ASTNode) { val listWithRightLength = node.children().filter { val operation = node.getFirstChildWithType(OPERATION_REFERENCE) val number = node.getFirstChildWithType(INTEGER_CONSTANT) it.elementType == DOT_QUALIFIED_EXPRESSION && it.lastChildNode.text == "length" && it.lastChildNode.elementType == REFERENCE_EXPRESSION && operation?.text == "-" && number?.text == "1" } if (listWithRightLength.toList().isNotEmpty()) { Warnings.USE_LAST_INDEX.warnAndFix(configRules, emitWarn, isFixMode, node.text, node.startOffset, node) { fix(node) } } } private fun fix(node: ASTNode) { // A.B.length - 1 -> A.B val text = node.firstChildNode.text.replace("length", "lastIndex") val parent = node.treeParent val textParent = parent.text.replace(node.text, text) val newParent = KotlinParser().createNode(textParent) parent.treeParent.replaceChild(parent, newParent) } companion object { const val NAME_ID = "last-index" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter6/UselessSupertype.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter6 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.USELESS_SUPERTYPE import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.* import com.saveourtool.diktat.ruleset.utils.parent import org.jetbrains.kotlin.KtNodeTypes.CALL_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.CLASS import org.jetbrains.kotlin.KtNodeTypes.CLASS_BODY import org.jetbrains.kotlin.KtNodeTypes.DOT_QUALIFIED_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.FUN import org.jetbrains.kotlin.KtNodeTypes.MODIFIER_LIST import org.jetbrains.kotlin.KtNodeTypes.REFERENCE_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.SUPER_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.SUPER_TYPE_CALL_ENTRY import org.jetbrains.kotlin.KtNodeTypes.SUPER_TYPE_ENTRY import org.jetbrains.kotlin.KtNodeTypes.SUPER_TYPE_LIST import org.jetbrains.kotlin.KtNodeTypes.TYPE_REFERENCE import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.lexer.KtTokens.CLASS_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.IDENTIFIER import org.jetbrains.kotlin.lexer.KtTokens.OPEN_KEYWORD import org.jetbrains.kotlin.psi.psiUtil.siblings import org.jetbrains.kotlin.psi.stubs.elements.KtFileElementType import java.util.HashMap /** * rule 6.1.5 * Explicit supertype qualification should not be used if there is not clash between called methods * fixme can't fix supertypes that are defined in other files. */ class UselessSupertype(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(USELESS_SUPERTYPE) ) { override fun logic(node: ASTNode) { if (node.elementType == CLASS) { checkClass(node) } } private fun checkClass(node: ASTNode) { val superNodes = node .findChildByType(SUPER_TYPE_LIST) ?.findAllNodesWithCondition { it.elementType in superType } ?.takeIf { it.isNotEmpty() } ?: return val qualifiedSuperCalls = node .findAllDescendantsWithSpecificType(DOT_QUALIFIED_EXPRESSION) .mapNotNull { findFunWithSuper(it) } .ifEmpty { return } if (superNodes.size == 1) { qualifiedSuperCalls.map { removeSupertype(it.first) } } else { handleManyImpl(superNodes, qualifiedSuperCalls) } } @Suppress("TYPE_ALIAS") private fun handleManyImpl(superNodes: List, overrideNodes: List>) { val uselessSuperType = findAllSupers(superNodes, overrideNodes.map { it.second.text }) ?.filter { it.value == 1 } // filtering methods whose names occur only once ?.map { it.key } // take their names ?: return overrideNodes .filter { it.second.text in uselessSuperType }.map { removeSupertype(it.first) } } @Suppress("UnsafeCallOnNullableType") private fun removeSupertype(node: ASTNode) { USELESS_SUPERTYPE.warnAndFix(configRules, emitWarn, isFixMode, node.text, node.startOffset, node) { val startNode = node.parent { it.elementType == SUPER_EXPRESSION }!!.findChildByType(REFERENCE_EXPRESSION)!! val lastNode = startNode.siblings(true).last() startNode.treeParent.removeRange(startNode.treeNext, lastNode) startNode.treeParent.removeChild(lastNode) } } /** * Method finds pair of identifier supertype and method name else return null * example: super.foo() -> return Pair(A, foo) * super.foo() -> null * * @param node - node of type DOT_QUALIFIED_EXPRESSION * @return pair of identifier */ @Suppress("UnsafeCallOnNullableType", "WRONG_NEWLINES") private fun findFunWithSuper(node: ASTNode) = Pair( node.findChildByType(SUPER_EXPRESSION) ?.findChildByType(TYPE_REFERENCE) ?.findAllDescendantsWithSpecificType(IDENTIFIER) ?.firstOrNull(), node.findChildByType(CALL_EXPRESSION) ?.findAllDescendantsWithSpecificType(IDENTIFIER) ?.firstOrNull()) .run { if (first == null || second == null) null else first!! to second!! } /** * The method looks in the same file for all super interfaces or a class, in each it looks for methods * that can be overridden and creates a map with a key - the name of the method and value - the number of times it meets * * @param superTypeList - list of identifiers super classes * @param methodsName - name of overrides methods * @return map name of method and the number of times it meets */ @Suppress("UnsafeCallOnNullableType", "WRONG_NEWLINES") private fun findAllSupers(superTypeList: List, methodsName: List): Map? { val fileNode = superTypeList.first().parent { it.elementType == KtFileElementType.INSTANCE }!! val superNodesIdentifier = superTypeList.map { it.findAllDescendantsWithSpecificType(IDENTIFIER) .first() .text } val superNodes = fileNode.findAllNodesWithCondition { superClass -> superClass.elementType == CLASS && superClass.getIdentifierName()!!.text in superNodesIdentifier }.mapNotNull { it.findChildByType(CLASS_BODY) } if (superNodes.size != superTypeList.size) { return null } val functionNameMap: HashMap = hashMapOf() superNodes.forEach { classBody -> val overrideFunctions = classBody.findAllDescendantsWithSpecificType(FUN) .filter { (if (classBody.treeParent.hasChildOfType(CLASS_KEYWORD)) it.findChildByType(MODIFIER_LIST)!!.hasChildOfType(OPEN_KEYWORD) else true) && it.getIdentifierName()!!.text in methodsName } @Suppress("PARAMETER_NAME_IN_OUTER_LAMBDA") overrideFunctions.forEach { functionNameMap.compute(it.getIdentifierName()!!.text) { _, oldValue -> (oldValue ?: 0) + 1 } } } return functionNameMap.toMap() } companion object { const val NAME_ID = "useless-override" private val superType = listOf(SUPER_TYPE_CALL_ENTRY, SUPER_TYPE_ENTRY) } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter6/classes/AbstractClassesRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter6.classes import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.CLASS_SHOULD_NOT_BE_ABSTRACT import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.getAllChildrenWithType import com.saveourtool.diktat.ruleset.utils.getFirstChildWithType import com.saveourtool.diktat.ruleset.utils.hasChildOfType import org.jetbrains.kotlin.KtNodeTypes.CLASS import org.jetbrains.kotlin.KtNodeTypes.CLASS_BODY import org.jetbrains.kotlin.KtNodeTypes.FUN import org.jetbrains.kotlin.KtNodeTypes.MODIFIER_LIST import org.jetbrains.kotlin.KtNodeTypes.PROPERTY import org.jetbrains.kotlin.KtNodeTypes.SUPER_TYPE_CALL_ENTRY import org.jetbrains.kotlin.KtNodeTypes.SUPER_TYPE_ENTRY import org.jetbrains.kotlin.KtNodeTypes.SUPER_TYPE_LIST import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement import org.jetbrains.kotlin.lexer.KtTokens.ABSTRACT_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.IDENTIFIER import org.jetbrains.kotlin.lexer.KtTokens.OPEN_KEYWORD import org.jetbrains.kotlin.psi.psiUtil.children /** * Checks if abstract class has any abstract method. If not, warns that class should not be abstract */ class AbstractClassesRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(CLASS_SHOULD_NOT_BE_ABSTRACT) ) { override fun logic(node: ASTNode) { if (node.elementType == CLASS) { val classBody = node.getFirstChildWithType(CLASS_BODY) ?: return // If an abstract class extends another class, than that base class can be abstract too. // Then this class must have `abstract` modifier even if it doesn't have any abstract members. // Class also can have `abstract` modifier if it implements interface if (node.hasAbstractModifier() && node.isNotSubclass()) { handleAbstractClass(classBody, node) } } } private fun ASTNode.hasAbstractModifier(): Boolean = getFirstChildWithType(MODIFIER_LIST)?.hasChildOfType(ABSTRACT_KEYWORD) ?: false private fun ASTNode.isNotSubclass(): Boolean = findChildByType(SUPER_TYPE_LIST)?.children()?.filter { it.elementType == SUPER_TYPE_CALL_ENTRY || it.elementType == SUPER_TYPE_ENTRY }?.none() ?: true @Suppress("UnsafeCallOnNullableType") private fun handleAbstractClass(node: ASTNode, classNode: ASTNode) { val functions = node.getAllChildrenWithType(FUN) val properties = node.getAllChildrenWithType(PROPERTY) val members = functions + properties val identifier = classNode.getFirstChildWithType(IDENTIFIER)!!.text if (members.isNotEmpty() && members.none { it.hasAbstractModifier() }) { CLASS_SHOULD_NOT_BE_ABSTRACT.warnAndFix(configRules, emitWarn, isFixMode, identifier, node.startOffset, node) { val modList = classNode.getFirstChildWithType(MODIFIER_LIST)!! val abstractKeyword = modList.getFirstChildWithType(ABSTRACT_KEYWORD)!! val newOpenKeyword = LeafPsiElement(OPEN_KEYWORD, "open") modList.replaceChild(abstractKeyword, newOpenKeyword) } } } companion object { const val NAME_ID = "abstract-classes" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter6/classes/CompactInitialization.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter6.classes import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.COMPACT_OBJECT_INITIALIZATION import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.KotlinParser import com.saveourtool.diktat.ruleset.utils.findAllDescendantsWithSpecificType import com.saveourtool.diktat.ruleset.utils.findLeafWithSpecificType import com.saveourtool.diktat.ruleset.utils.getFunctionName import com.saveourtool.diktat.ruleset.utils.isPartOfComment import io.github.oshai.kotlinlogging.KotlinLogging import org.jetbrains.kotlin.KtNodeTypes.CALL_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.OPERATION_REFERENCE import org.jetbrains.kotlin.KtNodeTypes.REFERENCE_EXPRESSION import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace import org.jetbrains.kotlin.kdoc.lexer.KDocTokens.KDOC import org.jetbrains.kotlin.lexer.KtTokens.BLOCK_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.EOL_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.EQ import org.jetbrains.kotlin.lexer.KtTokens.LBRACE import org.jetbrains.kotlin.lexer.KtTokens.THIS_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.WHITE_SPACE import org.jetbrains.kotlin.psi.KtBinaryExpression import org.jetbrains.kotlin.psi.KtCallExpression import org.jetbrains.kotlin.psi.KtCallableReferenceExpression import org.jetbrains.kotlin.psi.KtDotQualifiedExpression import org.jetbrains.kotlin.psi.KtNameReferenceExpression import org.jetbrains.kotlin.psi.KtParenthesizedExpression import org.jetbrains.kotlin.psi.KtProperty import org.jetbrains.kotlin.psi.KtQualifiedExpression import org.jetbrains.kotlin.psi.KtReferenceExpression import org.jetbrains.kotlin.psi.psiUtil.siblings import org.jetbrains.kotlin.psi.psiUtil.startOffset /** * This rules checks if an object initialization can be wrapped into an `apply` function. * This is useful for classes that, e.g. have single constructor without parameters and setters for all the parameters. * FixMe: When assigned variable's name is also a `this@apply`'s property, it should be changed to qualified name, * e.g `this@Foo`. But for this we need a mechanism to determine declaration scope and it's label. */ class CompactInitialization(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(COMPACT_OBJECT_INITIALIZATION) ) { private val kotlinParser by lazy { KotlinParser() } override fun logic(node: ASTNode) { node .psi .let { it as? KtProperty } ?.takeIf { it.hasInitializer() } ?.let(::handleProperty) } /** * Check property's initializer: if it is a method call, we find all consecutive statements that are this property's * fields accessors and wrap them in an `apply` function. */ @Suppress("UnsafeCallOnNullableType", "PARAMETER_NAME_IN_OUTER_LAMBDA") private fun handleProperty(property: KtProperty) { property.run { val propertyName = name siblings(forward = true, withItself = false) .filterNot { it.node.isPartOfComment() || it is PsiWhiteSpace } .takeWhile { // statements like `name.field = value` where name == propertyName it is KtBinaryExpression && it.node.findChildByType(OPERATION_REFERENCE)?.findChildByType(EQ) != null && (it.left as? KtDotQualifiedExpression)?.run { (receiverExpression as? KtNameReferenceExpression)?.getReferencedName() == propertyName } ?: false } .map { // collect as an assignment associated with assigned field name it as KtBinaryExpression to (it.left as KtDotQualifiedExpression).selectorExpression!! } } .filter { (assignment, _) -> assignment.node.findLeafWithSpecificType(THIS_KEYWORD) == null } .toList() .forEach { (assignment, field) -> COMPACT_OBJECT_INITIALIZATION.warnAndFix( configRules, emitWarn, isFixMode, field.text, assignment.startOffset, assignment.node ) { moveAssignmentIntoApply(property, assignment) } } } @Suppress( "UnsafeCallOnNullableType", "NestedBlockDepth", "TOO_LONG_FUNCTION" ) private fun moveAssignmentIntoApply(property: KtProperty, assignment: KtBinaryExpression) { // get apply expression or create empty; convert `apply(::foo)` to `apply { foo(this) }` if necessary getOrCreateApplyBlock(property).let(::convertValueParametersToLambdaArgument) // apply expression can have been changed earlier, so we need to get it once again with(getOrCreateApplyBlock(property)) { lambdaArguments .single() // KtLambdaArgument#getArgumentExpression is Nullable IfNotParsed .getLambdaExpression()!! .run { val bodyExpression = functionLiteral // note: we are dealing with function literal: braces belong to KtFunctionLiteral, // but it's body is a KtBlockExpression, which therefore doesn't have braces .bodyExpression!! .node // move comments and empty lines before `assignment` into `apply` assignment .node .siblings(forward = false) .takeWhile { it.elementType in listOf(WHITE_SPACE, EOL_COMMENT, BLOCK_COMMENT, KDOC) } .toList() .reversed() .forEachIndexed { index, it -> // adds whiteSpace to functional literal if previous of bodyExpression is LBRACE if (index == 0 && bodyExpression.treePrev.elementType == LBRACE && it.elementType == WHITE_SPACE) { bodyExpression.treeParent.addChild(it.clone() as ASTNode, bodyExpression) } else { bodyExpression.addChild(it.clone() as ASTNode, null) } it.treeParent.removeChild(it) } val receiverName = (assignment.left as KtDotQualifiedExpression).receiverExpression // looking for usages of receiver in right part val identifiers = assignment.right!!.node.findAllDescendantsWithSpecificType(REFERENCE_EXPRESSION) identifiers.forEach { if (it.text == receiverName.text && it.treeParent.elementType != CALL_EXPRESSION) { it.treeParent.replaceChild(it, kotlinParser.createNode("this")) } } // strip receiver name and move assignment itself into `apply` bodyExpression.addChild(kotlinParser.createNode(assignment.text.substringAfter('.')), null) assignment.node.run { treeParent.removeChild(this) } } } } @Suppress("UnsafeCallOnNullableType") private fun getOrCreateApplyBlock(property: KtProperty): KtCallExpression = (property.initializer as? KtDotQualifiedExpression) ?.selectorExpression ?.let { it as? KtCallExpression } ?.takeIf { it.getFunctionName() == "apply" } ?: run { // add apply block property.node.run { val newInitializerNodeText = buildInitializerNodeText(property) val newInitializerNode = kotlinParser.createNode(newInitializerNodeText) replaceChild(property.initializer!!.node, newInitializerNode) } (property.initializer as KtDotQualifiedExpression).selectorExpression!! as KtCallExpression } @Suppress("UnsafeCallOnNullableType") private fun buildInitializerNodeText(property: KtProperty): String { val isRequiresParentheses = property.initializer.let { // list of expression types, that can be directly followed by a dot-qualified call // e.g. val x = foo() -> val x = foo().apply {} // e.g. val x = foo + bar -> val x = (foo + bar).apply {} it is KtParenthesizedExpression || it is KtQualifiedExpression || it is KtReferenceExpression }.not() return buildString { if (isRequiresParentheses) { append("(") } append(property.initializer!!.text) if (isRequiresParentheses) { append(")") } append(".apply {}") } } /** * convert `apply(::foo)` to `apply { foo(this) }` if necessary */ private fun convertValueParametersToLambdaArgument(applyExpression: KtCallExpression) { if (applyExpression.lambdaArguments.isEmpty()) { val referenceExpression = applyExpression .valueArguments .singleOrNull() ?.getArgumentExpression() ?.let { it as? KtCallableReferenceExpression } ?.callableReference referenceExpression?.let { applyExpression.node.run { treeParent.replaceChild( this, kotlinParser.createNode( """ |apply { | ${referenceExpression.getReferencedName()}(this) |} """.trimMargin() ) ) } } ?: run { // valid code should always have apply with either lambdaArguments or valueArguments log.warn { "apply with unexpected parameters: ${applyExpression.text}" } } } } companion object { private val log = KotlinLogging.logger {} const val NAME_ID = "class-compact-initialization" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter6/classes/DataClassesRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter6.classes import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.USE_DATA_CLASS import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.* import org.jetbrains.kotlin.KtNodeTypes.BLOCK import org.jetbrains.kotlin.KtNodeTypes.CLASS import org.jetbrains.kotlin.KtNodeTypes.CLASS_BODY import org.jetbrains.kotlin.KtNodeTypes.CLASS_INITIALIZER import org.jetbrains.kotlin.KtNodeTypes.FUN import org.jetbrains.kotlin.KtNodeTypes.MODIFIER_LIST import org.jetbrains.kotlin.KtNodeTypes.PRIMARY_CONSTRUCTOR import org.jetbrains.kotlin.KtNodeTypes.PROPERTY import org.jetbrains.kotlin.KtNodeTypes.PROPERTY_ACCESSOR import org.jetbrains.kotlin.KtNodeTypes.REFERENCE_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.SUPER_TYPE_LIST import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.lexer.KtTokens.OPEN_KEYWORD import org.jetbrains.kotlin.psi.KtClass import org.jetbrains.kotlin.psi.KtClassBody import org.jetbrains.kotlin.psi.KtExpression import org.jetbrains.kotlin.psi.KtPrimaryConstructor import org.jetbrains.kotlin.psi.psiUtil.isAbstract /** * This rule checks if class can be made as data class */ class DataClassesRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(USE_DATA_CLASS) ) { override fun logic(node: ASTNode) { if (node.elementType == CLASS) { handleClass(node) } } private fun handleClass(node: ASTNode) { if ((node.psi as KtClass).isDefinitelyNotDataClass()) { return } if (node.canBeDataClass()) { raiseWarn(node) } } // fixme: Need to know types of vars and props to create data class private fun raiseWarn(node: ASTNode) { USE_DATA_CLASS.warn(configRules, emitWarn, "${(node.psi as KtClass).name}", node.startOffset, node) } @Suppress( "UnsafeCallOnNullableType", "FUNCTION_BOOLEAN_PREFIX", "ComplexMethod" ) private fun ASTNode.canBeDataClass(): Boolean { val isNotPropertyInClassBody = findChildByType(CLASS_BODY)?.let { (it.psi as KtClassBody).properties.isEmpty() } ?: true val constructorParametersNames: MutableList = mutableListOf() val hasPropertyInConstructor = findChildByType(PRIMARY_CONSTRUCTOR) ?.let { constructor -> (constructor.psi as KtPrimaryConstructor) .valueParameters .onEach { if (!it.hasValOrVar()) { constructorParametersNames.add(it.name!!) } } .run { isNotEmpty() && all { it.hasValOrVar() } } } ?: false if (isNotPropertyInClassBody && !hasPropertyInConstructor) { return false } // if parameter of the primary constructor is used in init block then it is hard to refactor this class to data class if (constructorParametersNames.isNotEmpty()) { val initBlocks = findChildByType(CLASS_BODY)?.getAllChildrenWithType(CLASS_INITIALIZER) initBlocks?.forEach { init -> val refExpressions = init.findAllDescendantsWithSpecificType(REFERENCE_EXPRESSION) if (refExpressions.any { it.text in constructorParametersNames }) { return false } } } return hasAppropriateClassBody() } @Suppress("UnsafeCallOnNullableType") private fun ASTNode.hasAppropriateClassBody(): Boolean { val classBody = getFirstChildWithType(CLASS_BODY) if (hasChildOfType(MODIFIER_LIST)) { val list = getFirstChildWithType(MODIFIER_LIST)!! return list.getChildren(null) .none { it.elementType in badModifiers } && classBody?.getAllChildrenWithType(FUN) ?.isEmpty() ?: false && getFirstChildWithType(SUPER_TYPE_LIST) == null } return classBody?.getFirstChildWithType(FUN) == null && getFirstChildWithType(SUPER_TYPE_LIST) == null && // if there is any prop with logic in accessor then don't recommend to convert class to data class classBody?.let(::areGoodProps) ?: true } /** * Checks if any property with accessor contains logic in accessor */ private fun areGoodProps(node: ASTNode): Boolean { val propertiesWithAccessors = node.getAllChildrenWithType(PROPERTY).filter { it.hasChildOfType(PROPERTY_ACCESSOR) } if (propertiesWithAccessors.isNotEmpty()) { return propertiesWithAccessors.any { val accessors = it.getAllChildrenWithType(PROPERTY_ACCESSOR) areGoodAccessors(accessors) } } return true } /** * We do not exclude inner classes here as if they have no * methods, then we definitely can refactor the code and make them data classes. * We only exclude: value/inline classes, enums, annotations, interfaces, abstract classes, * sealed classes and data classes itself. For sure there will be other corner cases, * for example, simple classes in Spring marked with @Entity annotation. * For these classes we expect users to Suppress warning manually for each corner case. **/ private fun KtClass.isDefinitelyNotDataClass() = isValue() || isAnnotation() || isInterface() || isData() || isSealed() || isInline() || isAbstract() || isEnum() @Suppress("UnsafeCallOnNullableType", "PARAMETER_NAME_IN_OUTER_LAMBDA") private fun areGoodAccessors(accessors: List): Boolean { accessors.forEach { if (it.hasChildOfType(BLOCK)) { val block = it.getFirstChildWithType(BLOCK)!! return block .getChildren(null) .count { expr -> expr.psi is KtExpression } <= 1 } } return true } companion object { const val NAME_ID = "data-classes" private val badModifiers = listOf(OPEN_KEYWORD) } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter6/classes/InlineClassesRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter6.classes import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.common.config.rules.getCommonConfiguration import com.saveourtool.diktat.ruleset.constants.Warnings.INLINE_CLASS_CAN_BE_USED import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.getFirstChildWithType import com.saveourtool.diktat.ruleset.utils.hasChildOfType import org.jetbrains.kotlin.KtNodeTypes.CLASS import org.jetbrains.kotlin.KtNodeTypes.CONSTRUCTOR_CALLEE import org.jetbrains.kotlin.KtNodeTypes.MODIFIER_LIST import org.jetbrains.kotlin.KtNodeTypes.SUPER_TYPE_LIST import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.lexer.KtTokens.FINAL_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.INTERNAL_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.PRIVATE_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.PROTECTED_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.PUBLIC_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.VAR_KEYWORD import org.jetbrains.kotlin.psi.KtClass import org.jetbrains.kotlin.psi.psiUtil.children import org.jetbrains.kotlin.psi.psiUtil.visibilityModifierType /** * This rule checks if inline class can be used. */ class InlineClassesRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(INLINE_CLASS_CAN_BE_USED) ) { override fun logic(node: ASTNode) { val configuration = configRules.getCommonConfiguration() if (node.elementType == CLASS && !(node.psi as KtClass).isInterface() && configuration.kotlinVersion >= minKtVersion && configuration.kotlinVersion < maxKtVersion ) { handleClasses(node.psi as KtClass) } } private fun handleClasses(classPsi: KtClass) { // Fixme: In Kotlin 1.4.30 inline classes may be used with internal constructors. When it will be released need to check it if (hasValidProperties(classPsi) && !isExtendingClass(classPsi.node) && classPsi.node .getFirstChildWithType(MODIFIER_LIST) ?.getChildren(null) ?.all { it.elementType in goodModifiers } != false) { // Fixme: since it's an experimental feature we shouldn't do fixer INLINE_CLASS_CAN_BE_USED.warn(configRules, emitWarn, "class ${classPsi.name}", classPsi.node.startOffset, classPsi.node) } } private fun hasValidProperties(classPsi: KtClass): Boolean { if (classPsi.getProperties().size == 1 && !classPsi.hasExplicitPrimaryConstructor()) { return !classPsi.getProperties().single().isVar } else if (classPsi.getProperties().isEmpty() && classPsi.hasExplicitPrimaryConstructor()) { return classPsi.primaryConstructorParameters.size == 1 && !classPsi.primaryConstructorParameters .first() .node .hasChildOfType(VAR_KEYWORD) && classPsi.primaryConstructor ?.visibilityModifierType() ?.value ?.let { it == "public" } ?: true } return false } private fun isExtendingClass(node: ASTNode): Boolean = node .getFirstChildWithType(SUPER_TYPE_LIST) ?.children() ?.any { it.hasChildOfType(CONSTRUCTOR_CALLEE) } ?: false companion object { const val NAME_ID = "inline-classes" val minKtVersion = KotlinVersion(1, 3) val maxKtVersion = KotlinVersion(1, 5, 0) val goodModifiers = listOf(PUBLIC_KEYWORD, PRIVATE_KEYWORD, FINAL_KEYWORD, PROTECTED_KEYWORD, INTERNAL_KEYWORD) } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter6/classes/SingleConstructorRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter6.classes import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.SINGLE_CONSTRUCTOR_SHOULD_BE_PRIMARY import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.KotlinParser import com.saveourtool.diktat.ruleset.utils.findChildrenMatching import com.saveourtool.diktat.ruleset.utils.getAllChildrenWithType import com.saveourtool.diktat.ruleset.utils.getIdentifierName import com.saveourtool.diktat.ruleset.utils.hasChildOfType import com.saveourtool.diktat.ruleset.utils.isGoingAfter import com.saveourtool.diktat.ruleset.utils.nextCodeSibling import org.jetbrains.kotlin.KtNodeTypes.CLASS import org.jetbrains.kotlin.KtNodeTypes.CLASS_BODY import org.jetbrains.kotlin.KtNodeTypes.MODIFIER_LIST import org.jetbrains.kotlin.KtNodeTypes.PRIMARY_CONSTRUCTOR import org.jetbrains.kotlin.KtNodeTypes.SECONDARY_CONSTRUCTOR import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl import org.jetbrains.kotlin.kdoc.lexer.KDocTokens.KDOC import org.jetbrains.kotlin.lexer.KtTokens.BLOCK_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.EOL_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.WHITE_SPACE import org.jetbrains.kotlin.psi.KtBinaryExpression import org.jetbrains.kotlin.psi.KtClass import org.jetbrains.kotlin.psi.KtDotQualifiedExpression import org.jetbrains.kotlin.psi.KtExpression import org.jetbrains.kotlin.psi.KtNameReferenceExpression import org.jetbrains.kotlin.psi.KtParameter import org.jetbrains.kotlin.psi.KtProperty import org.jetbrains.kotlin.psi.KtSecondaryConstructor import org.jetbrains.kotlin.psi.KtThisExpression import org.jetbrains.kotlin.psi.psiUtil.asAssignment import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType /** * This rule ensures that if a class has a single constructor, this constructor is primary. * Secondary constructor is converted into primary, statements that are not assignments are moved into an `init` block. */ class SingleConstructorRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(SINGLE_CONSTRUCTOR_SHOULD_BE_PRIMARY) ) { private val kotlinParser by lazy { KotlinParser() } override fun logic(node: ASTNode) { if (node.elementType == CLASS) { handleClassConstructors(node) } } private fun handleClassConstructors(node: ASTNode) { if (!node.hasChildOfType(PRIMARY_CONSTRUCTOR)) { // class has no primary constructor, need to count secondary constructors node .findChildByType(CLASS_BODY) ?.getAllChildrenWithType(SECONDARY_CONSTRUCTOR) ?.singleOrNull() ?.let { secondaryCtor -> SINGLE_CONSTRUCTOR_SHOULD_BE_PRIMARY.warnAndFix( configRules, emitWarn, isFixMode, "in class <${node.getIdentifierName()?.text}>", node.startOffset, node ) { convertConstructorToPrimary(node, secondaryCtor) } } } } /** * This method does the following: * - Inside the single secondary constructor find all assignments. * - Some of assigned values will have `this` qualifier, they are definitely class properties. * - For other assigned variables that are not declared in the same scope we check if they are properties and whether they depend only on constructor parameters. * - Create primary constructor moving all properties that we collected. * - Create init block with other statements from the secondary constructor, including initialization of properties that require local variables or complex calls. * - Finally, remove the secondary constructor. */ @Suppress( "GENERIC_VARIABLE_WRONG_DECLARATION", "TOO_LONG_FUNCTION" ) private fun convertConstructorToPrimary(classNode: ASTNode, secondaryCtor: ASTNode) { val secondaryCtorArguments = (secondaryCtor.psi as KtSecondaryConstructor).valueParameters // split all statements into assignments and all other statements (including comments) val (assignments, otherStatements) = (secondaryCtor.psi as KtSecondaryConstructor) .bodyBlockExpression ?.statements ?.partition { it is KtBinaryExpression && it.asAssignment() != null } ?.run { first.map { it as KtBinaryExpression } to second } ?: (emptyList() to emptyList()) val comments = (secondaryCtor.psi as KtSecondaryConstructor) .bodyBlockExpression ?.findChildrenMatching { it.elementType == EOL_COMMENT || it.elementType == BLOCK_COMMENT || it.elementType == KDOC } ?.associate { it.text to it.nextCodeSibling() } ?.filterValues { it != null } val classProperties = (classNode.psi as KtClass).getProperties() val localProperties = secondaryCtor.psi.collectDescendantsOfType { it.isLocal } // find all references to class properties that are getting assigned in a constructor val assignmentsToReferences = assignments.associateWithAssignedReference(localProperties, classProperties) // Split all assignments into trivial (that are just assigned from a constructor parameter) and non-trivial. // Logic for non-trivial assignments should than be kept and moved into a dedicated `init` block. val (trivialAssignments, nonTrivialAssignments) = assignmentsToReferences .toList() .partition { (assignment, _) -> assignment.right.let { rhs -> rhs is KtNameReferenceExpression && rhs.getReferencedName() in secondaryCtorArguments.map { it.name } } } .let { it.first.toMap() to it.second.toMap() } // find corresponding properties' declarations val declarationsAssignedInCtor = trivialAssignments .mapNotNull { (_, reference) -> (classNode.psi as KtClass).getProperties() .firstOrNull { it.nameIdentifier?.text == reference.getReferencedName() } } .distinct() // future init body val expressions = (secondaryCtor.psi as KtSecondaryConstructor) .bodyBlockExpression ?.statements ?.map { it.text } ?.filter { expr -> expr in otherStatements.map { it.text } || expr in nonTrivialAssignments.keys.map { it.text } } ?: emptyList() classNode.convertSecondaryConstructorToPrimary(secondaryCtor, declarationsAssignedInCtor, nonTrivialAssignments, otherStatements, comments, expressions) } @Suppress("UnsafeCallOnNullableType") private fun List.associateWithAssignedReference(localProperties: List, classProperties: List) = associateWith { // non-null assert is safe because of predicate in partitioning it.asAssignment()!!.left!! } .filterValues { left -> // we keep only statements where property is referenced via this (like `this.foo = ...`) left is KtDotQualifiedExpression && left.receiverExpression is KtThisExpression && left.selectorExpression is KtNameReferenceExpression || // or directly (like `foo = ...`) left is KtNameReferenceExpression && localProperties.none { // check for shadowing left.node.isGoingAfter(it.node) && it.name == left.name } } .mapValues { (_, left) -> when (left) { is KtDotQualifiedExpression -> left.selectorExpression as KtNameReferenceExpression is KtNameReferenceExpression -> left else -> error("Unexpected psi class ${left::class} with text ${left.text}") } } .filterValues { left -> left.getReferencedName() in classProperties.mapNotNull { it.name } } @Suppress( "NestedBlockDepth", "GENERIC_VARIABLE_WRONG_DECLARATION", "TOO_LONG_FUNCTION", "TOO_MANY_PARAMETERS", "LongParameterList", ) private fun ASTNode.convertSecondaryConstructorToPrimary( secondaryCtor: ASTNode, declarationsAssignedInCtor: List, nonTrivialAssignments: Map, otherStatements: List, comments: Map?, initBody: List ) { require(elementType == CLASS) val localProperties = secondaryCtor.psi.collectDescendantsOfType { it.isLocal } // find all arguments that are not directly assigned into properties val nonTrivialSecondaryCtorParameters = getNonTrivialParameters(secondaryCtor, nonTrivialAssignments.keys, localProperties) val primaryCtorNode = createPrimaryCtor(secondaryCtor, declarationsAssignedInCtor, nonTrivialSecondaryCtorParameters) val newArgumentListOfSecondaryCtor: List = (secondaryCtor.psi as KtSecondaryConstructor) .valueParameters .filter { arg -> arg.name !in nonTrivialSecondaryCtorParameters.map { it.name } } // get rid of ctor arguments .filter { arg -> arg.name !in declarationsAssignedInCtor.map { it.name } } // get rid of ctor arguments .filter { arg -> initBody.any { expr -> arg.name.toString() in expr } } // get rid of parameters that do not appear in text if (newArgumentListOfSecondaryCtor.isNotEmpty()) { return } addChild(primaryCtorNode, findChildByType(CLASS_BODY)) declarationsAssignedInCtor.forEach { ktProperty -> ktProperty.node.run { treePrev.takeIf { it.elementType == WHITE_SPACE }?.let { treeParent.removeChild(it) } treeParent.removeChild(this) } } // adding comments to init body val initBodyWithComments = initBody.toMutableList() comments?.forEach { (comment, nextExpression) -> if (initBodyWithComments.indexOf(nextExpression?.text) != -1) { initBodyWithComments.add(initBodyWithComments.indexOf(nextExpression?.text), comment) } } if (otherStatements.isNotEmpty() || nonTrivialAssignments.isNotEmpty()) { findChildByType(CLASS_BODY)?.run { val classInitializer = kotlinParser.createNodeForInit( """|init { | ${initBodyWithComments.joinToString("\n")} |} """.trimMargin()) addChild(classInitializer, secondaryCtor) addChild(PsiWhiteSpaceImpl("\n"), secondaryCtor) } } secondaryCtor .run { treePrev.takeIf { it.elementType == WHITE_SPACE } ?: treeNext } .takeIf { it.elementType == WHITE_SPACE } ?.run { treeParent.removeChild(this) } findChildByType(CLASS_BODY)?.removeChild(secondaryCtor) } @Suppress("UnsafeCallOnNullableType") private fun getNonTrivialParameters(secondaryCtor: ASTNode, nonTrivialAssignments: Collection, localProperties: List ) = (secondaryCtor.psi as KtSecondaryConstructor) .valueParameters.run { val dependencies = nonTrivialAssignments .flatMap { it.left!!.collectDescendantsOfType() } .filterNot { ref -> localProperties.any { ref.node.isGoingAfter(it.node) && ref.getReferencedName() == it.name } } .map { it.getReferencedName() } filter { it.name in dependencies } } private fun createPrimaryCtor(secondaryCtor: ASTNode, declarationsAssignedInCtor: List, valueParameters: List ) = kotlinParser.createPrimaryConstructor( (secondaryCtor .findChildByType(MODIFIER_LIST) ?.text ?.plus(" constructor ") ?: "") + "(" + declarationsAssignedInCtor.run { joinToString( ", ", postfix = if (isNotEmpty() && valueParameters.isNotEmpty()) ", " else "" ) { it.text } } + valueParameters.joinToString(", ") { it.text } + ")" ) .node companion object { const val NAME_ID = "single-constructor" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter6/classes/SingleInitRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter6.classes import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.MULTIPLE_INIT_BLOCKS import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.findAllDescendantsWithSpecificType import com.saveourtool.diktat.ruleset.utils.getAllChildrenWithType import com.saveourtool.diktat.ruleset.utils.getIdentifierName import com.saveourtool.diktat.ruleset.utils.parent import org.jetbrains.kotlin.KtNodeTypes import org.jetbrains.kotlin.KtNodeTypes.BLOCK import org.jetbrains.kotlin.KtNodeTypes.CLASS_BODY import org.jetbrains.kotlin.KtNodeTypes.CLASS_INITIALIZER import org.jetbrains.kotlin.KtNodeTypes.PRIMARY_CONSTRUCTOR import org.jetbrains.kotlin.KtNodeTypes.PROPERTY import org.jetbrains.kotlin.KtNodeTypes.REFERENCE_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.VALUE_PARAMETER_LIST import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl import org.jetbrains.kotlin.lexer.KtTokens.EQ import org.jetbrains.kotlin.lexer.KtTokens.WHITE_SPACE import org.jetbrains.kotlin.psi.KtBlockExpression import org.jetbrains.kotlin.psi.KtNameReferenceExpression import org.jetbrains.kotlin.psi.KtParameter import org.jetbrains.kotlin.psi.KtProperty import org.jetbrains.kotlin.psi.psiUtil.asAssignment import org.jetbrains.kotlin.psi.psiUtil.children /** * The rule that checks whether a class has a single `init` block or multiple. Having multiple `init` blocks is a bad practice. */ class SingleInitRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(MULTIPLE_INIT_BLOCKS) ) { override fun logic(node: ASTNode) { when (node.elementType) { CLASS_BODY -> handleInitBlocks(node) else -> return } } private fun handleInitBlocks(node: ASTNode) { // merge init blocks if there are multiple node .children() .filter { it.elementType == CLASS_INITIALIZER } .toList() .takeIf { it.size > 1 } ?.let { initBlocks -> val className = node.treeParent.getIdentifierName()?.text MULTIPLE_INIT_BLOCKS.warnAndFix(configRules, emitWarn, isFixMode, "in class <$className> found ${initBlocks.size} `init` blocks", node.startOffset, node) { mergeInitBlocks(initBlocks) } } // move property assignments from init block to property declarations node.findChildByType(CLASS_INITIALIZER)?.let { initBlock -> val propertiesFromPrimaryConstructor = node .treeParent .findChildByType(PRIMARY_CONSTRUCTOR) ?.findChildByType(VALUE_PARAMETER_LIST) ?.children() ?.filter { it.elementType == KtNodeTypes.VALUE_PARAMETER } ?.map { it.psi as KtParameter } ?.map { it.name } ?.toList() val propertiesFromClassBody = node .children() .filter { it.elementType == PROPERTY } .toList() moveAssignmentsToProperties(propertiesFromClassBody, propertiesFromPrimaryConstructor, initBlock) } } private fun mergeInitBlocks(initBlocks: List) { val firstInitBlock = initBlocks.first() initBlocks.drop(1).forEach { initBlock -> firstInitBlock.findChildByType(BLOCK)?.run { val beforeNode = lastChildNode.treePrev.takeIf { it.elementType == WHITE_SPACE } ?: lastChildNode (initBlock.findChildByType(BLOCK)?.psi as? KtBlockExpression)?.statements?.forEach { addChild(PsiWhiteSpaceImpl("\n"), beforeNode) addChild(it.node.clone() as ASTNode, beforeNode) } } if (initBlock.treePrev.elementType == WHITE_SPACE && initBlock.treeNext.elementType == WHITE_SPACE) { initBlock.treeParent.removeChild(initBlock.treeNext) } initBlock.treeParent.removeChild(initBlock) } firstInitBlock.parent(CLASS_BODY)?.let(::removeEmptyBlocks) } @Suppress("UnsafeCallOnNullableType", "TOO_LONG_FUNCTION") private fun moveAssignmentsToProperties( propertiesFromClassBody: List, propertiesFromPrimaryConstructor: List?, initBlock: ASTNode ) { initBlock .findChildByType(BLOCK) ?.run { (psi as KtBlockExpression) .statements .mapNotNull { it.asAssignment() } .filter { it.left is KtNameReferenceExpression } .filter { statement -> statement.right?.node?.findAllDescendantsWithSpecificType(REFERENCE_EXPRESSION)?.all { arg -> propertiesFromClassBody.any { (it.psi as KtProperty).name == arg.text } || propertiesFromPrimaryConstructor?.any { it == arg.text } == true } ?: false } .groupBy { assignment -> val assignedRef = assignment.left as KtNameReferenceExpression propertiesFromClassBody.find { (it.psi as KtProperty).name == assignedRef.getReferencedName() } } .filterKeys { it != null } .mapKeys { (k, _) -> k as ASTNode } .filter { (property, assignments) -> !(property.psi as KtProperty).hasBody() && assignments.size == 1 } .takeIf { it.isNotEmpty() } ?.let { map -> MULTIPLE_INIT_BLOCKS.warnAndFix(configRules, emitWarn, isFixMode, "`init` block has assignments that can be moved to declarations", initBlock.startOffset, initBlock ) { map.forEach { (property, assignments) -> val assignment = assignments.single() property.addChild(PsiWhiteSpaceImpl(" "), null) property.addChild(LeafPsiElement(EQ, "="), null) property.addChild(PsiWhiteSpaceImpl(" "), null) property.addChild(assignment.right!!.node.clone() as ASTNode, null) assignment.node.run { if (treePrev.elementType == WHITE_SPACE) { treeParent.removeChild(treePrev) } treeParent.removeChild(this) } } } } } initBlock.parent(CLASS_BODY)?.let(::removeEmptyBlocks) } private fun removeEmptyBlocks(node: ASTNode) { node .getAllChildrenWithType(CLASS_INITIALIZER) .filter { (it.findChildByType(BLOCK)?.psi as KtBlockExpression?)?.statements?.isEmpty() ?: false } .forEach { it.treeParent.removeChild(it) } } companion object { const val NAME_ID = "multiple-init-block" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/chapter6/classes/StatelessClassesRule.kt ================================================ package com.saveourtool.diktat.ruleset.rules.chapter6.classes import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.OBJECT_IS_PREFERRED import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.utils.findAllDescendantsWithSpecificType import com.saveourtool.diktat.ruleset.utils.getAllChildrenWithType import com.saveourtool.diktat.ruleset.utils.getFirstChildWithType import com.saveourtool.diktat.ruleset.utils.hasChildOfType import org.jetbrains.kotlin.KtNodeTypes.CLASS import org.jetbrains.kotlin.KtNodeTypes.FUN import org.jetbrains.kotlin.KtNodeTypes.OBJECT_DECLARATION import org.jetbrains.kotlin.KtNodeTypes.SUPER_TYPE_ENTRY import org.jetbrains.kotlin.KtNodeTypes.SUPER_TYPE_LIST import org.jetbrains.kotlin.com.intellij.lang.ASTFactory import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.lexer.KtTokens.CLASS_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.IDENTIFIER import org.jetbrains.kotlin.lexer.KtTokens.INTERFACE_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.OBJECT_KEYWORD import org.jetbrains.kotlin.psi.KtClass import org.jetbrains.kotlin.psi.psiUtil.children import org.jetbrains.kotlin.psi.stubs.elements.KtFileElementType /** * This rule checks if class is stateless and if so changes it to object. */ class StatelessClassesRule(configRules: List) : DiktatRule( NAME_ID, configRules, listOf(OBJECT_IS_PREFERRED) ) { override fun logic(node: ASTNode) { // Fixme: We should find interfaces in all project and then check them if (node.elementType == KtFileElementType.INSTANCE) { val interfacesNodes = node .findAllDescendantsWithSpecificType(CLASS) .filter { it.hasChildOfType(INTERFACE_KEYWORD) } node .findAllDescendantsWithSpecificType(CLASS) .filterNot { it.hasChildOfType(INTERFACE_KEYWORD) } .forEach { handleClass(it, interfacesNodes) } } } @Suppress("UnsafeCallOnNullableType") private fun handleClass(node: ASTNode, interfaces: List) { if (isClassExtendsValidInterface(node, interfaces) && isStatelessClass(node)) { OBJECT_IS_PREFERRED.warnAndFix(configRules, emitWarn, isFixMode, "class ${(node.psi as KtClass).name!!}", node.startOffset, node) { val newObjectNode = ASTFactory.composite(OBJECT_DECLARATION) val children = node.children().toList() node.treeParent.addChild(newObjectNode, node) children.forEach { if (it.elementType == CLASS_KEYWORD) { newObjectNode.addChild(ASTFactory.leaf(OBJECT_KEYWORD, "object")) } else { newObjectNode.addChild(it) } } node.treeParent.removeChild(node) } } } private fun isStatelessClass(node: ASTNode): Boolean { val properties = (node.psi as KtClass).getProperties() val functions = node.findAllDescendantsWithSpecificType(FUN) return properties.isEmpty() && functions.isNotEmpty() && !(node.psi as KtClass).hasExplicitPrimaryConstructor() } private fun isClassExtendsValidInterface(node: ASTNode, interfaces: List): Boolean = node.findChildByType(SUPER_TYPE_LIST) ?.getAllChildrenWithType(SUPER_TYPE_ENTRY) ?.isNotEmpty() ?.and(isClassInheritsStatelessInterface(node, interfaces)) ?: false @Suppress("UnsafeCallOnNullableType") private fun isClassInheritsStatelessInterface(node: ASTNode, interfaces: List): Boolean { val classInterfaces = node .findChildByType(SUPER_TYPE_LIST) ?.getAllChildrenWithType(SUPER_TYPE_ENTRY) val foundInterfaces = interfaces.filter { inter -> classInterfaces!!.any { it.text == inter.getFirstChildWithType(IDENTIFIER)!!.text } } return foundInterfaces.any { (it.psi as KtClass).getProperties().isEmpty() } } companion object { const val NAME_ID = "stateless-class" } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/utils/AstConstants.kt ================================================ @file:Suppress("FILE_NAME_MATCH_CLASS") package com.saveourtool.diktat.ruleset.utils import org.jetbrains.kotlin.KtNodeTypes.DO_WHILE import org.jetbrains.kotlin.KtNodeTypes.FOR import org.jetbrains.kotlin.KtNodeTypes.WHILE import org.jetbrains.kotlin.kdoc.lexer.KDocTokens.KDOC import org.jetbrains.kotlin.lexer.KtTokens.BLOCK_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.EOL_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.LBRACE import org.jetbrains.kotlin.lexer.KtTokens.RBRACE import org.jetbrains.kotlin.lexer.KtTokens.SEMICOLON import org.jetbrains.kotlin.lexer.KtTokens.WHITE_SPACE internal const val GET_PREFIX = "get" internal const val SET_PREFIX = "set" internal const val EMPTY_BLOCK_TEXT = "{}" /** * List of standard methods which do not need mandatory documentation */ internal val standardMethods = listOf("main", "equals", "hashCode", "toString", "clone", "finalize") /** * Mapping (value is negative infix) of infix methods that return Boolean */ internal val logicalInfixMethodMapping = mapOf( "==" to "!=", "!=" to "==", ">" to "<=", "<" to ">=", ">=" to "<", "<=" to ">", "in" to "!in", "!in" to "in", ) /** * List of infix methods that return Boolean */ internal val logicalInfixMethods = logicalInfixMethodMapping.keys + "xor" /** * List of element types present in empty code block `{ }` */ val emptyBlockList = listOf(LBRACE, WHITE_SPACE, SEMICOLON, RBRACE) val commentType = listOf(BLOCK_COMMENT, EOL_COMMENT, KDOC) val loopType = listOf(FOR, WHILE, DO_WHILE) val copyrightWords = setOf("copyright", "版权") internal val operatorMap = mapOf( "unaryPlus" to "+", "unaryMinus" to "-", "not" to "!", "plus" to "+", "minus" to "-", "times" to "*", "div" to "/", "rem" to "%", "mod" to "%", "rangeTo" to "..", "inc" to "++", "dec" to "--", "contains" to "in", "plusAssign" to "+=", "minusAssign" to "-=", "timesAssign" to "*=", "divAssign" to "/=", "modAssign" to "%=", ).mapValues { (_, value) -> listOf(value) } + mapOf( "equals" to listOf("==", "!="), "compareTo" to listOf("<", "<=", ">", ">="), ) internal val ignoreImports = setOf("invoke", "get", "set", "getValue", "setValue", "provideDelegate") /** * Enum that represents some standard platforms that can appear in kotlin code * @property packages beginnings of fully qualified names of packages belonging to a particular platform */ enum class StandardPlatforms(val packages: List) { ANDROID(listOf("android", "androidx", "com.android")), JAVA(listOf("java", "javax")), KOTLIN(listOf("kotlin", "kotlinx")), ; } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/utils/AstNodeUtils.kt ================================================ /** * Various utility methods to work with kotlin AST * FixMe: fix suppressed inspections on KDocs */ @file:Suppress( "FILE_NAME_MATCH_CLASS", "KDOC_WITHOUT_RETURN_TAG", "KDOC_WITHOUT_PARAM_TAG", "MatchingDeclarationName", ) package com.saveourtool.diktat.ruleset.utils import com.saveourtool.diktat.DIKTAT import com.saveourtool.diktat.api.isAnnotatedWithIgnoredAnnotation import com.saveourtool.diktat.common.config.rules.Rule import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.rules.chapter1.PackageNaming import org.jetbrains.kotlin.KtNodeTypes import org.jetbrains.kotlin.KtNodeTypes.ANNOTATED_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.ANNOTATION_ENTRY import org.jetbrains.kotlin.KtNodeTypes.BINARY_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.CALL_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.FILE_ANNOTATION_LIST import org.jetbrains.kotlin.KtNodeTypes.FUN import org.jetbrains.kotlin.KtNodeTypes.FUNCTION_LITERAL import org.jetbrains.kotlin.KtNodeTypes.IMPORT_LIST import org.jetbrains.kotlin.KtNodeTypes.LAMBDA_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.LONG_STRING_TEMPLATE_ENTRY import org.jetbrains.kotlin.KtNodeTypes.MODIFIER_LIST import org.jetbrains.kotlin.KtNodeTypes.OPERATION_REFERENCE import org.jetbrains.kotlin.KtNodeTypes.PARENTHESIZED import org.jetbrains.kotlin.KtNodeTypes.PROPERTY import org.jetbrains.kotlin.KtNodeTypes.REFERENCE_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.TYPE_PARAMETER_LIST import org.jetbrains.kotlin.KtNodeTypes.TYPE_REFERENCE import org.jetbrains.kotlin.KtNodeTypes.VALUE_PARAMETER_LIST import org.jetbrains.kotlin.builtins.PrimitiveType import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.openapi.util.Key import org.jetbrains.kotlin.com.intellij.psi.PsiElement import org.jetbrains.kotlin.com.intellij.psi.TokenType import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType import org.jetbrains.kotlin.com.intellij.psi.tree.TokenSet import org.jetbrains.kotlin.js.translate.declaration.hasCustomGetter import org.jetbrains.kotlin.js.translate.declaration.hasCustomSetter import org.jetbrains.kotlin.kdoc.lexer.KDocTokens.KDOC import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.lexer.KtTokens.ANDAND import org.jetbrains.kotlin.lexer.KtTokens.BLOCK_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.CONST_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.DOT import org.jetbrains.kotlin.lexer.KtTokens.ELVIS import org.jetbrains.kotlin.lexer.KtTokens.EOL_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.EQ import org.jetbrains.kotlin.lexer.KtTokens.IDENTIFIER import org.jetbrains.kotlin.lexer.KtTokens.INTERNAL_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.LATEINIT_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.LBRACE import org.jetbrains.kotlin.lexer.KtTokens.OROR import org.jetbrains.kotlin.lexer.KtTokens.OVERRIDE_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.PRIVATE_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.PROTECTED_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.PUBLIC_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.SAFE_ACCESS import org.jetbrains.kotlin.lexer.KtTokens.WHITE_SPACE import org.jetbrains.kotlin.psi.KtAnnotationEntry import org.jetbrains.kotlin.psi.KtClass import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtIfExpression import org.jetbrains.kotlin.psi.KtNullableType import org.jetbrains.kotlin.psi.KtParameterList import org.jetbrains.kotlin.psi.KtProperty import org.jetbrains.kotlin.psi.KtTypeReference import org.jetbrains.kotlin.psi.KtUserType import org.jetbrains.kotlin.psi.psiUtil.children import org.jetbrains.kotlin.psi.psiUtil.getChildOfType import org.jetbrains.kotlin.psi.psiUtil.getChildrenOfType import org.jetbrains.kotlin.psi.psiUtil.isPrivate import org.jetbrains.kotlin.psi.psiUtil.parents import org.jetbrains.kotlin.psi.psiUtil.siblings import org.jetbrains.kotlin.psi.stubs.elements.KtFileElementType import java.util.Locale /** * AST node visitor which accepts the node and, optionally, returns a value. */ typealias AstNodeVisitor = (node: ASTNode) -> T /** * The predicate which accepts a node and returns a `boolean` value. */ typealias AstNodePredicate = AstNodeVisitor /** * A class that represents result of nodes swapping. [oldNodes] should always have same size as [newNodes] * * @property oldNodes nodes that were to be moved * @property newNodes nodes that have been moved */ data class ReplacementResult(val oldNodes: List, val newNodes: List) { init { require(oldNodes.size == newNodes.size) } } /** * @return the highest parent node of the tree */ fun ASTNode.getRootNode() = if (isRoot()) this else parents().last() /** * Checks whether [this] node's text has length in range [range] * * @param range an [IntRange] for text * @return boolean result */ fun ASTNode.isTextLengthInRange(range: IntRange): Boolean = this.textLength in range /** * getting first child name with IDENTIFIER type * * @return node with type [org.jetbrains.kotlin.KtNodeTypes.IDENTIFIER] or null if it is not present */ fun ASTNode.getIdentifierName(): ASTNode? = this.getFirstChildWithType(IDENTIFIER) /** * getting first child name with TYPE_PARAMETER_LIST type * * @return a node with type TYPE_PARAMETER_LIST or null if it is not present */ fun ASTNode.getTypeParameterList(): ASTNode? = this.getFirstChildWithType(TYPE_PARAMETER_LIST) /** * @return true if this node contains no error elements, false otherwise */ fun ASTNode.isCorrect() = this.findAllDescendantsWithSpecificType(TokenType.ERROR_ELEMENT).isEmpty() /** * obviously returns list with children that match particular element type * * @param elementType the [IElementType] to search * @return list of nodes */ fun ASTNode.getAllChildrenWithType(elementType: IElementType): List = this.getChildren(null).filter { it.elementType == elementType } /** * Generates a sequence of this ASTNode's children in reversed order * * @return a reversed sequence of children */ fun ASTNode.reversedChildren(): Sequence = sequence { var node = lastChildNode while (node != null) { yield(node) node = node.treePrev } } /** * Replaces `this` node's child [beforeNode] of type [WHITE_SPACE] with the node with specified [text] * * @param beforeNode a node to replace * @param text a text (white space characters only) for the new node */ fun ASTNode.replaceWhiteSpaceText(beforeNode: ASTNode, text: String) { require(beforeNode.elementType == WHITE_SPACE) this.addChild(PsiWhiteSpaceImpl(text), beforeNode) this.removeChild(beforeNode) } /** * obviously returns first child that match particular element type * * @param elementType an [IElementType * @return a node or null if it was not found */ fun ASTNode.getFirstChildWithType(elementType: IElementType): ASTNode? = this.findChildByType(elementType) /** * Checks if a function is anonymous * throws exception if the node type is different from FUN */ fun ASTNode.isAnonymousFunction(): Boolean { require(this.elementType == FUN) return this.getIdentifierName() == null } /** * Checks if the function has boolean return type * * @return true if the function has boolean return type */ fun ASTNode.hasBooleanReturnType(): Boolean { val functionReturnType = this.findChildAfter(VALUE_PARAMETER_LIST, KtNodeTypes.TYPE_REFERENCE)?.text return functionReturnType != null && functionReturnType == PrimitiveType.BOOLEAN.typeName.asString() } /** * Checks if the function is an operator function * * @return true if the function is an operator function */ fun ASTNode.isOperatorFun(): Boolean { val modifierListNode = this.findChildByType(MODIFIER_LIST) return modifierListNode?.hasChildMatching { it.elementType == KtTokens.OPERATOR_KEYWORD } ?: false } /** * Checks if the symbols in this node are at the end of line */ fun ASTNode.isEol() = parent(false) { it.treeNext != null }?.isFollowedByNewline() ?: true /** * Checks if there is a newline after symbol corresponding to this element. We can't always check only this node itself, because * some nodes are placed not on the same level as white spaces, e.g. operators like [ElementType.ANDAND] are children of [ElementType.OPERATION_REFERENCE]. * Same is true also for semicolons in some cases. * Therefore, to check if they are followed by newline we need to check their parents. */ @Suppress("PARAMETER_NAME_IN_OUTER_LAMBDA") fun ASTNode.isFollowedByNewline() = parent(false) { it.treeNext != null }?.let { val probablyWhitespace = it.treeNext it.isFollowedByNewlineCheck() || (probablyWhitespace.elementType == WHITE_SPACE && probablyWhitespace.treeNext.run { elementType == EOL_COMMENT && isFollowedByNewlineCheck() }) } ?: false /** * This function is similar to isFollowedByNewline(), but there may be a comment after the node */ fun ASTNode.isFollowedByNewlineWithComment() = parent(false) { it.treeNext != null } ?.treeNext?.run { when (elementType) { WHITE_SPACE -> text.contains("\n") EOL_COMMENT, BLOCK_COMMENT, KDOC -> isFollowedByNewline() else -> false } || parent(false) { it.treeNext != null }?.let { it.treeNext.elementType == EOL_COMMENT && it.treeNext.isFollowedByNewline() } ?: false } ?: false /** * Checks if there is a newline before this element. See [isFollowedByNewline] for motivation on parents check. * Or if there is nothing before, it cheks, that there are empty imports and package before (Every FILE node has children of type IMPORT_LIST and PACKAGE) */ fun ASTNode.isBeginByNewline() = parent(false) { it.treePrev != null }?.let { it.treePrev.elementType == WHITE_SPACE && it.treePrev.text.contains("\n") || (it.treePrev.elementType == IMPORT_LIST && it.treePrev.isLeaf() && it.treePrev.treePrev.isLeaf()) } ?: false /** * Checks if there is a newline before this element or before comment before. See [isBeginByNewline] for motivation on parents check. */ fun ASTNode.isBeginNewLineWithComment() = isBeginByNewline() || siblings(forward = false).takeWhile { !it.textContains('\n') }.toList().run { all { it.isWhiteSpace() || it.isPartOfComment() } && isNotEmpty() } /** * checks if the node has corresponding child with elementTyp */ fun ASTNode.hasChildOfType(elementType: IElementType) = this.getFirstChildWithType(elementType) != null /** * Checks whether the node has at least one child of at least one of [elementType] * * @param elementType vararg of [IElementType] */ fun ASTNode.hasAnyChildOfTypes(vararg elementType: IElementType) = elementType.any { this.hasChildOfType(it) } /** * checks if node has parent of type */ fun ASTNode.hasParent(type: IElementType) = parent(type) != null /** * check if node's text is empty (contains only left and right braces) * check text because some nodes have empty BLOCK element inside (lambda) */ fun ASTNode?.isBlockEmpty() = this?.let { if (this.elementType == KtNodeTypes.WHEN) { val firstIndex = this.text.indexOf("{") this.text.substring(firstIndex - 1).replace("\\s+".toRegex(), "") == EMPTY_BLOCK_TEXT } else { this.text.replace("\\s+".toRegex(), "") == EMPTY_BLOCK_TEXT } } ?: true /** * Method that is trying to find and return child of this node, which * 1) stands before the node with type @beforeThisNodeType * 2) has type @childNodeType * 2) is closest to node with @beforeThisNodeType (i.e. is first in reversed list of children, starting at @beforeThisNodeType) */ fun ASTNode.findChildBefore(beforeThisNodeType: IElementType, childNodeType: IElementType): ASTNode? { val anchorNode = getChildren(null) .find { it.elementType == beforeThisNodeType } getChildren(null) .toList() .let { children -> anchorNode?.run { children.subList(0, children.indexOf(anchorNode)) } ?: children } .reversed() .find { it.elementType == childNodeType } ?.let { return it } return null } /** * method that is trying to find and return FIRST node that matches these conditions: * 1) it is one of children of "this" * 2) it stands in the list of children AFTER the node with type @afterThisNodeType * 3) it has type @childNodeType */ fun ASTNode.findChildAfter(afterThisNodeType: IElementType, childNodeType: IElementType): ASTNode? { var foundAnchorNode = false getChildren(null).forEach { if (foundAnchorNode && it.elementType == childNodeType) { // if we have already found previous node and type matches - then can return child return it } if (it.elementType == afterThisNodeType) { // found the node that is used as anchor and we are trying to find // a node with IElementType that stands after this anchor node foundAnchorNode = true } } return null } /** * method that traverses previous nodes until it finds needed node or it finds stop node * * @return ASTNode? */ fun ASTNode.prevNodeUntilNode(stopNodeType: IElementType, checkNodeType: IElementType): ASTNode? = siblings(false).takeWhile { it.elementType != stopNodeType }.find { it.elementType == checkNodeType } /** * Returns all siblings of [this] node * * @param withSelf whether [this] node is included in the result * @return list of siblings */ fun ASTNode.allSiblings(withSelf: Boolean = false): List = siblings(false).toList() + (if (withSelf) listOf(this) else emptyList()) + siblings(true) /** * Checks whether [this] node belongs to a companion object * * @return boolean result */ fun ASTNode.isNodeFromCompanionObject(): Boolean { val parent = this.treeParent parent?.let { val grandParent = parent.treeParent if (grandParent != null && grandParent.elementType == KtNodeTypes.OBJECT_DECLARATION) { grandParent.findLeafWithSpecificType(KtTokens.COMPANION_KEYWORD) ?.run { return true } } } return false } /** * Checks whether this node is a constant * * @return boolean result */ fun ASTNode.isConstant() = (this.isNodeFromFileLevel() || this.isNodeFromObject()) && this.isValProperty() && this.isConst() /** * Checks whether this node is an object * * @return boolean result */ fun ASTNode.isNodeFromObject(): Boolean { val parent = this.treeParent if (parent != null && parent.elementType == KtNodeTypes.CLASS_BODY) { val grandParent = parent.treeParent if (grandParent != null && grandParent.elementType == KtNodeTypes.OBJECT_DECLARATION) { return true } } return false } /** * Checks whether this node is declared on a file level * * @return boolean result */ fun ASTNode.isNodeFromFileLevel(): Boolean = this.treeParent.elementType == KtFileElementType.INSTANCE /** * Checks whether [this] node of type PROPERTY is `val` */ fun ASTNode.isValProperty() = this.getChildren(null) .any { it.elementType == KtTokens.VAL_KEYWORD } /** * Checks whether this node of type PROPERTY has `const` modifier */ fun ASTNode.isConst() = this.findLeafWithSpecificType(CONST_KEYWORD) != null /** * Checks whether this node of type PROPERTY has `lateinit` modifier */ fun ASTNode.isLateInit() = this.findLeafWithSpecificType(LATEINIT_KEYWORD) != null /** * @param modifier modifier to find in node */ fun ASTNode.hasModifier(modifier: IElementType) = this.findChildByType(MODIFIER_LIST)?.hasChildOfType(modifier) ?: false /** * Checks whether [this] node of type PROPERTY is `var` */ fun ASTNode.isVarProperty() = this.getChildren(null) .any { it.elementType == KtTokens.VAR_KEYWORD } /** * Replaces text of [this] node with lowercase text */ fun ASTNode.toLower() { (this as LeafPsiElement).rawReplaceWithText(this.text.lowercase(Locale.getDefault())) } /** * This util method does tree traversal and stores to the result all tree leaf node of particular type (elementType). * Recursively will visit each and every node and will get leaves of specific type. Those nodes will be added to the result. */ fun ASTNode.getAllLeafsWithSpecificType(elementType: IElementType, result: MutableList) { // if statements here have the only right order - don't change it if (this.isLeaf()) { if (this.elementType == elementType) { result.add(this) } } else { this.getChildren(null).forEach { it.getAllLeafsWithSpecificType(elementType, result) } } } /** * This util method does tree traversal and returns first node that matches specific type * This node isn't necessarily a leaf though method name implies it */ fun ASTNode.findLeafWithSpecificType(elementType: IElementType): ASTNode? { if (this.elementType == elementType) { return this } if (this.isLeaf()) { return null } return getChildren(null) .mapNotNull { it.findLeafWithSpecificType(elementType) } .firstOrNull() } /** * This method counts number of \n in node's text */ fun ASTNode.numNewLines() = text.count { it == '\n' } /** * This method performs tree traversal and returns all nodes with specific element type */ fun ASTNode.findAllDescendantsWithSpecificType(elementType: IElementType, withSelf: Boolean = true) = findAllNodesWithCondition(withSelf) { it.elementType == elementType } /** * This method performs tree traversal and returns all nodes which satisfy the condition */ fun ASTNode.findAllNodesWithCondition(withSelf: Boolean = true, excludeChildrenCondition: AstNodePredicate = { false }, condition: AstNodePredicate, ): List { val result = if (condition(this) && withSelf) mutableListOf(this) else mutableListOf() return result + this.getChildren(null) .filterNot { excludeChildrenCondition(it) } .flatMap { it.findAllNodesWithCondition(withSelf = true, excludeChildrenCondition, condition) } } /** * Check a node of type CLASS if it is an enum class */ fun ASTNode.isClassEnum(): Boolean = (psi as? KtClass)?.isEnum() ?: false /** * This method finds first parent node from the sequence of parents that has specified elementType */ fun ASTNode.findParentNodeWithSpecificType(elementType: IElementType) = this.parents().find { it.elementType == elementType } /** * Finds all children of optional type which match the predicate */ fun ASTNode.findChildrenMatching(elementType: IElementType? = null, predicate: (ASTNode) -> Boolean): List = getChildren(elementType?.let { TokenSet.create(it) }) .filter(predicate) /** * Check if this node has any children of optional type matching the predicate */ fun ASTNode.hasChildMatching(elementType: IElementType? = null, predicate: AstNodePredicate): Boolean = findChildrenMatching(elementType, predicate).isNotEmpty() /** * Converts this AST node and all its children to pretty string representation */ @Suppress("AVOID_NESTED_FUNCTIONS") fun ASTNode.prettyPrint(level: Int = 0, maxLevel: Int = -1): String { /** * AST operates with \n only, so we need to build the whole string representation and then change line separator */ fun ASTNode.doPrettyPrint(level: Int, maxLevel: Int): String { val result = StringBuilder("${this.elementType}: \"${this.text}\"").append('\n') if (maxLevel != 0) { this.getChildren(null).forEach { child -> result.append( "${"-".repeat(level + 1)} " + child.doPrettyPrint(level + 1, maxLevel - 1) ) } } return result.toString() } return doPrettyPrint(level, maxLevel).replace("\n", System.lineSeparator()) } /** * Checks if this modifier list corresponds to accessible outside entity. * The receiver should be an ASTNode with ElementType.MODIFIER_LIST, can be null if entity has no modifier list */ fun ASTNode?.isAccessibleOutside(): Boolean = this?.run { require(this.elementType == MODIFIER_LIST) this.hasAnyChildOfTypes(PUBLIC_KEYWORD, PROTECTED_KEYWORD, INTERNAL_KEYWORD) || !this.hasAnyChildOfTypes(PUBLIC_KEYWORD, INTERNAL_KEYWORD, PROTECTED_KEYWORD, PRIVATE_KEYWORD) } ?: true /** * Checks whether [this] node has a parent annotated with `@Suppress` with [warningName] * * @param warningName a name of the warning which is checked * @return boolean result */ fun ASTNode.isSuppressed( warningName: String, rule: Rule, configs: List ) = this.parent(false, hasAnySuppressorForInspection(warningName, rule, configs)) != null /** * Checks node has `override` modifier */ fun ASTNode.isOverridden(): Boolean = findChildByType(MODIFIER_LIST)?.findChildByType(OVERRIDE_KEYWORD) != null /** * removing all newlines in WHITE_SPACE node and replacing it to a one newline saving the initial indenting format */ fun ASTNode.leaveOnlyOneNewLine() = leaveExactlyNumNewLines(1) /** * removing all newlines in WHITE_SPACE node and replacing it to [num] newlines saving the initial indenting format */ fun ASTNode.leaveExactlyNumNewLines(num: Int) { require(this.elementType == WHITE_SPACE) (this as LeafPsiElement).rawReplaceWithText("${"\n".repeat(num)}${this.text.replace("\n", "")}") } /** * If [whiteSpaceNode] is not null and has type [WHITE_SPACE] and this [WHITE_SPACE] contains 1 line, prepend a line break to it's text. * If [whiteSpaceNode] is null or has`t type [WHITE_SPACE], insert a new node with a line break before [beforeNode] * * @param whiteSpaceNode a node that can possibly be modified * @param beforeNode a node before which a new WHITE_SPACE node will be inserted */ fun ASTNode.appendNewlineMergingWhiteSpace(whiteSpaceNode: ASTNode?, beforeNode: ASTNode?) { if (whiteSpaceNode != null && whiteSpaceNode.elementType == WHITE_SPACE) { if (whiteSpaceNode.text.lines().size == 1) { (whiteSpaceNode as LeafPsiElement).rawReplaceWithText("\n${whiteSpaceNode.text}") } } else { addChild(PsiWhiteSpaceImpl("\n"), beforeNode) } } /** * Appends newline after this node */ fun ASTNode.appendNewline() { val nextNode = this.treeNext if (nextNode.elementType == WHITE_SPACE) { (nextNode as LeafPsiElement).rawReplaceWithText("\n${nextNode.text}") } else { this.treeParent.addChild(PsiWhiteSpaceImpl("\n"), nextNode) } } /** * Changes any whitespace node on newline node * * @param whiteSpaceNode * @param beforeNode */ fun ASTNode.changeWhiteSpaceOnNewline(whiteSpaceNode: ASTNode?, beforeNode: ASTNode?) { if (whiteSpaceNode != null && whiteSpaceNode.elementType == WHITE_SPACE) { if (whiteSpaceNode.text.lines().size == 1) { (whiteSpaceNode as LeafPsiElement).rawReplaceWithText("\n") } } else { addChild(PsiWhiteSpaceImpl("\n"), beforeNode) } } /** * Transforms last line of this WHITE_SPACE to exactly [indent] spaces */ fun ASTNode.indentBy(indent: Int) { require(this.elementType == WHITE_SPACE) (this as LeafPsiElement).rawReplaceWithText(text.substringBeforeLast('\n') + "\n" + " ".repeat(indent)) } /** * @param beforeThisNode node before which childToMove will be placed. If null, childToMove will be appended after last child of this node. * @param withNextNode whether next node after childToMove should be moved too. In most cases it corresponds to moving * the node with newline. */ fun ASTNode.moveChildBefore( childToMove: ASTNode, beforeThisNode: ASTNode?, withNextNode: Boolean = false ): ReplacementResult { require(childToMove in children()) { "can only move child ($childToMove) of this node $this" } require(beforeThisNode == null || beforeThisNode in children()) { "can only place node before another child ($beforeThisNode) of this node $this" } val movedChild = childToMove.clone() as ASTNode val nextMovedChild = childToMove.treeNext?.takeIf { withNextNode }?.let { it.clone() as ASTNode } val nextOldChild = childToMove.treeNext.takeIf { withNextNode && it != null } addChild(movedChild, beforeThisNode) if (nextMovedChild != null && nextOldChild != null) { addChild(nextMovedChild, beforeThisNode) removeChild(nextOldChild) } removeChild(childToMove) return ReplacementResult(listOfNotNull(childToMove, nextOldChild), listOfNotNull(movedChild, nextMovedChild)) } /** * Finds a first `{` node inside [this] node * * @return a LBRACE node or `null` if it can't be found */ @Suppress("UnsafeCallOnNullableType", "FUNCTION_NAME_INCORRECT_CASE", "WRONG_NEWLINES") fun ASTNode.findLBrace(): ASTNode? = when (this.elementType) { KtNodeTypes.THEN, KtNodeTypes.ELSE, KtNodeTypes.FUN, KtNodeTypes.TRY, KtNodeTypes.CATCH, KtNodeTypes.FINALLY -> this.findChildByType(KtNodeTypes.BLOCK)?.findChildByType(LBRACE) KtNodeTypes.WHEN -> this.findChildByType(LBRACE)!! in loopType -> this.findChildByType(KtNodeTypes.BODY) ?.findChildByType(KtNodeTypes.BLOCK) ?.findChildByType(LBRACE) KtNodeTypes.CLASS, KtNodeTypes.OBJECT_DECLARATION -> this.findChildByType(KtNodeTypes.CLASS_BODY) ?.findChildByType(LBRACE) KtNodeTypes.FUNCTION_LITERAL -> this.findChildByType(LBRACE) else -> null } /** * Checks whether this node of type IF is a single line expression with single else, like `if (true) x else y` * * @return boolean result */ fun ASTNode.isSingleLineIfElse(): Boolean { val elseNode = (psi as KtIfExpression).`else`?.node val hasSingleElse = elseNode != null && elseNode.elementType != KtNodeTypes.IF return treeParent.elementType != KtNodeTypes.ELSE && hasSingleElse && text.lines().size == 1 } /** * Checks whether [child] is after [afterChild] among the children of [this] node * * @return boolean result */ fun ASTNode.isChildAfterAnother(child: ASTNode, afterChild: ASTNode): Boolean = getChildren(null).indexOf(child) > getChildren(null).indexOf(afterChild) /** * Checks whether [child] is after all nodes from [group] among the children of [this] node * * @return boolean result */ fun ASTNode.isChildAfterGroup(child: ASTNode, group: List): Boolean = getChildren(null).indexOf(child) > (group.map { getChildren(null).indexOf(it) }.maxOrNull() ?: 0) /** * Checks whether [child] is before [beforeChild] among the children of [this] node * * @return boolean result */ fun ASTNode.isChildBeforeAnother(child: ASTNode, beforeChild: ASTNode): Boolean = areChildrenBeforeGroup(listOf(child), listOf(beforeChild)) /** * Checks whether [child] is before all nodes is [group] among the children of [this] node * * @return boolean result */ fun ASTNode.isChildBeforeGroup(child: ASTNode, group: List): Boolean = areChildrenBeforeGroup(listOf(child), group) /** * Checks whether all nodes in [children] is before [beforeChild] among the children of [this] node * * @return boolean result */ fun ASTNode.areChildrenBeforeChild(children: List, beforeChild: ASTNode): Boolean = areChildrenBeforeGroup(children, listOf(beforeChild)) /** * Checks whether all nodes in [children] is before all nodes in [group] among the children of [this] node * * @return boolean result */ @Suppress("UnsafeCallOnNullableType") fun ASTNode.areChildrenBeforeGroup(children: List, group: List): Boolean { require(children.isNotEmpty() && group.isNotEmpty()) { "no sense to operate on empty lists" } return children.maxOf { getChildren(null).indexOf(it) } < group.minOf { getChildren(null).indexOf(it) } } /** * A function that rearranges nodes in a [this] list. * * @param getSiblingBlocks a function which returns nodes that should be before and after the current node * @param incorrectPositionHandler function that moves the current node with respect to node before which in should be placed */ @Suppress("TYPE_ALIAS") fun List.handleIncorrectOrder( getSiblingBlocks: ASTNode.() -> Pair, incorrectPositionHandler: (nodeToMove: ASTNode, beforeThisNode: ASTNode) -> Unit ) { forEach { astNode -> val (afterThisNode, beforeThisNode) = astNode.getSiblingBlocks() val isPositionIncorrect = (afterThisNode != null && !astNode.treeParent.isChildAfterAnother(astNode, afterThisNode)) || !astNode.treeParent.isChildBeforeAnother(astNode, beforeThisNode) if (isPositionIncorrect) { incorrectPositionHandler(astNode, beforeThisNode) } } } /** * This method returns text of this [ASTNode] plus text from it's siblings after last and until next newline, if present in siblings. * I.e., if this node occupies no more than a single line, this whole line or it's part will be returned. */ @Suppress("WRONG_NEWLINES") fun ASTNode.extractLineOfText(): String { val text: MutableList = mutableListOf() siblings(false) .map { it.text.split("\n") } .takeWhileInclusive { it.size <= 1 } .forEach { text.add(0, it.last()) } text.add(this.text) val nextNode = parent(false) { it.treeNext != null } ?: this nextNode.siblings(true) .map { it.text.split("\n") } .takeWhileInclusive { it.size <= 1 } .forEach { text.add(it.first()) } return text.joinToString(separator = "").trim() } /** * Checks node has `@Test` annotation */ fun ASTNode.hasTestAnnotation() = findChildByType(MODIFIER_LIST) ?.getAllChildrenWithType(ANNOTATION_ENTRY) ?.flatMap { it.findAllDescendantsWithSpecificType(KtNodeTypes.CONSTRUCTOR_CALLEE) } ?.any { it.findLeafWithSpecificType(KtTokens.IDENTIFIER)?.text == "Test" } ?: false /** * Return the number in the file of the last line of this node's text */ fun ASTNode.lastLineNumber() = getLineNumber() + text.count { it == '\n' } /** * copy-pasted method from ktlint to determine line and column number by offset */ fun ASTNode.calculateLineColByOffset() = buildPositionInTextLocator(text) /** * Return all nodes located at the specific line * * @param line * @return corresponding list of nodes */ fun ASTNode.findAllNodesOnLine( line: Int ): List? { val rootNode = this.getRootNode() val fileLines = rootNode.text.lines() if (line <= 0 || line > fileLines.size) { return null } val positionByOffset = rootNode.calculateLineColByOffset() val lineOffset = this.findOffsetByLine(line, positionByOffset) return this.getAllNodesOnLine(lineOffset, line, positionByOffset).toList() } /** * Return all nodes located at the specific line satisfying [condition] * * @param line * @param condition * @return corresponding list of nodes */ fun ASTNode.findAllNodesWithConditionOnLine( line: Int, condition: AstNodePredicate ): List? = this.findAllNodesOnLine(line)?.filter(condition) /** * Safely retrieves file name from user data of this node * * @return name of the file [this] node belongs to or null */ fun ASTNode.getFilePathSafely(): String? = run { val rootNode = getRootNode() @Suppress("DEPRECATION") Key.findKeyByName("FILE_PATH") ?.let { key -> rootNode.getUserData(key) ?.let { it as? String } } ?: run { // KtLint doesn't set file path for snippets // will take a file name from KtFile // it doesn't work for all cases since KtLint creates KtFile using a file name, not a file path // https://github.com/pinterest/ktlint/issues/1921 (rootNode.psi as? KtFile)?.virtualFilePath } } /** * Retrieves file name from user data of this node * * @return name of the file [this] node belongs to */ fun ASTNode.getFilePath(): String = requireNotNull(getFilePathSafely()) { "Failed to retrieve a file path for node $this (${this.javaClass.simpleName})" } /** * checks that this one node is placed after the other node in code (by comparing lines of code where nodes start) */ fun ASTNode.isGoingAfter(otherNode: ASTNode): Boolean { val thisLineNumber = this.getLineNumber() val otherLineNumber = otherNode.getLineNumber() return (thisLineNumber > otherLineNumber) } /** * check that node has binary expression with `EQ` */ fun ASTNode.hasEqBinaryExpression(): Boolean = findChildByType(BINARY_EXPRESSION) ?.findChildByType(OPERATION_REFERENCE) ?.hasChildOfType(EQ) ?: false /** * Get line number, where this node's content starts. * * @return line number */ fun ASTNode.getLineNumber(): Int = calculateLineNumber() /** * Get node by taking children by types and ignore `PARENTHESIZED` * * @return child of type */ fun ASTNode.takeByChainOfTypes(vararg types: IElementType): ASTNode? { var node: ASTNode? = this types.forEach { while (node?.hasChildOfType(PARENTHESIZED) == true) { node = node?.findChildByType(PARENTHESIZED) } node = node?.findChildByType(it) } return node } /** * @return whether this node is a dot (`.`, `.?`) before a function call or a * property reference. * @since 1.2.4 */ fun ASTNode.isDotBeforeCallOrReference(): Boolean = elementType in sequenceOf(DOT, SAFE_ACCESS) && treeNext.elementType in sequenceOf(CALL_EXPRESSION, REFERENCE_EXPRESSION) /** * @return whether this node is an _Elvis_ operation reference (i.e. an * [OPERATION_REFERENCE] which holds [ELVIS] is its only child). * @see OPERATION_REFERENCE * @see ELVIS * @since 1.2.4 */ fun ASTNode.isElvisOperationReference(): Boolean = treeParent.elementType == BINARY_EXPRESSION && elementType == OPERATION_REFERENCE && run { val children = children().toList() children.size == 1 && children[0].elementType == ELVIS } /** * @return whether this node is a whitespace or a comment. * @since 1.2.4 */ fun ASTNode.isWhiteSpaceOrComment(): Boolean = isWhiteSpace() || elementType in commentType /** * @return whether this node is a boolean expression (i.e. a [BINARY_EXPRESSION] * holding an [OPERATION_REFERENCE] which, in turn, holds either [ANDAND] or * [OROR] is its only child). * @see BINARY_EXPRESSION * @see OPERATION_REFERENCE * @see ANDAND * @see OROR * @since 1.2.4 */ @Suppress( "MAGIC_NUMBER", "MagicNumber", ) fun ASTNode.isBooleanExpression(): Boolean = elementType == BINARY_EXPRESSION && run { val operationAndArgs = children().filterNot(ASTNode::isWhiteSpaceOrComment).toList() operationAndArgs.size == 3 && run { val operationReference = operationAndArgs[1] operationReference.elementType == OPERATION_REFERENCE && run { val operations = operationReference.children().toList() operations.size == 1 && operations[0].elementType in sequenceOf(ANDAND, OROR) } } } /** * Before _KtLint_ **0.48**, this extension used to reside in the * `com.pinterest.ktlint.core.ast` package. * Because _KtLint_ **0.47** has changed the way syntax nodes are traversed (see * the diagrams [here](https://github.com/saveourtool/diktat/issues/1538)), the * codebase of _KtLint_ no longer needs it: in most cases, it's sufficient to * invoke * * ```kotlin * visitor(this) * ``` * * and rely on _KtLint_ to invoke the same visitor (which is usually a `Rule`) * on this node's children. * Still, _Diktat_ sometimes needs exactly this old behaviour. * * @param visitor the visitor to recursively traverse this node as well as its * children. * @since 1.2.5 */ fun ASTNode.visit(visitor: AstNodeVisitor) { visitor(this) @Suppress("NULLABLE_PROPERTY_TYPE") val filter: TokenSet? = null getChildren(filter).forEach { child -> child.visit(visitor) } } /** * @return whether this PSI element is a long string template entry. * @since 1.2.4 */ fun PsiElement.isLongStringTemplateEntry(): Boolean = node.elementType == LONG_STRING_TEMPLATE_ENTRY /** * Checks that node has child PRIVATE_KEYWORD * * @return true if node has child PRIVATE_KEYWORD */ fun ASTNode.isPrivate(): Boolean = this.getFirstChildWithType(MODIFIER_LIST)?.getFirstChildWithType(PRIVATE_KEYWORD) != null /** * Checks that node has getter or setter * * @return true if node has getter or setter */ fun ASTNode.hasSetterOrGetter(): Boolean = this.getFirstChildWithType(KtNodeTypes.PROPERTY_ACCESSOR) != null /** * Checks if ASTNode has parameter `it` * * @return true if this ASTNode has parameter `it` */ fun ASTNode.hasExplicitIt(): Boolean { require(elementType == LAMBDA_EXPRESSION) { "Method can be called only for lambda" } val parameterList = findChildByType(FUNCTION_LITERAL) ?.findChildByType(VALUE_PARAMETER_LIST) ?.psi as KtParameterList? return parameterList?.parameters ?.any { it.name == "it" } ?: false } private fun ASTNode.isFollowedByNewlineCheck() = this.treeNext.elementType == WHITE_SPACE && this.treeNext.text.contains("\n") private fun Sequence.takeWhileInclusive(pred: (T) -> Boolean): Sequence { var shouldContinue = true return takeWhile { val result = shouldContinue shouldContinue = pred(it) result } } private fun Collection.containSuppressWithName(name: String) = this.any { annotationEntry -> annotationEntry.shortName.toString() == (Suppress::class.simpleName) && (annotationEntry.valueArgumentList ?.arguments ?.any { annotation -> annotation.text.trim('"') == name } ?: false) } private fun ASTNode.findOffsetByLine(line: Int, positionByOffset: (Int) -> Pair): Int { val currentLine = this.getLineNumber() val currentOffset = this.startOffset var forwardDirection = true // We additionaly consider the offset and line for current node in aim to speed up the search // and not start any time from beginning of file, if possible // If current line is closer to the requested line, start search from it // otherwise search from the beginning of file var lineOffset = if (line < currentLine) { if ((currentLine - line) < line) { forwardDirection = false currentOffset } else { 0 } } else { currentOffset } while (positionByOffset(lineOffset).first != line) { if (forwardDirection) { lineOffset++ } else { lineOffset-- } } return lineOffset } @Suppress("UnsafeCallOnNullableType") private fun ASTNode.getAllNodesOnLine( lineOffset: Int, line: Int, positionByOffset: (Int) -> Pair ): MutableSet { val rootNode = this.getRootNode() var beginningOfLineOffset = lineOffset var endOfLineOffset = lineOffset while (beginningOfLineOffset > 0 && positionByOffset(beginningOfLineOffset - 1).first == line) { beginningOfLineOffset-- } while (endOfLineOffset < rootNode.text.length && positionByOffset(endOfLineOffset + 1).first == line) { endOfLineOffset++ } val nodesSet: MutableSet = mutableSetOf() var currentOffset = beginningOfLineOffset while (currentOffset <= endOfLineOffset) { nodesSet.add(rootNode.psi.findElementAt(currentOffset)!!.node) currentOffset++ } return nodesSet } /** * This function calculates line number instead of using cached values. * It should be used when AST could be previously mutated by auto fixers. */ private fun ASTNode.calculateLineNumber() = getRootNode() .text .lineSequence() // calculate offset for every line end, `+1` for `\n` which is trimmed in `lineSequence` .runningFold(0) { acc, line -> acc + line.length + 1 } .drop(1) .indexOfFirst { it > startOffset } .let { index -> require(index >= 0) { "Cannot calculate line number correctly, node's offset $startOffset is greater than file length ${getRootNode().textLength}" } index + 1 } /** * Gets list of property nodes * * @param node * @return list of property nodes */ fun getPropertyNodes(node: ASTNode?): List? = node?.treeParent?.getAllChildrenWithType(PROPERTY) /** * Checks node is located in file src/test/**/*Test.kt * * @param testAnchors names of test directories, e.g. "test", "jvmTest" */ fun isLocatedInTest(filePathParts: List, testAnchors: List) = filePathParts .takeIf { it.contains(PackageNaming.PACKAGE_PATH_ANCHOR) } ?.run { subList(lastIndexOf(PackageNaming.PACKAGE_PATH_ANCHOR), size) } ?.run { // e.g. src/test/ClassTest.kt, other files like src/test/Utils.kt are still checked testAnchors.any { contains(it) } && last().substringBeforeLast('.').endsWith("Test") } ?: false /** * Count number of lines in code block. * * @return the number of lines in a block of code. */ fun countCodeLines(node: ASTNode): Int { val copyNode = node.clone() as ASTNode copyNode.findAllNodesWithCondition { it.isPartOfComment() }.forEach { it.treeParent.removeChild(it) } val text = copyNode.text.lines().filter { it.isNotBlank() } return text.size } /** * Check that lambda contains `it`. * * @param lambdaNode ASTNode with type LAMBDA_EXPRESSION * @return true if `it` contains in lambdaNode.text */ fun hasItInLambda(lambdaNode: ASTNode): Boolean { val excludeChildrenCondition = { node: ASTNode -> node.elementType == LAMBDA_EXPRESSION && (hasNoParameters(node) || node.hasExplicitIt()) } val hasIt = lambdaNode .findAllNodesWithCondition(excludeChildrenCondition = excludeChildrenCondition) { it.elementType == REFERENCE_EXPRESSION } .map { it.text } .contains("it") return hasIt } /** * @param lambdaNode * @return true if lambdaNode has no parameters and has `it` in lambdaNode.text */ @Suppress("FUNCTION_BOOLEAN_PREFIX") fun doesLambdaContainIt(lambdaNode: ASTNode): Boolean { require(lambdaNode.elementType == LAMBDA_EXPRESSION) { "Method can be called only for lambda" } return hasNoParameters(lambdaNode) && hasItInLambda(lambdaNode) } /** * Checks that lambda has no parameters * * @param lambdaNode ASTNode with type LAMBDA_EXPRESSION * @return true if node has no parameters */ fun hasNoParameters(lambdaNode: ASTNode): Boolean { require(lambdaNode.elementType == LAMBDA_EXPRESSION) { "Method can be called only for lambda" } return null == lambdaNode .findChildByType(FUNCTION_LITERAL) ?.findChildByType(VALUE_PARAMETER_LIST) } /** * Checks that property node has pair backing field node or backing field node has pair property node. Nodes make a pair of property node and backing field node if they have: * 1. matching names (xyz -> _xyz) * 2. same type (but backing field can be nullable), * 3. backing field is private * 4. backing field should have no accessors/modifiers * 5. property should have at least an accessor, or a modifier, or both * * @param propertyNode if not null, trying to find matching backing field node * @param backingFieldNode if not null, trying to find matching property node * @return true if node has a pair */ @Suppress("CyclomaticComplexMethod") internal fun isPairPropertyBackingField(propertyNode: KtProperty?, backingFieldNode: KtProperty?): Boolean { val node = (propertyNode ?: backingFieldNode) val propertyList = (node?.parent?.getChildrenOfType()) ?: return false val nodeType: KtTypeReference? = node.getChildOfType() val nodeNullableType = nodeType?.getChildOfType() val nodeName = node.name val matchingNode = propertyList.find {pairNode -> val propertyType: KtTypeReference? = pairNode.getChildOfType() // check that property and backing field has same type val sameType = nodeType?.text == propertyType?.text // check that property USER_TYPE is same as backing field NULLABLE_TYPE val sameTypeWithNullable = propertyType?.getChildOfType()?.text == nodeNullableType?.getChildOfType()?.text // check matching names val propertyName = pairNode.name val matchingNames = propertyNode?.let { nodeName == propertyName?.drop(1) } ?: run { nodeName?.drop(1) == propertyName } val isPrivate = propertyNode?.let { pairNode.isPrivate() } ?: run { node.isPrivate() } val noSetterGetterBackingField = propertyNode?.let { !(pairNode.hasCustomSetter() || pairNode.hasCustomGetter()) } ?: run { !(node.hasCustomSetter() || node.hasCustomGetter()) } val hasSetterOrGetterProperty = propertyNode?.let { node.hasCustomSetter() || node.hasCustomGetter() } ?: run { pairNode.hasCustomSetter() || pairNode.hasCustomGetter() } matchingNames && (sameType || sameTypeWithNullable) && isPrivate && noSetterGetterBackingField && hasSetterOrGetterProperty } return matchingNode?.let { true } ?: false } private fun hasAnySuppressorForInspection( warningName: String, rule: Rule, configs: List ) = { node: ASTNode -> val annotationsForNode = if (node.elementType != KtFileElementType.INSTANCE) { node.findChildByType(MODIFIER_LIST) ?: node.findChildByType(ANNOTATED_EXPRESSION) } else { node.findChildByType(FILE_ANNOTATION_LIST) } ?.findAllDescendantsWithSpecificType(ANNOTATION_ENTRY) ?.map { it.psi as KtAnnotationEntry } ?: emptySet() val foundSuppress = annotationsForNode.containSuppressWithName(warningName) val foundIgnoredAnnotation = configs.isAnnotatedWithIgnoredAnnotation(rule, annotationsForNode.map { it.shortName.toString() }.toSet()) val isCompletelyIgnoredBlock = annotationsForNode.containSuppressWithName(DIKTAT) foundSuppress || foundIgnoredAnnotation || isCompletelyIgnoredBlock } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/utils/AstNodeUtilsFromKtLint.kt ================================================ /** * Various utility methods to work with kotlin AST * Copied from KtLint for backward compatibility */ package com.saveourtool.diktat.ruleset.utils import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.PsiComment import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.psi.psiUtil.parents import org.jetbrains.kotlin.psi.psiUtil.siblings import org.jetbrains.kotlin.psi.stubs.elements.KtFileElementType /** * @return true if current [ASTNode] is root */ fun ASTNode.isRoot(): Boolean = elementType == KtFileElementType.INSTANCE /** * @return true if current [ASTNode] is a leaf */ fun ASTNode.isLeaf(): Boolean = firstChildNode == null /** * @return true if this [ASTNode] is [KtTokens.WHITE_SPACE] and contains '\n' */ fun ASTNode?.isWhiteSpaceWithNewline(): Boolean = this?.elementType == KtTokens.WHITE_SPACE && this?.textContains('\n') == true /** * @param strict true if it doesn't need to check current [ASTNode] * @param predicate * @return first parent which meets [predicate] condition */ fun ASTNode.parent( strict: Boolean = true, predicate: (ASTNode) -> Boolean, ): ASTNode? = if (!strict && predicate(this)) { this } else { parents().firstOrNull(predicate) } /** * @param elementType [IElementType] * @param strict */ fun ASTNode.parent( elementType: IElementType, strict: Boolean = true, ): ASTNode? = parent(strict) { it.elementType == elementType } /** * @return true if current [ASTNode] is [KtTokens.WHITE_SPACE] */ fun ASTNode?.isWhiteSpace(): Boolean = this?.elementType == KtTokens.WHITE_SPACE /** * @return true if current [ASTNode] is not part of a comment */ fun ASTNode.isPartOfComment(): Boolean = parent(strict = false) { it.psi is PsiComment } != null /** * @return previous sibling [ASTNode] which is code */ fun ASTNode.prevCodeSibling(): ASTNode? = prevSibling { it.isCode() } /** * @param predicate * @return previous sibling [ASTNode] which matches [predicate] */ inline fun ASTNode.prevSibling(predicate: (ASTNode) -> Boolean = { true }): ASTNode? = siblings(false).firstOrNull(predicate) /** * @return next sibling [ASTNode] which is code */ fun ASTNode.nextCodeSibling(): ASTNode? = nextSibling { it.isCode() } /** * @param predicate * @return [ASTNode] next sibling which matches [predicate] */ inline fun ASTNode.nextSibling(predicate: (ASTNode) -> Boolean = { true }): ASTNode? = siblings(true).firstOrNull(predicate) /** * @return next [ASTNode] which is a code leaf */ fun ASTNode.nextCodeLeaf(): ASTNode? = generateSequence(nextLeaf()) { it.nextLeaf() } .firstOrNull { it.isCode() } /** * @param elementType * @return true if current [ASTNode] has a parent with [elementType] */ fun ASTNode.isPartOf(elementType: IElementType): Boolean = parent(elementType, strict = false) != null private fun ASTNode.nextLeaf(): ASTNode? = generateSequence(nextLeafAny()) { it.nextLeafAny() } .firstOrNull { it.textLength != 0 } private fun ASTNode.nextLeafAny(): ASTNode? = firstChildLeaf() ?: nextLeafStrict() private fun ASTNode.nextLeafStrict(): ASTNode? = treeNext?.firstChildLeafOrSelf() ?: treeParent?.nextLeafStrict() private fun ASTNode.firstChildLeafOrSelf(): ASTNode = firstChildLeaf() ?: this private fun ASTNode.firstChildLeaf(): ASTNode? = generateSequence(firstChildNode, ASTNode::getFirstChildNode) .lastOrNull() private fun ASTNode.isCode(): Boolean = !isWhiteSpace() && !isPartOfComment() ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/utils/FileUtils.kt ================================================ /** * Utility methods to work with file paths. */ package com.saveourtool.diktat.ruleset.utils internal const val SRC_DIRECTORY_NAME = "src" /** * Splits [this] string by file path separator. * * @return list of path parts */ fun String.splitPathToDirs(): List = this.replace("\\", "/") .replace("//", "/") .split("/") /** * Checks if [this] String is a name of a gradle kotlin script file by checking whether file extension equals 'gradle.kts' * * @return true if this is a gradle kotlin script file name, false otherwise */ fun String.isGradleScript() = endsWith("gradle.kts") ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/utils/FunctionAstNodeUtils.kt ================================================ /** * Various utility methods to work with AST nodes containing functions */ package com.saveourtool.diktat.ruleset.utils import org.jetbrains.kotlin.KtNodeTypes import org.jetbrains.kotlin.KtNodeTypes.BLOCK import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.psi.KtFunction /** * @return whether the function has any parameters */ fun ASTNode.hasParameters(): Boolean { checkNodeIsFun(this) val argList = this.argList() return argList != null && argList.hasChildOfType(KtNodeTypes.VALUE_PARAMETER) } /** * @return names of function parameters as Strings */ fun ASTNode.parameterNames(): Collection { checkNodeIsFun(this) return (psi as KtFunction).valueParameters.map { it.name } } /** * Returns list of lines of this function body, excluding opening and closing braces if they are on separate lines * * @return function body text as a list of strings */ fun ASTNode.getBodyLines(): List { checkNodeIsFun(this) return this.getFirstChildWithType(BLOCK)?.let { blockNode -> blockNode.text .lines() .let { if (it.first().matches("\\{\\s*".toRegex())) it.drop(1) else it } .let { if (it.last().matches("\\s*}".toRegex())) it.dropLast(1) else it } } ?: emptyList() } /** * @return if this function is getter or setter according to it's signature */ fun ASTNode.isGetterOrSetter(): Boolean { checkNodeIsFun(this) return getIdentifierName()?.let { functionName -> when { functionName.text.startsWith(SET_PREFIX) -> parameterNames().size == 1 functionName.text.startsWith(GET_PREFIX) -> parameterNames().isEmpty() else -> false } } ?: false } /** * @return whether this function is a standard method */ fun ASTNode.isStandardMethod() = also(::checkNodeIsFun) .getIdentifierName() ?.let { it.text in standardMethods } ?: false private fun ASTNode.argList(): ASTNode? { checkNodeIsFun(this) return this.getFirstChildWithType(KtNodeTypes.VALUE_PARAMETER_LIST) } private fun checkNodeIsFun(node: ASTNode) = require(node.elementType == KtNodeTypes.FUN) { "This utility method operates on nodes of type ElementType.FUN only" } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/utils/KdocUtils.kt ================================================ /** * Various utility methods to work with KDoc representation in AST */ package com.saveourtool.diktat.ruleset.utils import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.CompositeElement import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl import org.jetbrains.kotlin.kdoc.lexer.KDocTokens import org.jetbrains.kotlin.kdoc.parser.KDocElementTypes import org.jetbrains.kotlin.kdoc.parser.KDocKnownTag import org.jetbrains.kotlin.kdoc.psi.impl.KDocTag import org.jetbrains.kotlin.lexer.KtTokens.WHITE_SPACE /** * @return a list of [KDocTag]s from this KDoc node */ fun ASTNode.kDocTags(): List { require(this.elementType == KDocTokens.KDOC) { "kDoc tags can be retrieved only from KDOC node" } return this.getAllChildrenWithType(KDocElementTypes.KDOC_SECTION).flatMap { sectionNode -> sectionNode.getAllChildrenWithType(KDocElementTypes.KDOC_TAG) .map { its -> its.psi as KDocTag } } } /** * @param knownTag a tag to look for * @return whether this tag is present */ fun Iterable.hasKnownKdocTag(knownTag: KDocKnownTag): Boolean = this.any { it.knownTag == knownTag } /** * Checks for trailing newlines in tag's body. Handles cases, when there is no leading asterisk on an empty line: * ``` * * @param param * * * @return * ``` * as well as usual simple cases. * * @return true if there is a trailing newline */ fun KDocTag.hasTrailingNewlineInTagBody() = node.lastChildNode.isWhiteSpaceWithNewline() || node.reversedChildren() .takeWhile { it.elementType == WHITE_SPACE || it.elementType == KDocTokens.LEADING_ASTERISK } .firstOrNull { it.elementType == KDocTokens.LEADING_ASTERISK } ?.takeIf { it.treeNext == null || it.treeNext.elementType == WHITE_SPACE } != null /** * This method inserts a new tag into KDoc before specified another tag, aligning it with the rest of this KDoc * * @param beforeTag tag before which the new one will be placed * @param consumer lambda which should be used to fill new tag with data, accepts CompositeElement as an argument */ @Suppress("UnsafeCallOnNullableType") inline fun ASTNode.insertTagBefore( beforeTag: ASTNode?, consumer: CompositeElement.() -> Unit ) { require(this.elementType == KDocTokens.KDOC && this.hasChildOfType(KDocElementTypes.KDOC_SECTION)) { "kDoc tags can be inserted only into KDOC node" } val kdocSection = this.getFirstChildWithType(KDocElementTypes.KDOC_SECTION)!! val newTag = CompositeElement(KDocElementTypes.KDOC_TAG) val beforeTagLineStart = beforeTag?.prevSibling { it.elementType == WHITE_SPACE && it.treeNext?.elementType == KDocTokens.LEADING_ASTERISK } val indent = this .getFirstChildWithType(WHITE_SPACE) ?.text ?.split("\n") ?.last() ?: "" kdocSection.addChild(PsiWhiteSpaceImpl("\n$indent"), beforeTagLineStart) kdocSection.addChild(LeafPsiElement(KDocTokens.LEADING_ASTERISK, "*"), beforeTagLineStart) kdocSection.addChild(LeafPsiElement(KDocTokens.TEXT, " "), beforeTagLineStart) kdocSection.addChild(newTag, beforeTagLineStart) consumer(newTag) } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/utils/KotlinParseException.kt ================================================ package com.saveourtool.diktat.ruleset.utils /** * An [Exception] that can be thrown during parsing of kotlin code */ class KotlinParseException(message: String) : Exception(message) ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/utils/KotlinParser.kt ================================================ package com.saveourtool.diktat.ruleset.utils import org.jetbrains.kotlin.KtNodeTypes.BLOCK import org.jetbrains.kotlin.KtNodeTypes.IMPORT_LIST import org.jetbrains.kotlin.cli.common.environment.setIdeaIoUseFallback import org.jetbrains.kotlin.cli.common.messages.MessageCollector import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.mock.MockProject import org.jetbrains.kotlin.com.intellij.openapi.project.Project import org.jetbrains.kotlin.com.intellij.openapi.util.UserDataHolderBase import org.jetbrains.kotlin.com.intellij.pom.PomModel import org.jetbrains.kotlin.com.intellij.pom.PomModelAspect import org.jetbrains.kotlin.com.intellij.pom.PomTransaction import org.jetbrains.kotlin.com.intellij.pom.impl.PomTransactionBase import org.jetbrains.kotlin.com.intellij.pom.tree.TreeAspect import org.jetbrains.kotlin.com.intellij.psi.TokenType.ERROR_ELEMENT import org.jetbrains.kotlin.config.CommonConfigurationKeys import org.jetbrains.kotlin.config.CompilerConfiguration import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.lexer.KtTokens.IMPORT_KEYWORD import org.jetbrains.kotlin.psi.KtPsiFactory import org.jetbrains.kotlin.resolve.ImportPath import sun.reflect.ReflectionFactory /** * A class that wraps kotlin compiler's code parser and converts source code into AST */ class KotlinParser { private val project: Project by lazy { val compilerConfiguration = CompilerConfiguration() compilerConfiguration.put(CommonConfigurationKeys.MESSAGE_COLLECTOR_KEY, MessageCollector.NONE) // mute the output logging to process it themselves val pomModel: PomModel = object : UserDataHolderBase(), PomModel { override fun runTransaction(transaction: PomTransaction) { (transaction as PomTransactionBase).run() } @Suppress("UNCHECKED_CAST", "SpreadOperator") override fun getModelAspect(aspect: Class): T? { if (aspect == TreeAspect::class.java) { val constructor = ReflectionFactory.getReflectionFactory().newConstructorForSerialization( aspect, Any::class.java.getDeclaredConstructor(*arrayOfNulls>(0)) ) return constructor.newInstance() as T } return null } } // I don't really understand what's going on here, but thanks to this, you can use this node in the future val project = KotlinCoreEnvironment.createForProduction({}, compilerConfiguration, EnvironmentConfigFiles.JVM_CONFIG_FILES ).project // create project project as MockProject project.registerService(PomModel::class.java, pomModel) project } private val ktPsiFactory by lazy { KtPsiFactory(project, true) } /** * Set idea.io.use.nio2 in system property to true * fixme Maybe should check OS to don't change system property */ init { setIdeaIoUseFallback() } /** * @param text kotlin code * @param isPackage whether resulting node should be a package directive * @return [ASTNode] * @throws KotlinParseException if code is incorrect */ fun createNode(text: String, isPackage: Boolean = false) = makeNode(text, isPackage) ?: throw KotlinParseException("Text is not valid: [$text]") /** * @param text kotlin code * @return [ASTNode] which is secondary constructor * @throws KotlinParseException if code is incorrect */ @Suppress("SAY_NO_TO_VAR") fun createNodeForSecondaryConstructor(text: String): ASTNode { val node: ASTNode = ktPsiFactory .createSecondaryConstructor(text) .node if (!node.isCorrect()) { throw KotlinParseException("Text is not valid: [$text]") } return node } /** * @param text kotlin code * @return [ASTNode] which is init block * @throws KotlinParseException if code is incorrect */ @Suppress( "SAY_NO_TO_VAR", "UnsafeCallOnNullableType" ) fun createNodeForInit(text: String): ASTNode { val node: ASTNode = ktPsiFactory .createBlockCodeFragment(text, null) .node .findChildByType(BLOCK)!! .firstChildNode if (!node.isCorrect()) { throw KotlinParseException("Text is not valid: [$text]") } return node } /** * @param text kotlin code * @return [KtPrimaryConstructor] */ fun createPrimaryConstructor(text: String) = ktPsiFactory.createPrimaryConstructor(text) /** * This method create a node based on text. * * @param isPackage - flag to check if node will contains package. * If this flag is true, node's element type will be FILE. * Else, try to create node based on text. * If this node will contain ERROR_ELEMENT type children this mean that cannot create node based on this text */ @Suppress( "UnsafeCallOnNullableType", "TOO_LONG_FUNCTION", "SAY_NO_TO_VAR" ) private fun makeNode(text: String, isPackage: Boolean = false): ASTNode? { if (text.isEmpty()) { return null } if (text.trim().isEmpty()) { return ktPsiFactory.createWhiteSpace(text).node } var node: ASTNode = if (isPackage || isContainKdoc(text)) { ktPsiFactory.createFile(text).node } else if (text.contains(KtTokens.IMPORT_KEYWORD.value)) { val (imports, blockCode) = text.lines().partition { it.contains(KtTokens.IMPORT_KEYWORD.value) } when { blockCode.isNotEmpty() -> return null imports.size == 1 -> { val importText = ImportPath.fromString(text.substringAfter("$IMPORT_KEYWORD ")) ktPsiFactory.createImportDirective(importText).node } else -> ktPsiFactory .createBlockCodeFragment(text, null) .node .findChildByType(BLOCK)!! .findChildByType(ERROR_ELEMENT)!! .findChildByType(IMPORT_LIST)!! } } else { ktPsiFactory .createBlockCodeFragment(text, null) .node .findChildByType(BLOCK)!! } if (node.getChildren(null).size == 1) { node = node.firstChildNode } if (!node.isCorrect()) { node = ktPsiFactory.createFile(text).node if (!node.isCorrect()) { return null } } return node } private fun isContainKdoc(text: String) = text.lines().any { it.trim().startsWith("/**") } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/utils/PositionInTextLocator.kt ================================================ /** * Code to create fast mapping of text offset tol ine and column numbers * fixme: this code is copy-pasted from ktlint. Change it */ package com.saveourtool.diktat.ruleset.utils internal typealias LineAndColumn = Pair @Suppress( "MISSING_KDOC_ON_FUNCTION", "KDOC_WITHOUT_PARAM_TAG", "KDOC_WITHOUT_RETURN_TAG" ) private class SegmentTree(sortedArray: IntArray) { private val segments: List = sortedArray .dropLast(1) .mapIndexed { index: Int, element: Int -> Segment(element, sortedArray[index + 1] - 1) } init { require(sortedArray.size > 1) { "At least two data points are required" } sortedArray.reduce { current, next -> require(current <= next) { "Data points are not sorted (ASC)" } next } } fun get(index: Int): Segment = segments[index] fun indexOf(index: Int): Int = binarySearch(index, 0, segments.size - 1) private fun binarySearch( compareElement: Int, left: Int, right: Int ): Int = when { left > right -> -1 else -> { val index = left + (right - left) / 2 val midElement = segments[index] if (compareElement < midElement.left) { binarySearch(compareElement, left, index - 1) } else { if (midElement.right < compareElement) binarySearch(compareElement, index + 1, right) else index } } } } @Suppress("KDOC_NO_CONSTRUCTOR_PROPERTY") private data class Segment( val left: Int, val right: Int ) /** * Calculate position in text - line and column based on offset from the text start. * * @param text a piece of text * @return mapping function from offset to line and column number */ internal fun buildPositionInTextLocator(text: String): (offset: Int) -> LineAndColumn { val textLength = text.length val identifierArray: ArrayList = ArrayList() var endOfLineIndex = -1 do { identifierArray.add(endOfLineIndex + 1) endOfLineIndex = text.indexOf('\n', endOfLineIndex + 1) } while (endOfLineIndex != -1) identifierArray.add(textLength + if (identifierArray.last() == textLength) 1 else 0) val segmentTree = SegmentTree(identifierArray.toIntArray()) return { offset -> val line = segmentTree.indexOf(offset) if (line != -1) { val column = offset - segmentTree.get(line).left line + 1 to column + 1 } else { 1 to 1 } } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/utils/PsiUtils.kt ================================================ /** * Utility methods to work with PSI code representation */ package com.saveourtool.diktat.ruleset.utils import org.jetbrains.kotlin.psi.KtBinaryExpression import org.jetbrains.kotlin.psi.KtBlockExpression import org.jetbrains.kotlin.psi.KtCallExpression import org.jetbrains.kotlin.psi.KtConstantExpression import org.jetbrains.kotlin.psi.KtDotQualifiedExpression import org.jetbrains.kotlin.psi.KtExpression import org.jetbrains.kotlin.psi.KtNameReferenceExpression import org.jetbrains.kotlin.psi.KtProperty import org.jetbrains.kotlin.psi.KtReferenceExpression import org.jetbrains.kotlin.psi.KtStringTemplateExpression import org.jetbrains.kotlin.psi.psiUtil.getParentOfType import org.jetbrains.kotlin.psi.psiUtil.isAncestor import org.jetbrains.kotlin.psi.psiUtil.parents /** * Checks if this [KtExpression] contains only constant literals, strings with possibly constant expressions in templates, * method calls on literals. * * @return boolean result */ @Suppress("UnsafeCallOnNullableType", "FUNCTION_BOOLEAN_PREFIX") fun KtExpression.containsOnlyConstants(): Boolean = when (this) { is KtConstantExpression -> true is KtStringTemplateExpression -> entries.all { it.expression?.containsOnlyConstants() ?: true } // left and right are marked @Nullable @IfNotParsed, so it should be safe to `!!` is KtBinaryExpression -> left!!.containsOnlyConstants() && right!!.containsOnlyConstants() is KtDotQualifiedExpression -> receiverExpression.containsOnlyConstants() && (selectorExpression is KtReferenceExpression || ((selectorExpression as? KtCallExpression) ?.valueArgumentList ?.arguments ?.all { it.getArgumentExpression()!!.containsOnlyConstants() } ?: false) ) else -> false } /** * Get block inside which the property is declared. * Here we assume that property can be declared only in block, since declaration is not an expression in kotlin * and compiler prohibits things like `if (condition) val x = 0`. */ fun KtProperty.getDeclarationScope() = // FixMe: class body is missing here getParentOfType(true) /** * Method that tries to find a local property declaration with the same name as current [KtNameReferenceExpression] element * * @return [KtProperty] if it is found, null otherwise */ fun KtNameReferenceExpression.findLocalDeclaration(): KtProperty? = parents .mapNotNull { it as? KtBlockExpression } .mapNotNull { blockExpression -> blockExpression .statements .takeWhile { !it.isAncestor(this, true) } .mapNotNull { it as? KtProperty } .find { it.isLocal && it.hasInitializer() && it.name?.equals(getReferencedName()) ?: false } } .firstOrNull() /** * @return name of a function which is called in a [KtCallExpression] or null if it can't be found */ fun KtCallExpression.getFunctionName() = (calleeExpression as? KtNameReferenceExpression)?.getReferencedName() ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/utils/StringCaseUtils.kt ================================================ @file:Suppress("FILE_NAME_MATCH_CLASS", "MatchingDeclarationName") package com.saveourtool.diktat.ruleset.utils import com.google.common.base.CaseFormat import io.github.oshai.kotlinlogging.KotlinLogging import java.util.Locale private val log = KotlinLogging.logger {} /** * Available cases to name enum members * @property str */ enum class Style(val str: String) { PASCAL_CASE("PascalCase"), SNAKE_CASE("UPPER_SNAKE_CASE"), ; } /** * checking that string looks like: PascalCaseForClassName * * @return boolean result */ fun String.isPascalCase(): Boolean = this.matches("([A-Z][a-z0-9]+)+".toRegex()) /** * checking that string looks like: lowerCaseOfVariable * * @return boolean result */ fun String.isLowerCamelCase(): Boolean = this.matches("[a-z]([a-z0-9])*([A-Z][a-z0-9]+)*".toRegex()) /** * checking that string looks like: CORRECT_CASE_FOR_CONSTANTS * * @return boolean result */ fun String.isUpperSnakeCase(): Boolean = this.matches("(([A-Z0-9]+)_*)+[A-Z0-9]*".toRegex()) /** * checking that string looks like: lower_case_for_script_names * * @return boolean result */ fun String.isLowerSnakeCase(): Boolean = this.matches("(([a-z]+)_*)+[a-z0-9]*".toRegex()) /** * detecting the case of _this_ String and converting it to the right UpperSnakeCase (UPPER_UNDERSCORE) case * * @return converted string */ fun String.toUpperSnakeCase(): String { // PascalCase -> PASCAL_CASE if (this.isPascalCase()) { return CaseFormat.UPPER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, this) } // lower -> LOWER if (this.all { it.isLowerCase() }) { return this.uppercase(Locale.getDefault()) } // lowerCamel -> LOWER_CAMEL if (this.isLowerCamelCase()) { return CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, this) } // lower_snake -> LOWER_SNAKE if (this.isLowerSnakeCase()) { return CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_UNDERSCORE, this) } val idx = getFirstLetterOrDigit() if (idx != -1) { // any other format -> UPPER_SNAKE_CASE // [p]a[SC]a[_]l -> [P]A_[SC]_A_[L] return this[idx].uppercaseChar().toString() + convertUnknownCaseToUpperSnake(this.substring(idx + 1)) } log.error { "Not able to fix case format for: $this" } return this } /** * detecting the case of _this_ String and converting it to the right UpperSnakeCase (UPPER_UNDERSCORE) case * * @return converted string */ @Suppress("ForbiddenComment") fun String.toLowerCamelCase(): String { // PascalCase -> PASCAL_CASE if (this.isPascalCase()) { return CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_CAMEL, this) } // lower -> LOWER if (this.all { it.isUpperCase() }) { return this.lowercase(Locale.getDefault()) } // lowerCamel -> LOWER_CAMEL if (this.isUpperSnakeCase()) { return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, this) } // lower_snake -> LOWER_SNAKE if (this.isLowerSnakeCase()) { return CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, this) } val idx = getFirstLetterOrDigit() if (idx != -1) { // any other format -> camelCase // changing first letter to uppercase and replacing several uppercase letters in raw to lowercase: // example of change: [P]a[SC]a[_]l -> [p]a[Sc]a[L] // FixMe: there is some discussion on how lowerN_Case should be resolved: to lowerNcase or to lowernCase or lowerNCase (current version) return this[idx].lowercaseChar().toString() + convertUnknownCaseToCamel(this.substring(idx + 1), this[idx].isUpperCase()) } log.error { "Not able to fix case format for: $this" } return this } /** * detecting the case of _this_ String and converting it to the right PascalCase (UpperCamel) case * * @return converted string */ @Suppress("ForbiddenComment") fun String.toPascalCase(): String = when { all { it.isUpperCase() } -> { // all letters UPPER -> Upper this[0] + substring(1).lowercase(Locale.getDefault()) } all { it.isLowerCase() } -> { // all letters lower -> Lower this[0].uppercaseChar() + substring(1) } isUpperSnakeCase() -> { // lowerCamel -> LowerCamel CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, this) } isLowerSnakeCase() -> { // lower_snake -> LowerSnake CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, this) } else -> { val idx = getFirstLetterOrDigit() if (idx != -1) { // any other format -> PascalCase // changing first letter to uppercase and replacing several uppercase letters in raw to lowercase: // example of change: [p]a[SC]a[_]l -> [P]a[Sc]a[L] // FixMe: there is some discussion on how PascalN_Case should be resolved: to PascalNcase or to PascalnCase or PascalNCase (current version) this[idx].uppercaseChar().toString() + convertUnknownCaseToCamel(substring(idx + 1), true) } else { log.error { "Not able to fix case format for: $this" } this } } } /** * @return index of first character which is a letter or a digit */ private fun String.getFirstLetterOrDigit() = indexOfFirst { it.isLetterOrDigit() } private fun convertUnknownCaseToCamel(str: String, isFirstLetterCapital: Boolean): String { // [p]a[SC]a[_]l -> [P]a[Sc]a[L] var isPreviousLetterCapital = isFirstLetterCapital var isPreviousLetterUnderscore = false return str.map { char -> if (char.isUpperCase()) { val result = if (isPreviousLetterCapital && !isPreviousLetterUnderscore) char.lowercaseChar() else char isPreviousLetterCapital = true isPreviousLetterUnderscore = false result.toString() } else { val result = when { char == '_' -> { isPreviousLetterUnderscore = true "" } isPreviousLetterUnderscore -> { isPreviousLetterCapital = true isPreviousLetterUnderscore = false char.uppercaseChar().toString() } else -> { isPreviousLetterCapital = false isPreviousLetterUnderscore = false char.toString() } } result } }.joinToString("") } private fun convertUnknownCaseToUpperSnake(str: String): String { // [p]a[SC]a[_]l -> [P]A_[SC]_A_[L] var alreadyInsertedUnderscore = true return str.map { char -> if (char.isUpperCase()) { if (!alreadyInsertedUnderscore) { alreadyInsertedUnderscore = true "_$char" } else { char.toString() } } else { alreadyInsertedUnderscore = (char == '_') char.uppercaseChar().toString() } }.joinToString("") } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/utils/StringUtils.kt ================================================ /** * Utility methods and constants to work with strings */ package com.saveourtool.diktat.ruleset.utils import org.jetbrains.kotlin.lexer.KtTokens internal const val NEWLINE = '\n' internal const val SPACE = ' ' internal const val TAB = '\t' @Suppress("VARIABLE_NAME_INCORRECT_FORMAT") val JAVA = arrayOf("abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue", "default", "do", "double", "else", "extends", "false", "final", "finally", "float", "for", "goto", "if", "implements", "import", "instanceof", "int", "interface", "long", "native", "new", "null", "package", "private", "protected", "public", "return", "short", "static", "strictfp", "super", "switch", "synchronized", "this", "throw", "throws", "transient", "true", "try", "void", "volatile", "while") @Suppress("VARIABLE_NAME_INCORRECT_FORMAT") val KOTLIN = KtTokens.KEYWORDS .types .map { line -> line.toString() } .plus(KtTokens.SOFT_KEYWORDS.types.map { line -> line.toString() }) /** * Either `log` or `logger`, case-insensitive. * * A name like `psychologist` or `LOGIN` won't be matched by this regular * expression. */ val loggerPropertyRegex = "(?iu)^log(?:ger)?$".toRegex() /** * @return whether [this] string represents a Java keyword */ fun String.isJavaKeyWord() = JAVA.contains(this) /** * @return whether [this] string represents a Kotlin keyword */ fun String.isKotlinKeyWord() = KOTLIN.contains(this) /** * @return whether [this] string contains only ASCII letters and/or digits */ @Suppress("FUNCTION_NAME_INCORRECT_CASE") fun String.isASCIILettersAndDigits(): Boolean = this.all { it.isDigit() || it in 'A'..'Z' || it in 'a'..'z' } /** * @return whether [this] string contains only digits */ fun String.isDigits(): Boolean = this.all { it.isDigit() } /** * @return whether [this] string contains any uppercase letters */ fun String.hasUppercaseLetter(): Boolean = this.any { it.isUpperCase() } /** * @return whether [this] string contains exactly one or zero letters */ @Suppress("FUNCTION_BOOLEAN_PREFIX") fun String.containsOneLetterOrZero(): Boolean { val count = this.count { it.isLetter() } return count == 1 || count == 0 } /** * method checks that string has prefix like: * mFunction, kLength or M_VAR * * @return true if string has prefix */ @Suppress("ForbiddenComment") fun String.hasPrefix(): Boolean { // checking cases like mFunction if (this.isLowerCamelCase() && this.length >= 2 && this.substring(0, 2).count { it in 'A'..'Z' } == 1) { return true } // checking cases like M_VAL if (this.isUpperSnakeCase() && this.length >= 2 && this.substring(0, 2).contains('_')) { return true } return false } /** * removing the prefix in the word * M_VAR -> VAR * mVariable -> variable * * @return a string without prefix */ @Suppress("ForbiddenComment") fun String.removePrefix(): String { // FixMe: there can be cases when after you will change variable name - it becomes a keyword if (this.isLowerCamelCase()) { return this[1].lowercaseChar() + this.substring(2) } if (this.isUpperSnakeCase()) { return this.substring(2) } return this } /** * @return the indentation of the last line of this string. */ internal fun String.lastIndent() = substringAfterLast(NEWLINE).count(::isSpaceCharacter) /** * @return the number of leading space characters in this string. */ internal fun String.leadingSpaceCount(): Int = asSequence() .takeWhile(::isSpaceCharacter) .count() /** * @param ch the character to examine. * @return `true` if [ch] is a [SPACE], `false` otherwise. */ private fun isSpaceCharacter(ch: Char): Boolean = ch == SPACE ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/utils/indentation/Checkers.kt ================================================ /** * Implementations of CustomIndentationChecker for IndentationRule */ package com.saveourtool.diktat.ruleset.utils.indentation import com.saveourtool.diktat.ruleset.rules.chapter3.files.IndentationAmount import com.saveourtool.diktat.ruleset.rules.chapter3.files.IndentationAmount.SINGLE import com.saveourtool.diktat.ruleset.rules.chapter3.files.IndentationError import com.saveourtool.diktat.ruleset.utils.hasParent import com.saveourtool.diktat.ruleset.utils.isBooleanExpression import com.saveourtool.diktat.ruleset.utils.isDotBeforeCallOrReference import com.saveourtool.diktat.ruleset.utils.isElvisOperationReference import com.saveourtool.diktat.ruleset.utils.isLongStringTemplateEntry import com.saveourtool.diktat.ruleset.utils.lastIndent import com.saveourtool.diktat.ruleset.utils.nextCodeSibling import com.saveourtool.diktat.ruleset.utils.prevSibling import org.jetbrains.kotlin.KtNodeTypes.BINARY_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.BINARY_WITH_TYPE import org.jetbrains.kotlin.KtNodeTypes.BODY import org.jetbrains.kotlin.KtNodeTypes.DOT_QUALIFIED_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.ELSE import org.jetbrains.kotlin.KtNodeTypes.IS_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.LONG_STRING_TEMPLATE_ENTRY import org.jetbrains.kotlin.KtNodeTypes.OPERATION_REFERENCE import org.jetbrains.kotlin.KtNodeTypes.SAFE_ACCESS_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.SUPER_TYPE_LIST import org.jetbrains.kotlin.KtNodeTypes.THEN import org.jetbrains.kotlin.KtNodeTypes.VALUE_ARGUMENT import org.jetbrains.kotlin.KtNodeTypes.VALUE_ARGUMENT_LIST import org.jetbrains.kotlin.KtNodeTypes.VALUE_PARAMETER import org.jetbrains.kotlin.KtNodeTypes.VALUE_PARAMETER_LIST import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.PsiElement import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace import org.jetbrains.kotlin.kdoc.lexer.KDocTokens import org.jetbrains.kotlin.kdoc.parser.KDocElementTypes import org.jetbrains.kotlin.lexer.KtTokens.ARROW import org.jetbrains.kotlin.lexer.KtTokens.AS_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.AS_SAFE import org.jetbrains.kotlin.lexer.KtTokens.BLOCK_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.COLON import org.jetbrains.kotlin.lexer.KtTokens.ELVIS import org.jetbrains.kotlin.lexer.KtTokens.EOL_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.EQ import org.jetbrains.kotlin.lexer.KtTokens.LPAR import org.jetbrains.kotlin.lexer.KtTokens.WHITE_SPACE import org.jetbrains.kotlin.psi.KtBlockExpression import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtIfExpression import org.jetbrains.kotlin.psi.KtLoopExpression import org.jetbrains.kotlin.psi.KtProperty import org.jetbrains.kotlin.psi.KtPropertyAccessor import org.jetbrains.kotlin.psi.KtWhenEntry import org.jetbrains.kotlin.psi.psiUtil.children import org.jetbrains.kotlin.psi.psiUtil.parents import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf import org.jetbrains.kotlin.psi.psiUtil.siblings /** * Performs the following check: assignment operator increases indent by one step for the expression after it. * If [IndentationConfig.extendedIndentForExpressionBodies] is set to `true`, indentation is increased by two steps instead. */ internal class AssignmentOperatorChecker(configuration: IndentationConfig) : CustomIndentationChecker(configuration) { override fun checkNode(whiteSpace: PsiWhiteSpace, indentError: IndentationError): CheckResult? { whiteSpace.prevSibling?.node?.let { prevNode -> if (prevNode.elementType == EQ && prevNode.treeNext.let { it.elementType == WHITE_SPACE && it.textContains('\n') }) { return CheckResult.from(indentError.actual, (whiteSpace.parentIndent() ?: indentError.expected) + IndentationAmount.valueOf(configuration.extendedIndentForExpressionBodies), true) } } return null } } /** * Performs the following check: When breaking parameter list of a method/class constructor it can be aligned with 8 spaces * or in a method/class declaration a parameter that was moved to a newline can be on the same level as the previous argument. */ @Suppress("ForbiddenComment") internal class ValueParameterListChecker(configuration: IndentationConfig) : CustomIndentationChecker(configuration) { /** * This check triggers if the following conditions are met: * 1. line break is inside value parameter or value argument list (function declaration or invocation) * 2. there are no other line breaks before this node * 3. there are no more arguments after this node */ private fun isCheckNeeded(whiteSpace: PsiWhiteSpace) = whiteSpace.parent .node .elementType .let { it == VALUE_PARAMETER_LIST || it == VALUE_ARGUMENT_LIST } && whiteSpace.siblings(forward = false, withItself = false).none { it is PsiWhiteSpace && it.textContains('\n') } && @Suppress("PARAMETER_NAME_IN_OUTER_LAMBDA") // no need to trigger when there are no more parameters in the list whiteSpace.siblings(forward = true, withItself = false).any { it.node.elementType.run { this == VALUE_ARGUMENT || this == VALUE_PARAMETER } } override fun checkNode(whiteSpace: PsiWhiteSpace, indentError: IndentationError): CheckResult? { if (isCheckNeeded(whiteSpace)) { val parameterList = whiteSpace.parent.node // parameters in lambdas are VALUE_PARAMETER_LIST and might have no LPAR: list { elem -> ... } val parameterAfterLpar = parameterList .findChildByType(LPAR) ?.treeNext ?.takeIf { it.elementType != WHITE_SPACE && // there can be multiline arguments and in this case we don't align parameters with them !it.textContains('\n') } val expectedIndent = if (parameterAfterLpar != null && configuration.alignedParameters && parameterList.elementType == VALUE_PARAMETER_LIST) { val ktFile = whiteSpace.parents.last() as KtFile // count column number of the first parameter ktFile.text .lineSequence() // calculate offset for every line end, `+1` for `\n` which is trimmed in `lineSequence` .scan(0 to "") { (length, _), line -> length + line.length + 1 to line } .run { // find the line where `parameterAfterLpar` resides find { it.first > parameterAfterLpar.startOffset } ?: last() } .let { (_, line) -> line.substringBefore(parameterAfterLpar.text).length } } else if (configuration.extendedIndentOfParameters) { indentError.expected + SINGLE } else { indentError.expected } return CheckResult.from(indentError.actual, expectedIndent, adjustNext = true, includeLastChild = false) } return null } } /** * Performs the following check: When breaking line after operators like +/-/`*` etc. new line can be indented with 8 space */ internal class ExpressionIndentationChecker(configuration: IndentationConfig) : CustomIndentationChecker(configuration) { override fun checkNode(whiteSpace: PsiWhiteSpace, indentError: IndentationError): CheckResult? = when { whiteSpace.parent.node.elementType in sequenceOf(BINARY_EXPRESSION, BINARY_WITH_TYPE) && whiteSpace.immediateSiblings().any { sibling -> /* * We're looking for an operation reference, including * `as` and `as?` (`AS_SAFE`), but excluding `?:` (`ELVIS`), * because there's a separate flag for Elvis expressions * in IDEA (`CONTINUATION_INDENT_IN_ELVIS`). */ sibling.node.elementType == OPERATION_REFERENCE && sibling.node.children().firstOrNull()?.elementType != ELVIS } -> { val parentIndent = whiteSpace.parentIndent() ?: indentError.expected val expectedIndent = parentIndent + IndentationAmount.valueOf(configuration.extendedIndentAfterOperators) CheckResult.from(indentError.actual, expectedIndent, true) } else -> null } } /** * In KDoc leading asterisks should be indented with one additional space */ internal class KdocIndentationChecker(config: IndentationConfig) : CustomIndentationChecker(config) { override fun checkNode(whiteSpace: PsiWhiteSpace, indentError: IndentationError): CheckResult? { if (whiteSpace.nextSibling.node.elementType in listOf(KDocTokens.LEADING_ASTERISK, KDocTokens.END, KDocElementTypes.KDOC_SECTION)) { val expectedIndent = indentError.expected + 1 return CheckResult.from(indentError.actual, expectedIndent) } return null } } /** * This checker indents all super types of class by one INDENT_SIZE or by two if colon is on a new line * If class declaration has supertype list, then it should have a colon before it, therefore UnsafeCallOnNullableType inspection is suppressed */ @Suppress("UnsafeCallOnNullableType") internal class SuperTypeListChecker(config: IndentationConfig) : CustomIndentationChecker(config) { override fun checkNode(whiteSpace: PsiWhiteSpace, indentError: IndentationError): CheckResult? { if (whiteSpace.nextSibling.node.elementType == SUPER_TYPE_LIST) { val hasNewlineBeforeColon = whiteSpace.node .prevSibling { it.elementType == COLON }!! .treePrev .takeIf { it.elementType == WHITE_SPACE } ?.textContains('\n') ?: false val expectedIndent = indentError.expected + IndentationAmount.valueOf(extendedIndent = hasNewlineBeforeColon) return CheckResult.from(indentError.actual, expectedIndent) } else if (whiteSpace.parent.node.elementType == SUPER_TYPE_LIST) { val expectedIndent = whiteSpace.parentIndent() ?: (indentError.expected + SINGLE) return CheckResult.from(indentError.actual, expectedIndent) } return null } } /** * This checker performs the following check: When dot call start on a new line, it should be indented by [IndentationConfig.indentationSize]. * Same is true for safe calls (`?.`) and elvis operator (`?:`). */ internal class DotCallChecker(config: IndentationConfig) : CustomIndentationChecker(config) { /** * @param nextNodePredicate the predicate which the next non-comment * non-whitespace node should satisfy. * @return `true` if this is a comment node which is immediately preceding * the node specified by [nextNodePredicate]. */ private fun ASTNode.isCommentBefore(nextNodePredicate: ASTNode.() -> Boolean): Boolean { if (elementType in sequenceOf(EOL_COMMENT, BLOCK_COMMENT)) { var nextNode: ASTNode? = treeNext while (nextNode != null && nextNode.elementType in sequenceOf(WHITE_SPACE, EOL_COMMENT)) { nextNode = nextNode.treeNext } return nextNode?.nextNodePredicate() ?: false } return false } private fun ASTNode.isElvisReferenceOrCommentBeforeElvis(): Boolean = isElvisOperationReference() || isCommentBefore(ASTNode::isElvisOperationReference) private fun ASTNode.isFromStringTemplate(): Boolean = hasParent(LONG_STRING_TEMPLATE_ENTRY) @Suppress( "ComplexMethod", "TOO_LONG_FUNCTION", ) override fun checkNode(whiteSpace: PsiWhiteSpace, indentError: IndentationError): CheckResult? { whiteSpace.nextSibling .node .takeIf { nextNode -> (nextNode.isDotBeforeCallOrReference() || nextNode.elementType == OPERATION_REFERENCE && nextNode.firstChildNode.elementType in sequenceOf(ELVIS, IS_EXPRESSION, AS_KEYWORD, AS_SAFE) || nextNode.isCommentBefore(ASTNode::isDotBeforeCallOrReference) || nextNode.isCommentBefore(ASTNode::isElvisOperationReference)) && whiteSpace.parents.none(PsiElement::isLongStringTemplateEntry) } /*- * Here, `node` is any of: * * - a `DOT` or a `SAFE_ACCESS`, * - an `OPERATION_REFERENCE` with `ELVIS` as the only child, or */ ?.let { node -> val indentIncrement = IndentationAmount.valueOf(configuration.extendedIndentBeforeDot) if (node.isFromStringTemplate()) { return CheckResult.from(indentError.actual, indentError.expected + indentIncrement, true) } /*- * The list of immediate parents of this whitespace node, * nearest-to-farthest order * (the farthest parent is the file node). */ val parentExpressions = whiteSpace.parents.takeWhile { parent -> val parentType = parent.node.elementType when { /* * #1532, 1.2.4+: if this is an Elvis operator * (OPERATION_REFERENCE -> ELVIS), or an EOL or a * block comment which immediately precedes this * Elvis operator, then the indent of the parent * binary expression should be used as a base for * the increment. */ node.isElvisReferenceOrCommentBeforeElvis() -> parentType == BINARY_EXPRESSION /* * Pre-1.2.4 behaviour, all other cases: the indent * of the parent dot-qualified or safe-access * expression should be used as a base for the * increment. */ else -> parentType in sequenceOf( DOT_QUALIFIED_EXPRESSION, SAFE_ACCESS_EXPRESSION, ) } }.toList() /* * Selects from the matching parent nodes. */ val matchOrNull: Iterable.() -> PsiElement? = { when { /* * Selects nearest. */ node.isElvisReferenceOrCommentBeforeElvis() -> firstOrNull() /* * Selects farthest. */ else -> lastOrNull() } } // we need to get indent before the first expression in calls chain /*- * If the parent indent (the one before a `DOT_QUALIFIED_EXPRESSION` * or a `SAFE_ACCESS_EXPRESSION`) is `null`, then use 0 as the * fallback value. * * If `indentError.expected` is used as a fallback (pre-1.2.2 * behaviour), this breaks chained dot-qualified or safe-access * expressions (see #1336), e.g.: * * ```kotlin * val a = first() * .second() * .third() * ``` */ val parentIndent = (parentExpressions.matchOrNull() ?: whiteSpace).parentIndent() ?: 0 val expectedIndent = when { /*- * Don't indent Elvis expressions (and the corresponding comments) * which are nested inside boolean expressions: * * ```kotlin * val x = true && * "" * ?.isEmpty() * ?: true * ``` * * This is a special case, and this is how IDEA formats source code. */ node.isElvisReferenceOrCommentBeforeElvis() && parentExpressions.any { it.node.isBooleanExpression() } -> parentIndent /*- * All other cases (dot-qualified, safe-access, Elvis). * Expression parts are indented regularly, e.g.: * * ```kotlin * val a = null as Boolean? * ?: true * ``` */ else -> parentIndent + indentIncrement } return CheckResult.from(indentError.actual, expectedIndent, true) } return null } } /** * This [CustomIndentationChecker] checks indentation in loops and if-else expressions without braces around body. */ internal class ConditionalsAndLoopsWithoutBracesChecker(config: IndentationConfig) : CustomIndentationChecker(config) { override fun checkNode(whiteSpace: PsiWhiteSpace, indentError: IndentationError): CheckResult? { val parent = whiteSpace.parent val nextNode = whiteSpace.node.nextCodeSibling() // if there is comment after if or else, it should be indented too return when (parent) { is KtLoopExpression -> nextNode?.elementType == BODY && parent.body !is KtBlockExpression is KtIfExpression -> nextNode?.elementType == THEN && parent.then !is KtBlockExpression || nextNode?.elementType == ELSE && parent.`else`.let { it !is KtBlockExpression && it !is KtIfExpression } else -> false } .takeIf { it } ?.let { CheckResult.from(indentError.actual, indentError.expected + SINGLE, true) } } } /** * This [CustomIndentationChecker] check indentation before custom getters and setters on property. */ internal class CustomGettersAndSettersChecker(config: IndentationConfig) : CustomIndentationChecker(config) { override fun checkNode(whiteSpace: PsiWhiteSpace, indentError: IndentationError): CheckResult? { val parent = whiteSpace.parent if (parent is KtProperty && whiteSpace.nextSibling is KtPropertyAccessor) { return CheckResult.from(indentError.actual, (parent.parentIndent() ?: indentError.expected) + SINGLE, true) } return null } } /** * Performs the following check: arrow in `when` expression increases indent by one step for the expression after it. */ internal class ArrowInWhenChecker(configuration: IndentationConfig) : CustomIndentationChecker(configuration) { override fun checkNode(whiteSpace: PsiWhiteSpace, indentError: IndentationError): CheckResult? { val prevNode = whiteSpace.prevSibling?.node if (prevNode?.elementType == ARROW && whiteSpace.parent is KtWhenEntry) { return CheckResult.from(indentError.actual, (whiteSpace.parentIndent() ?: indentError.expected) + SINGLE, true) } return null } } /** * @return indentation of parent node */ internal fun PsiElement.parentIndent(): Int? = parentsWithSelf .map { parent -> parent.node.prevSibling { it.elementType == WHITE_SPACE && it.textContains('\n') } } .filterNotNull() .firstOrNull() ?.text ?.lastIndent() /** * @return the sequence of immediate siblings (the previous and the next one), * excluding `null`'s. */ private fun PsiElement.immediateSiblings(): Sequence = sequenceOf(prevSibling, nextSibling).filterNotNull() ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/utils/indentation/CustomIndentationChecker.kt ================================================ /** * Utility classes for IndentationRule */ package com.saveourtool.diktat.ruleset.utils.indentation import com.saveourtool.diktat.ruleset.rules.chapter3.files.IndentationConfigAware import com.saveourtool.diktat.ruleset.rules.chapter3.files.IndentationError import com.saveourtool.diktat.ruleset.utils.NEWLINE import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace /** * @property configuration configuration of indentation rule */ internal abstract class CustomIndentationChecker(override val configuration: IndentationConfig) : IndentationConfigAware { /** * This method checks if this white space is an exception from general rule * If true, checks if it is properly indented and fixes * * @param whiteSpace PSI element of type [PsiWhiteSpace]. The whitespace is * guaranteed to contain a [newline][NEWLINE]. * @param indentError and [IndentationError] on this node * @return null true if node is not an exception, CheckResult otherwise */ abstract fun checkNode(whiteSpace: PsiWhiteSpace, indentError: IndentationError): CheckResult? } /** * @property adjustNext Indicates whether the indent returned by this exception checker needs to be applied to other nodes with same parent * @property includeLastChild Indicates whether the white space before the last child node of the initiator node should have increased indent or not * @property isCorrect whether indentation check is correct * @property expectedIndent expected indentation */ internal data class CheckResult( val isCorrect: Boolean, val expectedIndent: Int, val adjustNext: Boolean, val includeLastChild: Boolean = true ) { companion object { /** * @param actual actual indentation * @param expected expected indentation * @param adjustNext see [CheckResult.adjustNext] * @param includeLastChild see [CheckResult.includeLastChild] * @return an instance of [CheckResult] */ fun from(actual: Int, expected: Int, adjustNext: Boolean = false, includeLastChild: Boolean = true ) = CheckResult(actual == expected, expected, adjustNext, includeLastChild) } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/utils/indentation/IndentationConfig.kt ================================================ package com.saveourtool.diktat.ruleset.utils.indentation import com.saveourtool.diktat.api.DiktatErrorEmitter import com.saveourtool.diktat.common.config.rules.RuleConfiguration /** * [RuleConfiguration] for indentation logic */ class IndentationConfig(config: Map) : RuleConfiguration(config) { /** * Is newline at the end of a file needed */ val newlineAtEnd = config[NEWLINE_AT_END]?.toBoolean() ?: true /** * If true, in parameter list when parameters are split by newline they are indented with two indentations instead of one */ val extendedIndentOfParameters = config[EXTENDED_INDENT_OF_PARAMETERS]?.toBoolean() ?: false /** * If true, if first parameter in parameter list is on the same line as opening parenthesis, then other parameters * can be aligned with it */ val alignedParameters = config[ALIGNED_PARAMETERS]?.toBoolean() ?: true /** * If `true`, expression bodies which begin on a separate line are indented * using a _continuation indent_. The flag is **off**: * * ```kotlin * val a: Boolean = * false * * val b: Boolean * get() = * false * * fun f(): Boolean = * false * ``` * * The flag is **on**: * * ```kotlin * val a: Boolean = * false * * val b: Boolean * get() = * false * * fun f(): Boolean = * false * ``` * * The default is `false`. * * This flag is called `CONTINUATION_INDENT_FOR_EXPRESSION_BODIES` in _IDEA_ * and `ij_kotlin_continuation_indent_for_expression_bodies` in * `.editorconfig`. * * @since 1.2.2 */ val extendedIndentForExpressionBodies = config[EXTENDED_INDENT_FOR_EXPRESSION_BODIES]?.toBoolean() ?: false /** * If true, if expression is split by newline after operator like +/-/`*`, then the next line is indented with two indentations instead of one */ val extendedIndentAfterOperators = config[EXTENDED_INDENT_AFTER_OPERATORS]?.toBoolean() ?: true /** * If true, when dot qualified expression starts on a new line, this line will be indented with * two indentations instead of one */ val extendedIndentBeforeDot = config[EXTENDED_INDENT_BEFORE_DOT]?.toBoolean() ?: false /** * The indentation size for each file */ val indentationSize = config[INDENTATION_SIZE]?.toInt() ?: DEFAULT_INDENTATION_SIZE override fun equals(other: Any?): Boolean = other is IndentationConfig && configWithExplicitDefaults == other.configWithExplicitDefaults override fun hashCode(): Int = configWithExplicitDefaults.hashCode() override fun toString(): String = "${javaClass.simpleName}$configWithExplicitDefaults" /** * @param errorEmitter * @return overridden [errorEmitter] which warns message with configured [indentationSize] */ fun overrideIfRequiredWarnMessage(errorEmitter: DiktatErrorEmitter): DiktatErrorEmitter = if (indentationSize == DEFAULT_INDENTATION_SIZE) { errorEmitter } else { DiktatErrorEmitter { offset, errorMessage, canBeAutoCorrected -> errorEmitter(offset, errorMessage.replace("should equal to 4 spaces (tabs are not allowed)", "should equal to $indentationSize spaces (tabs are not allowed)"), canBeAutoCorrected) } } companion object { internal const val ALIGNED_PARAMETERS = "alignedParameters" /** * The default indent size (space characters), configurable via * `indentationSize`. */ private const val DEFAULT_INDENTATION_SIZE = 4 const val EXTENDED_INDENT_AFTER_OPERATORS = "extendedIndentAfterOperators" const val EXTENDED_INDENT_BEFORE_DOT = "extendedIndentBeforeDot" const val EXTENDED_INDENT_FOR_EXPRESSION_BODIES = "extendedIndentForExpressionBodies" const val EXTENDED_INDENT_OF_PARAMETERS = "extendedIndentOfParameters" const val INDENTATION_SIZE = "indentationSize" const val NEWLINE_AT_END = "newlineAtEnd" @Suppress("CUSTOM_GETTERS_SETTERS") private val IndentationConfig.configWithExplicitDefaults: Map get() = mutableMapOf().apply { putAll(config) putIfAbsent(ALIGNED_PARAMETERS, alignedParameters) putIfAbsent(EXTENDED_INDENT_AFTER_OPERATORS, extendedIndentAfterOperators) putIfAbsent(EXTENDED_INDENT_BEFORE_DOT, extendedIndentBeforeDot) putIfAbsent(EXTENDED_INDENT_FOR_EXPRESSION_BODIES, extendedIndentForExpressionBodies) putIfAbsent(EXTENDED_INDENT_OF_PARAMETERS, extendedIndentOfParameters) putIfAbsent(INDENTATION_SIZE, indentationSize) putIfAbsent(NEWLINE_AT_END, newlineAtEnd) }.mapValues { (_, value) -> value.toString() } } } ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/utils/search/VariablesSearch.kt ================================================ @file:Suppress( "KDOC_NO_CONSTRUCTOR_PROPERTY", "MISSING_KDOC_CLASS_ELEMENTS", "MISSING_KDOC_ON_FUNCTION", "KDOC_WITHOUT_PARAM_TAG", "KDOC_WITHOUT_RETURN_TAG" ) package com.saveourtool.diktat.ruleset.utils.search import com.saveourtool.diktat.ruleset.utils.findAllDescendantsWithSpecificType import com.saveourtool.diktat.ruleset.utils.getDeclarationScope import com.saveourtool.diktat.ruleset.utils.isGoingAfter import org.jetbrains.kotlin.KtNodeTypes import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.psi.KtBlockExpression import org.jetbrains.kotlin.psi.KtClassBody import org.jetbrains.kotlin.psi.KtDotQualifiedExpression import org.jetbrains.kotlin.psi.KtElement import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtFunctionLiteral import org.jetbrains.kotlin.psi.KtNameReferenceExpression import org.jetbrains.kotlin.psi.KtProperty import org.jetbrains.kotlin.psi.psiUtil.getChildrenOfType import org.jetbrains.kotlin.psi.psiUtil.getParentOfType import org.jetbrains.kotlin.psi.psiUtil.parents import org.jetbrains.kotlin.psi.psiUtil.referenceExpression import org.jetbrains.kotlin.psi.stubs.elements.KtFileElementType /** * @param node root node of a type File that is used to search all declared properties (variables) * it should be ONLY node of File elementType * @param filterForVariables condition to filter */ abstract class VariablesSearch(val node: ASTNode, private val filterForVariables: (KtProperty) -> Boolean) { /** * to complete implementation of a search mechanism you need to specify what and how you will search in current scope * [this] - scope where to search the usages/assignments/e.t.c of the variable (can be of types KtBlockExpression/KtFile/KtClassBody) */ protected abstract fun KtElement.getAllSearchResults(property: KtProperty): List /** * method collects all declared variables and it's usages * * @return a map of a property to it's usages */ @Suppress("TYPE_ALIAS") fun collectVariables(): Map> { require(node.elementType == KtFileElementType.INSTANCE) { "To collect all variables in a file you need to provide file root node" } return node .findAllDescendantsWithSpecificType(KtNodeTypes.PROPERTY) .map { it.psi as KtProperty } .filter(filterForVariables) .associateWith { it.getSearchResults() } } @Suppress("UnsafeCallOnNullableType") fun KtProperty.getSearchResults() = this .getDeclarationScope() // if declaration scope is not null - then we have found out the block where this variable is stored // else - it is a global variable on a file level or a property on the class level .let { declarationScope -> // searching in the scope with declaration (in the context) declarationScope?.getAllSearchResults(this) // searching on the class level in class body ?: (this.getParentOfType(true)?.getAllSearchResults(this)) // searching on the file level ?: (this.getParentOfType(true)!!.getAllSearchResults(this)) } /** * filtering object's fields (expressions) that have same name as variable */ protected fun KtNameReferenceExpression.isReferenceToFieldOfObject(): Boolean { val expression = this return (expression.parent as? KtDotQualifiedExpression)?.run { receiverExpression != expression && selectorExpression?.referenceExpression() == expression } ?: false } /** * filtering local properties from other context (shadowed) and lambda and function arguments with same name * going through all parent scopes from bottom to top until we will find the scope where the initial variable was declared * all these scopes are on lower level of inheritance that's why if in one of these scopes we will find any * variable declaration with the same name - we will understand that it is usage of another variable */ protected fun isReferenceToOtherVariableWithSameName(expression: KtNameReferenceExpression, codeBlock: KtElement, property: KtProperty ) = expression.parents // getting all block expressions/class bodies/file node from bottom to the top // FixMe: Object companion is not resolved properly yet .filter { it is KtBlockExpression || it is KtClassBody || it is KtFile } // until we reached the block that contains the initial declaration .takeWhile { codeBlock != it } .any { block -> // this is not the expression that we needed if: // 1) there is a new shadowed declaration for this expression (but the declaration should stay on the previous line!) // 2) or there one of top blocks is a function/lambda that has arguments with the same name // FixMe: in class or a file the declaration can easily go after the usage (by lines of code) block.getChildrenOfType() .any { it.nameAsName == property.nameAsName && expression.node.isGoingAfter(it.node) } || block.parent .let { it as? KtFunctionLiteral } ?.valueParameters ?.any { it.nameAsName == property.nameAsName } ?: false // FixMe: also see very strange behavior of Kotlin in tests (disabled) } } /** * this is a small workaround in case we don't want to make any custom filter while searching variables * * @param node an [ASTNode] */ @Suppress("UNUSED_PARAMETER", "FunctionOnlyReturningConstant") fun default(node: KtProperty) = true ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/utils/search/VariablesWithAssignmentSearch.kt ================================================ @file:Suppress( "MISSING_KDOC_TOP_LEVEL", "KDOC_NO_CONSTRUCTOR_PROPERTY", "MISSING_KDOC_CLASS_ELEMENTS", "MISSING_KDOC_ON_FUNCTION", "KDOC_WITHOUT_PARAM_TAG", "KDOC_WITHOUT_RETURN_TAG", "KDOC_NO_EMPTY_TAGS" ) package com.saveourtool.diktat.ruleset.utils.search import com.saveourtool.diktat.ruleset.utils.findAllDescendantsWithSpecificType import com.saveourtool.diktat.ruleset.utils.isGoingAfter import org.jetbrains.kotlin.KtNodeTypes import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.psi.KtBinaryExpression import org.jetbrains.kotlin.psi.KtElement import org.jetbrains.kotlin.psi.KtNameReferenceExpression import org.jetbrains.kotlin.psi.KtProperty class VariablesWithAssignmentSearch(fileNode: ASTNode, filterForVariables: (KtProperty) -> Boolean) : VariablesSearch(fileNode, filterForVariables) { /** * searching for all assignments of variables in current context [this] * * @param property * @return */ override fun KtElement.getAllSearchResults(property: KtProperty) = this.node .findAllDescendantsWithSpecificType(KtNodeTypes.BINARY_EXPRESSION) // filtering out all usages that are declared in the same context but are going before the variable declaration // AND checking that there is an assignment .filter { // FixMe: bug is here with a search of postfix/prefix variables assignment (like ++). // FixMe: Currently we check only val a = 5, ++a is not checked here // FixMe: also there can be some tricky cases with setters, but I am not able to imagine them now it.isGoingAfter(property.node) && (it.psi as KtBinaryExpression).operationToken == KtTokens.EQ && (it.psi as KtBinaryExpression) .left ?.node ?.elementType == KtNodeTypes.REFERENCE_EXPRESSION } .map { (it.psi as KtBinaryExpression).left as KtNameReferenceExpression } // checking that name of the property in usage matches with the name in the declaration .filter { it.getReferencedNameAsName() == property.nameAsName } .filterNot { expression -> // to avoid false triggering on objects' fields with same name as property expression.isReferenceToFieldOfObject() || // to exclude usages of local properties from other context (shadowed) and lambda arguments with same name isReferenceToOtherVariableWithSameName(expression, this, property) } .toList() } /** * the default value for filtering condition is always true */ fun ASTNode.findAllVariablesWithAssignments(filterForVariables: (KtProperty) -> Boolean = ::default) = VariablesWithAssignmentSearch(this, filterForVariables).collectVariables() ================================================ FILE: diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/utils/search/VariablesWithUsagesSearch.kt ================================================ @file:Suppress( "MISSING_KDOC_TOP_LEVEL", "KDOC_NO_CONSTRUCTOR_PROPERTY", "MISSING_KDOC_CLASS_ELEMENTS", "MISSING_KDOC_ON_FUNCTION", "KDOC_WITHOUT_PARAM_TAG", "KDOC_WITHOUT_RETURN_TAG", "KDOC_NO_EMPTY_TAGS" ) package com.saveourtool.diktat.ruleset.utils.search import com.saveourtool.diktat.ruleset.utils.findAllDescendantsWithSpecificType import com.saveourtool.diktat.ruleset.utils.isGoingAfter import org.jetbrains.kotlin.KtNodeTypes import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.psi.KtElement import org.jetbrains.kotlin.psi.KtNameReferenceExpression import org.jetbrains.kotlin.psi.KtProperty class VariablesWithUsagesSearch(fileNode: ASTNode, filterForVariables: (KtProperty) -> Boolean) : VariablesSearch(fileNode, filterForVariables) { override fun KtElement.getAllSearchResults(property: KtProperty) = this.node .findAllDescendantsWithSpecificType(KtNodeTypes.REFERENCE_EXPRESSION) // filtering out all usages that are declared in the same context but are going before the variable declaration .filter { it.isGoingAfter(property.node) } .map { it.psi as KtNameReferenceExpression } .filter { it.getReferencedNameAsName() == property.nameAsName } .filterNot { expression -> // to avoid false triggering on objects' fields with same name as property expression.isReferenceToFieldOfObject() || // to exclude usages of local properties from other context (shadowed) and lambda arguments with same name isReferenceToOtherVariableWithSameName(expression, this, property) } } /** * the default value for filtering condition is always true */ fun ASTNode.findAllVariablesWithUsages(filterForVariables: (KtProperty) -> Boolean = ::default) = VariablesWithUsagesSearch(this, filterForVariables).collectVariables() ================================================ FILE: diktat-rules/src/main/resources/diktat-analysis-huawei.yml ================================================ # Common configuration - name: DIKTAT_COMMON enabled: true configuration: # put your package name here - it will be autofixed and checked domainName: com.huawei # testDirs: test disabledChapters: "" testDirs: test kotlinVersion: 2.1 srcDirectories: "main" # Checks that the Class/Enum/Interface name matches Pascal case - name: CLASS_NAME_INCORRECT enabled: true # all code blocks with MyAnnotation will be ignored and not checked ignoreAnnotated: [ MyAnnotation ] # Checks that CONSTANT (treated as const val from companion object or class level) is in non UPPER_SNAKE_CASE - name: CONSTANT_UPPERCASE enabled: true # Checks that enum value is in upper SNAKE_CASE or in PascalCase depending on the config. UPPER_SNAKE_CASE is the default, but can be changed by 'enumStyle' config - name: ENUM_VALUE enabled: true configuration: # Two options: SNAKE_CASE (default), PascalCase enumStyle: SNAKE_CASE # Checks that class which extends any Exception class has Exception suffix - name: EXCEPTION_SUFFIX enabled: true # Checks that file name has extension - name: FILE_NAME_INCORRECT enabled: true # Checks that file name matches class name, if it is only one class in file - name: FILE_NAME_MATCH_CLASS enabled: true # Checks that functions/methods which return boolean have special prefix like "is/should/e.t.c" - name: FUNCTION_BOOLEAN_PREFIX enabled: true configuration: allowedPrefixes: "" # A list of functions that return boolean and are allowed to use. Input is in a form "foo, bar". # Checks that function/method name is in lowerCamelCase - name: FUNCTION_NAME_INCORRECT_CASE enabled: true # Checks that typealias name is in PascalCase - name: TYPEALIAS_NAME_INCORRECT_CASE enabled: true # Checks that generic name doesn't contain more than 1 letter (capital). It can be followed by numbers, example: T12, T - name: GENERIC_NAME enabled: true # Identifier length should be in range [2,64] except names that used in industry like {i, j} and 'e' for catching exceptions - name: IDENTIFIER_LENGTH enabled: true # Checks that the object matches PascalCase - name: OBJECT_NAME_INCORRECT enabled: true # Checks that package name is in correct (lower) case - name: PACKAGE_NAME_INCORRECT_CASE enabled: true # Checks that package name starts with the company's domain - name: PACKAGE_NAME_INCORRECT_PREFIX enabled: true # Checks that package name does not have incorrect symbols like underscore or non-ASCII letters/digits - name: PACKAGE_NAME_INCORRECT_SYMBOLS enabled: true # Checks that the path for a file matches with a package name - name: PACKAGE_NAME_INCORRECT_PATH enabled: true # Checks that package name is in the file - name: PACKAGE_NAME_MISSING enabled: true # Checks that variable does not have prefix (like mVariable or M_VARIABLE) - name: VARIABLE_HAS_PREFIX enabled: true # Checks that variable does not contain one single letter, only exceptions are fixed names that used in industry like {i, j} - name: VARIABLE_NAME_INCORRECT enabled: true # Checks that the name of variable is in lowerCamelCase and contains only ASCII letters - name: VARIABLE_NAME_INCORRECT_FORMAT enabled: true # Checks that functions have kdoc - name: MISSING_KDOC_ON_FUNCTION enabled: true # Checks that on file level internal or public class or function has missing KDoc - name: MISSING_KDOC_TOP_LEVEL enabled: true # Checks that accessible internal elements (protected, public, internal) in a class are documented - name: MISSING_KDOC_CLASS_ELEMENTS enabled: true # Checks that accessible method parameters are documented in KDoc - name: KDOC_WITHOUT_PARAM_TAG enabled: true # Checks that accessible method explicit return type is documented in KDoc - name: KDOC_WITHOUT_RETURN_TAG enabled: true # Checks that accessible method throw keyword is documented in KDoc - name: KDOC_WITHOUT_THROWS_TAG enabled: true # Checks that KDoc is not empty - name: KDOC_EMPTY_KDOC enabled: true # Checks that underscore is correctly used to split package naming - name: INCORRECT_PACKAGE_SEPARATOR enabled: true # Checks that code block doesn't contain kdoc comments - name: COMMENTED_BY_KDOC enabled: true # Checks that there is no @deprecated tag in kdoc - name: KDOC_NO_DEPRECATED_TAG enabled: true # Checks that there is no empty content in kdoc tags - name: KDOC_NO_EMPTY_TAGS enabled: true # Checks that there is only one space after kdoc tag - name: KDOC_WRONG_SPACES_AFTER_TAG enabled: true # Checks tags order in kDoc. `@param`, `@return`, `@throws` - name: KDOC_WRONG_TAGS_ORDER enabled: true # Checks that there is no newline of empty KDoc line (with leading asterisk) between `@param`, `@return`, `@throws` tags - name: KDOC_NO_NEWLINES_BETWEEN_BASIC_TAGS enabled: true # Checks that block of tags @param, @return, @throws is separated from previous part of KDoc by exactly one empty line - name: KDOC_NEWLINES_BEFORE_BASIC_TAGS enabled: true # Checks that special tags `@apiNote`, `@implNote`, `@implSpec` have exactly one empty line after - name: KDOC_NO_NEWLINE_AFTER_SPECIAL_TAGS enabled: true # Checks that kdoc does not contain @author tag or date - name: KDOC_CONTAINS_DATE_OR_AUTHOR enabled: true configuration: versionRegex: \d+\.\d+\.\d+[-.\w\d]* # Checks that KDoc does not contain single line with words 'return', 'get' or 'set' - name: KDOC_TRIVIAL_KDOC_ON_FUNCTION enabled: true # Checks that there is newline after header KDoc - name: HEADER_WRONG_FORMAT enabled: true # Checks that file with zero or >1 classes has header KDoc - name: HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE enabled: true # Checks that copyright exists on top of file and is properly formatted (as a block comment) - name: HEADER_MISSING_OR_WRONG_COPYRIGHT enabled: true configuration: isCopyrightMandatory: true copyrightText: ' Copyright (c) Huawei Technologies Co., Ltd. 2012-;@currYear;. All rights reserved.' # Checks that header kdoc is located before package directive - name: HEADER_NOT_BEFORE_PACKAGE enabled: true # Checks that file does not contain lines > maxSize - name: FILE_IS_TOO_LONG enabled: true configuration: # number of lines maxSize: '2000' # Checks that file does not contain commented out code - name: COMMENTED_OUT_CODE enabled: true # Checks that file does not contain only comments, imports and package directive - name: FILE_CONTAINS_ONLY_COMMENTS enabled: true # Orders imports alphabetically - name: FILE_UNORDERED_IMPORTS enabled: true configuration: # use logical imports grouping with sorting inside of a group useRecommendedImportsOrder: true # Checks that general order of code parts is right - name: FILE_INCORRECT_BLOCKS_ORDER enabled: true # Checks that there is exactly one line between code blocks - name: FILE_NO_BLANK_LINE_BETWEEN_BLOCKS enabled: true # Checks that there is no wildcard imports. Exception: allowedWildcards - name: FILE_WILDCARD_IMPORTS enabled: true configuration: allowedWildcards: "" # Allowed wildcards for imports (e.g. "import com.saveourtool.diktat.*, import org.jetbrains.kotlin.*") useRecommendedImportsOrder: true # Checks unused imports - name: UNUSED_IMPORT enabled: true configuration: deleteUnusedImport: true # Checks that braces are used in if, else, when, for, do, and while statements. Exception: single line ternary operator statement - name: NO_BRACES_IN_CONDITIONALS_AND_LOOPS enabled: true # Checks that the declaration part of a class-like code structures (class/interface/etc.) is in the proper order - name: WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES enabled: true # Checks that properties with comments are separated by a blank line - name: BLANK_LINE_BETWEEN_PROPERTIES enabled: true # Checks top level order - name: TOP_LEVEL_ORDER enabled: true # Checks that non-empty code blocks with braces follow the K&R style (1TBS or OTBS style) - name: BRACES_BLOCK_STRUCTURE_ERROR enabled: true configuration: openBraceNewline: 'True' closeBraceNewline: 'True' # Checks that indentation is correct - name: WRONG_INDENTATION enabled: true configuration: # Is newline at the end of a file needed newlineAtEnd: true # If true: in parameter list when parameters are split by newline they are indented with two indentations instead of one extendedIndentOfParameters: false # If true: if first parameter in parameter list is on the same line as opening parenthesis, then other parameters can be aligned with it alignedParameters: true # If true, expression bodies which begin on a separate line are indented # using a continuation indent. The default is false. # # This flag is called CONTINUATION_INDENT_FOR_EXPRESSION_BODIES in IDEA and # ij_kotlin_continuation_indent_for_expression_bodies in .editorconfig. extendedIndentForExpressionBodies: false # If true: if expression is split by newline after operator like +/-/`*`, then the next line is indented with two indentations instead of one extendedIndentAfterOperators: true # If true: when dot qualified expression starts on a new line, this line will be indented with two indentations instead of one extendedIndentBeforeDot: false # The indentation size for each file indentationSize: 4 # Checks that there is no empty blocks in a file. # If allowEmptyBlocks is true, checks that it follows correct style (have a newline) - name: EMPTY_BLOCK_STRUCTURE_ERROR enabled: true configuration: # Whether a newline after `{` is required in an empty block styleEmptyBlockWithNewline: 'True' allowEmptyBlocks: 'False' # Checks that there is no more than one statement per line - name: MORE_THAN_ONE_STATEMENT_PER_LINE enabled: true # Checks that the line length is < lineLength parameter - name: LONG_LINE enabled: true configuration: lineLength: '120' # Checks that semicolons are not used at the end of a line - name: REDUNDANT_SEMICOLON enabled: true # Checks that line breaks follow code style guide: rule 3.6 - name: WRONG_NEWLINES enabled: true configuration: # If the number of parameters on one line is more than this threshold, all parameters will be placed on separate lines. maxParametersInOneLine: 2 # 3 by default. maxCallsInOneLine: 3 # Checks trailing comma - name: TRAILING_COMMA enabled: true configuration: # VALUE_ARGUMENT valueArgument: false # VALUE_PARAMETER valueParameter: false # REFERENCE_EXPRESSION indices: false # WHEN_CONDITION_WITH_EXPRESSION whenConditions: false # STRING_TEMPLATE collectionLiteral: false # TYPE_PROJECTION typeArgument: false # TYPE_PARAMETER typeParameter: false # DESTRUCTURING_DECLARATION_ENTRY destructuringDeclaration: false # Checks that there are not too many consecutive spaces in line - name: TOO_MANY_CONSECUTIVE_SPACES enabled: true configuration: # Maximum allowed number of consecutive spaces (not counting indentation) maxSpaces: '1' # Whether formatting for enums should be kept without checking saveInitialFormattingForEnums: false # Inspection that checks if a long dot qualified expression is used in condition or as an argument - name: COMPLEX_EXPRESSION enabled: true # Checks that blank lines are used correctly. # For example: triggers when there are too many blank lines between function declaration - name: TOO_MANY_BLANK_LINES enabled: true # Checks that usage of horizontal spaces doesn't violate code style guide - name: WRONG_WHITESPACE enabled: true # Checks that backticks (``) are not used in the identifier name, except the case when it is test method (marked with @Test annotation) - name: BACKTICKS_PROHIBITED enabled: true # Checks that a single line concatenation of strings is not used - name: STRING_CONCATENATION enabled: true # Checks that each when statement have else in the end - name: WHEN_WITHOUT_ELSE enabled: true # Checks that annotation is on a single line - name: ANNOTATION_NEW_LINE enabled: true # Checks that method annotated with `Preview` annotation is private and has Preview suffix - name: PREVIEW_ANNOTATION enabled: true # Checks that enum structure is correct: enum entries should be separated by comma and line break and last entry should have semicolon in the end. - name: ENUMS_SEPARATED enabled: true # Checks that value on integer or float constant is not too big - name: LONG_NUMERICAL_VALUES_SEPARATED enabled: true configuration: # Maximum number of digits which are not split maxNumberLength: '5' # Maximum number of digits between separators maxBlockLength: '3' # Checks magic number - name: MAGIC_NUMBER enabled: true configuration: # Ignore numbers from test ignoreTest: "true" # Ignore numbers ignoreNumbers: "-1, 1, 0, 2, 0U, 1U, 2U, -1L, 0L, 1L, 2L, 0UL, 1UL, 2UL" # Is ignore override hashCode function ignoreHashCodeFunction: "true" # Is ignore property ignorePropertyDeclaration: "false" # Is ignore local variable ignoreLocalVariableDeclaration: "false" # Is ignore value parameter ignoreValueParameter: "true" # Is ignore constant ignoreConstantDeclaration: "true" # Is ignore property in companion object ignoreCompanionObjectPropertyDeclaration: "true" # Is ignore numbers in enum ignoreEnums: "false" # Is ignore number in ranges ignoreRanges: "false" # Is ignore number in extension function ignoreExtensionFunctions: "false" # Is ignore number in pairs created using to ignorePairsCreatedUsingTo: "false" # Checks that order of enum values or constant property inside companion is correct - name: WRONG_DECLARATIONS_ORDER enabled: true configuration: # Whether enum members should be sorted alphabetically sortEnum: true # Whether class properties should be sorted alphabetically sortProperty: true # Checks that multiple modifiers sequence is in the correct order - name: WRONG_MULTIPLE_MODIFIERS_ORDER enabled: true # Checks that identifier has appropriate name (See table of rule 1.2 part 6) - name: CONFUSING_IDENTIFIER_NAMING enabled: true # Checks year in the copyright - name: WRONG_COPYRIGHT_YEAR enabled: true # Inspection that checks if local variables are declared close to the first usage site - name: LOCAL_VARIABLE_EARLY_DECLARATION enabled: true # Try to avoid initialize val by null (e.g. val a: Int? = null -> val a: Int = 0) - name: NULLABLE_PROPERTY_TYPE enabled: true # Inspection that checks if there is a blank line before kDoc and none after - name: WRONG_NEWLINES_AROUND_KDOC enabled: true # Inspection that checks if there is no blank lines before first comment - name: FIRST_COMMENT_NO_BLANK_LINE enabled: true # Inspection that checks if there are blank lines between code and comment and between code start token and comment's text - name: COMMENT_WHITE_SPACE enabled: true configuration: maxSpacesBeforeComment: 2 maxSpacesInComment: 1 # Inspection that checks if all comment's are inside if-else code blocks. Exception is general if comment - name: IF_ELSE_COMMENTS enabled: true # Type aliases provide alternative names for existing types when type's reference text is longer 25 chars - name: TYPE_ALIAS enabled: true configuration: typeReferenceLength: '25' # max length of type reference # Checks if casting can be omitted - name: SMART_CAST_NEEDED enabled: true # Checks that variables of generic types have explicit type declaration - name: GENERIC_VARIABLE_WRONG_DECLARATION enabled: true # Inspection that checks if string template has redundant curly braces - name: STRING_TEMPLATE_CURLY_BRACES enabled: true # Variables with `val` modifier - are immutable (read-only). Usage of such variables instead of `var` variables increases # robustness and readability of code, because `var` variables can be reassigned several times in the business logic. # This rule prohibits usage of `var`s as local variables - the only exception is accumulators and counters - name: SAY_NO_TO_VAR enabled: true # Inspection that checks if string template has redundant quotes - name: STRING_TEMPLATE_QUOTES enabled: true # Check if there are redundant nested if-statements, which could be collapsed into a single one by concatenating their conditions - name: COLLAPSE_IF_STATEMENTS enabled: true configuration: startCollapseFromNestedLevel: 2 # Checks that floating-point values are not used in arithmetic expressions - name: FLOAT_IN_ACCURATE_CALCULATIONS enabled: true # Checks that function length isn't too long - name: TOO_LONG_FUNCTION enabled: true configuration: maxFunctionLength: 30 # max length of function isIncludeHeader: false # count function's header # Warns if there are nested functions - name: AVOID_NESTED_FUNCTIONS enabled: true # Checks that lambda inside function parameters is in the end - name: LAMBDA_IS_NOT_LAST_PARAMETER enabled: true # Checks that function doesn't contains too many parameters - name: TOO_MANY_PARAMETERS enabled: true configuration: maxParameterListSize: '5' # max parameters size # Checks that function doesn't have too many nested blocks - name: NESTED_BLOCK enabled: true configuration: maxNestedBlockQuantity: '4' # Checks that function use default values, instead overloading - name: WRONG_OVERLOADING_FUNCTION_ARGUMENTS enabled: true # Checks that using runBlocking inside async block code - name: RUN_BLOCKING_INSIDE_ASYNC enabled: true # Checks that the long lambda has parameters - name: TOO_MANY_LINES_IN_LAMBDA enabled: true configuration: maxLambdaLength: 10 # max length of lambda without parameters # Checks that using unnecessary, custom label - name: CUSTOM_LABEL enabled: true # Check that lambda with inner lambda doesn't use implicit parameter - name: PARAMETER_NAME_IN_OUTER_LAMBDA enabled: true configuration: strictMode: true # don't let outer lambdas have `it` as parameter # Checks that property in constructor doesn't contains comment - name: KDOC_NO_CONSTRUCTOR_PROPERTY enabled: true configuration: isParamTagsForParameters: false # create param tags for parameters without val or var isParamTagsForPrivateProperties: false # create param tags for private properties isParamTagsForGenericTypes: false # create param tags for generic types # Checks that property in KDoc present in class - name: KDOC_EXTRA_PROPERTY enabled: true # There's a property in KDoc which is already present - name: KDOC_DUPLICATE_PROPERTY enabled: true # Checks that KDoc in constructor has property tag but with comment inside constructor - name: KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT enabled: true # if a class has single constructor, it should be converted to a primary constructor - name: SINGLE_CONSTRUCTOR_SHOULD_BE_PRIMARY enabled: true # Checks if class can be made as data class - name: USE_DATA_CLASS enabled: true # Checks that never use the name of a variable in the custom getter or setter - name: WRONG_NAME_OF_VARIABLE_INSIDE_ACCESSOR enabled: true # Checks that classes have only one init block - name: MULTIPLE_INIT_BLOCKS enabled: true # Checks that there are abstract functions in abstract class - name: CLASS_SHOULD_NOT_BE_ABSTRACT enabled: true # Checks if there are any trivial getters or setters - name: TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED enabled: true # Checks that no custom getters and setters are used for properties. It is a more wide rule than TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED # Kotlin compiler automatically generates `get` and `set` methods for properties and also lets the possibility to override it. # But in all cases it is very confusing when `get` and `set` are overridden for a developer who uses this particular class. # Developer expects to get the value of the property, but receives some unknown value and some extra side effect hidden by the custom getter/setter. # Use extra functions for it instead. - name: CUSTOM_GETTERS_SETTERS enabled: true # Checks if null-check was used explicitly (for example: if (a == null)) # Try to avoid explicit null checks (explicit comparison with `null`) # Kotlin is declared as [Null-safe](https://kotlinlang.org/docs/reference/null-safety.html) language. # But Kotlin architects wanted Kotlin to be fully compatible with Java, that's why `null` keyword was also introduced in Kotlin. # There are several code-structures that can be used in Kotlin to avoid null-checks. For example: `?:`, `.let {}`, `.also {}`, e.t.c - name: AVOID_NULL_CHECKS enabled: true # Checks if class instantiation can be wrapped in `apply` for better readability - name: COMPACT_OBJECT_INITIALIZATION enabled: true # Checks explicit supertype qualification - name: USELESS_SUPERTYPE enabled: true # Checks if extension function with the same signature don't have related classes - name: EXTENSION_FUNCTION_SAME_SIGNATURE enabled: true # Checks if there is empty primary constructor - name: EMPTY_PRIMARY_CONSTRUCTOR enabled: true # In case of not using field keyword in property accessors, # there should be explicit backing property with the name of real property # Example: val table get() {if (_table == null) ...} -> table should have _table - name: NO_CORRESPONDING_PROPERTY enabled: true # Checks if there is class/object that can be replace with extension function - name: AVOID_USING_UTILITY_CLASS enabled: true # If there is stateless class it is preferred to use object - name: OBJECT_IS_PREFERRED enabled: true # If there exists negated version of function you should prefer it instead of !functionCall - name: INVERSE_FUNCTION_PREFERRED enabled: true # Checks if class can be converted to inline class - name: INLINE_CLASS_CAN_BE_USED enabled: true # If file contains class, then it can't contain extension functions for the same class - name: EXTENSION_FUNCTION_WITH_CLASS enabled: true # Check if kts script contains other functions except run code - name: RUN_IN_SCRIPT enabled: true # Check if boolean expression can be simplified - name: COMPLEX_BOOLEAN_EXPRESSION enabled: true # Check if range can replace with until or `rangeTo` function with range - name: CONVENTIONAL_RANGE enabled: true configuration: isRangeToIgnore: false # Check if there is a call of print()\println() or console.log(). Assumption that it's a debug print - name: DEBUG_PRINT enabled: true # Check that typealias name is in PascalCase - name: TYPEALIAS_NAME_INCORRECT_CASE enabled: true # Should change property length - 1 to property lastIndex - name: USE_LAST_INDEX enabled: true # Only properties from the primary constructor should be documented in a @property tag in class KDoc - name: KDOC_NO_CLASS_BODY_PROPERTIES_IN_HEADER enabled: true ================================================ FILE: diktat-rules/src/main/resources/diktat-analysis.yml ================================================ # Common configuration - name: DIKTAT_COMMON configuration: # put your package name here - it will be autofixed and checked domainName: your.name.here testDirs: test # expected values: disabledChapters: "Naming, Comments, General, Variables, Functions, Classes" # or: "1, 2, 3, 4, 5, 6" disabledChapters: "" kotlinVersion: 2.1 srcDirectories: "main" # Checks that the Class/Enum/Interface name matches Pascal case - name: CLASS_NAME_INCORRECT enabled: true # all code blocks with MyAnnotation will be ignored and not checked ignoreAnnotated: [ MyAnnotation ] # Checks that CONSTANT (treated as const val from companion object or class level) is in non UPPER_SNAKE_CASE - name: CONSTANT_UPPERCASE enabled: true # Checks that enum value is in upper SNAKE_CASE or in PascalCase depending on the config. UPPER_SNAKE_CASE is the default, but can be changed by 'enumStyle' config - name: ENUM_VALUE enabled: true configuration: # Two options: SNAKE_CASE (default), PascalCase enumStyle: SNAKE_CASE # Checks that class which extends any Exception class has Exception suffix - name: EXCEPTION_SUFFIX enabled: true # Checks that file name has extension - name: FILE_NAME_INCORRECT enabled: true # Checks that file name matches class name, if it is only one class in file - name: FILE_NAME_MATCH_CLASS enabled: true # Checks that functions/methods which return boolean have special prefix like "is/should/e.t.c" - name: FUNCTION_BOOLEAN_PREFIX enabled: true configuration: allowedPrefixes: "" # A list of functions that return boolean and are allowed to use. Input is in a form "foo, bar". # Checks that function/method name is in lowerCamelCase - name: FUNCTION_NAME_INCORRECT_CASE enabled: true # Checks that typealias name is in PascalCase - name: TYPEALIAS_NAME_INCORRECT_CASE enabled: true # Checks that generic name doesn't contain more than 1 letter (capital). It can be followed by numbers, example: T12, T - name: GENERIC_NAME enabled: true # Identifier length should be in range [2,64] except names that used in industry like {i, j} and 'e' for catching exceptions - name: IDENTIFIER_LENGTH enabled: true # Checks that the object matches PascalCase - name: OBJECT_NAME_INCORRECT enabled: true # Checks that package name is in correct (lower) case - name: PACKAGE_NAME_INCORRECT_CASE enabled: true # Checks that package name starts with the company's domain - name: PACKAGE_NAME_INCORRECT_PREFIX enabled: false # Checks that package name does not have incorrect symbols like underscore or non-ASCII letters/digits - name: PACKAGE_NAME_INCORRECT_SYMBOLS enabled: true # Checks that the path for a file matches with a package name - name: PACKAGE_NAME_INCORRECT_PATH enabled: false # Checks that package name is in the file - name: PACKAGE_NAME_MISSING enabled: true # Checks that variable does not have prefix (like mVariable or M_VARIABLE) - name: VARIABLE_HAS_PREFIX enabled: true # Checks that variable does not contain one single letter, only exceptions are fixed names that used in industry like {i, j} - name: VARIABLE_NAME_INCORRECT enabled: true # Checks that the name of variable is in lowerCamelCase and contains only ASCII letters - name: VARIABLE_NAME_INCORRECT_FORMAT enabled: true # Checks that functions have kdoc - name: MISSING_KDOC_ON_FUNCTION enabled: true # Checks that on file level internal or public class or function has missing KDoc - name: MISSING_KDOC_TOP_LEVEL enabled: true # Checks that accessible internal elements (protected, public, internal) in a class are documented - name: MISSING_KDOC_CLASS_ELEMENTS enabled: true # Checks that accessible method parameters are documented in KDoc - name: KDOC_WITHOUT_PARAM_TAG enabled: true # Checks that accessible method explicit return type is documented in KDoc - name: KDOC_WITHOUT_RETURN_TAG enabled: true # Checks that accessible method throw keyword is documented in KDoc - name: KDOC_WITHOUT_THROWS_TAG enabled: true # Checks that KDoc is not empty - name: KDOC_EMPTY_KDOC enabled: true # Checks that underscore is correctly used to split package naming - name: INCORRECT_PACKAGE_SEPARATOR enabled: true # Checks that code block doesn't contain kdoc comments - name: COMMENTED_BY_KDOC enabled: true # Checks that there is no @deprecated tag in kdoc - name: KDOC_NO_DEPRECATED_TAG enabled: true # Checks that there is no empty content in kdoc tags - name: KDOC_NO_EMPTY_TAGS enabled: true # Checks that there is only one space after kdoc tag - name: KDOC_WRONG_SPACES_AFTER_TAG enabled: true # Checks tags order in kDoc. `@param`, `@return`, `@throws` - name: KDOC_WRONG_TAGS_ORDER enabled: true # Checks that there is no newline of empty KDoc line (with leading asterisk) between `@param`, `@return`, `@throws` tags - name: KDOC_NO_NEWLINES_BETWEEN_BASIC_TAGS enabled: true # Checks that block of tags @param, @return, @throws is separated from previous part of KDoc by exactly one empty line - name: KDOC_NEWLINES_BEFORE_BASIC_TAGS enabled: true # Checks that special tags `@apiNote`, `@implNote`, `@implSpec` have exactly one empty line after - name: KDOC_NO_NEWLINE_AFTER_SPECIAL_TAGS enabled: true # Checks that kdoc does not contain @author tag or date - name: KDOC_CONTAINS_DATE_OR_AUTHOR enabled: true configuration: versionRegex: \d+\.\d+\.\d+[-.\w\d]* # Checks that KDoc does not contain single line with words 'return', 'get' or 'set' - name: KDOC_TRIVIAL_KDOC_ON_FUNCTION enabled: true # Checks that there is newline after header KDoc - name: HEADER_WRONG_FORMAT enabled: true # Checks that file with zero or >1 classes has header KDoc - name: HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE enabled: true # Checks that copyright exists on top of file and is properly formatted (as a block comment) - name: HEADER_MISSING_OR_WRONG_COPYRIGHT enabled: true configuration: isCopyrightMandatory: false copyrightText: 'Copyright (c) Your Company Name Here. 2010-;@currYear;' # Checks that header kdoc is located before package directive - name: HEADER_NOT_BEFORE_PACKAGE enabled: true # Checks that file does not contain lines > maxSize - name: FILE_IS_TOO_LONG enabled: true configuration: # number of lines maxSize: '2000' # Checks that file does not contain commented out code - name: COMMENTED_OUT_CODE enabled: true # Checks that file does not contain only comments, imports and package directive - name: FILE_CONTAINS_ONLY_COMMENTS enabled: true # Orders imports alphabetically - name: FILE_UNORDERED_IMPORTS enabled: true configuration: # use logical imports grouping with sorting inside of a group useRecommendedImportsOrder: true # Checks that general order of code parts is right - name: FILE_INCORRECT_BLOCKS_ORDER enabled: true # Checks that there is exactly one line between code blocks - name: FILE_NO_BLANK_LINE_BETWEEN_BLOCKS enabled: true # Checks that there is no wildcard imports. Exception: allowedWildcards - name: FILE_WILDCARD_IMPORTS enabled: true configuration: allowedWildcards: "" # Allowed wildcards for imports (e.g. "import com.saveourtool.diktat.*, import org.jetbrains.kotlin.*") useRecommendedImportsOrder: true # Checks unused imports - name: UNUSED_IMPORT enabled: true configuration: deleteUnusedImport: true # Checks that braces are used in if, else, when, for, do, and while statements. Exception: single line ternary operator statement - name: NO_BRACES_IN_CONDITIONALS_AND_LOOPS enabled: true # Checks that the declaration part of a class-like code structures (class/interface/etc.) is in the proper order - name: WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES enabled: true # Checks that properties with comments are separated by a blank line - name: BLANK_LINE_BETWEEN_PROPERTIES enabled: true # Checks top level order - name: TOP_LEVEL_ORDER enabled: true # Checks that non-empty code blocks with braces follow the K&R style (1TBS or OTBS style) - name: BRACES_BLOCK_STRUCTURE_ERROR enabled: true configuration: openBraceNewline: 'True' closeBraceNewline: 'True' # Checks that indentation is correct - name: WRONG_INDENTATION enabled: true configuration: # Is newline at the end of a file needed newlineAtEnd: true # If true: in parameter list when parameters are split by newline they are indented with two indentations instead of one extendedIndentOfParameters: false # If true: if first parameter in parameter list is on the same line as opening parenthesis, then other parameters can be aligned with it alignedParameters: true # If true, expression bodies which begin on a separate line are indented # using a continuation indent. The default is false. # # This flag is called CONTINUATION_INDENT_FOR_EXPRESSION_BODIES in IDEA and # ij_kotlin_continuation_indent_for_expression_bodies in .editorconfig. extendedIndentForExpressionBodies: false # If true: if expression is split by newline after operator like +/-/`*`, then the next line is indented with two indentations instead of one extendedIndentAfterOperators: true # If true: when dot qualified expression starts on a new line, this line will be indented with two indentations instead of one extendedIndentBeforeDot: false # The indentation size for each file indentationSize: 4 # Checks that there is no empty blocks in a file. # If allowEmptyBlocks is true, checks that it follows correct style (have a newline) - name: EMPTY_BLOCK_STRUCTURE_ERROR enabled: true configuration: # Whether a newline after `{` is required in an empty block styleEmptyBlockWithNewline: 'True' allowEmptyBlocks: 'False' # Checks that there is no more than one statement per line - name: MORE_THAN_ONE_STATEMENT_PER_LINE enabled: true # Checks that the line length is < lineLength parameter - name: LONG_LINE enabled: true configuration: lineLength: '120' # Checks that semicolons are not used at the end of a line - name: REDUNDANT_SEMICOLON enabled: true # Checks that line breaks follow code style guide: rule 3.6 - name: WRONG_NEWLINES enabled: true configuration: # If the number of parameters on one line is more than this threshold, all parameters will be placed on separate lines. maxParametersInOneLine: 2 # 3 by default. maxCallsInOneLine: 3 # Checks trailing comma - name: TRAILING_COMMA enabled: true configuration: # VALUE_ARGUMENT valueArgument: false # VALUE_PARAMETER valueParameter: false # REFERENCE_EXPRESSION indices: false # WHEN_CONDITION_WITH_EXPRESSION whenConditions: false # STRING_TEMPLATE collectionLiteral: false # TYPE_PROJECTION typeArgument: false # TYPE_PARAMETER typeParameter: false # DESTRUCTURING_DECLARATION_ENTRY destructuringDeclaration: false # Checks that there are not too many consecutive spaces in line - name: TOO_MANY_CONSECUTIVE_SPACES enabled: true configuration: # Maximum allowed number of consecutive spaces (not counting indentation) maxSpaces: '1' # Whether formatting for enums should be kept without checking saveInitialFormattingForEnums: false # Inspection that checks if a long dot qualified expression is used in condition or as an argument - name: COMPLEX_EXPRESSION enabled: true # Checks that blank lines are used correctly. # For example: triggers when there are too many blank lines between function declaration - name: TOO_MANY_BLANK_LINES enabled: true # Checks that usage of horizontal spaces doesn't violate code style guide - name: WRONG_WHITESPACE enabled: true # Checks that backticks (``) are not used in the identifier name, except the case when it is test method (marked with @Test annotation) - name: BACKTICKS_PROHIBITED enabled: true # Checks that a single line concatenation of strings is not used - name: STRING_CONCATENATION enabled: true # Checks that each when statement have else in the end - name: WHEN_WITHOUT_ELSE enabled: true # Checks that annotation is on a single line - name: ANNOTATION_NEW_LINE enabled: true # Checks that method annotated with `Preview` annotation is private and has Preview suffix - name: PREVIEW_ANNOTATION enabled: true # Checks that enum structure is correct: enum entries should be separated by comma and line break and last entry should have semicolon in the end. - name: ENUMS_SEPARATED enabled: true # Checks that value on integer or float constant is not too big - name: LONG_NUMERICAL_VALUES_SEPARATED enabled: true configuration: # Maximum number of digits which are not split maxNumberLength: '5' # Maximum number of digits between separators maxBlockLength: '3' # Checks magic number - name: MAGIC_NUMBER enabled: true configuration: # Ignore numbers from test ignoreTest: "true" # Ignore numbers ignoreNumbers: "-1, 1, 0, 2, 0U, 1U, 2U, -1L, 0L, 1L, 2L, 0UL, 1UL, 2UL" # Is ignore override hashCode function ignoreHashCodeFunction: "true" # Is ignore property ignorePropertyDeclaration: "false" # Is ignore local variable ignoreLocalVariableDeclaration: "false" # Is ignore value parameter ignoreValueParameter: "true" # Is ignore constant ignoreConstantDeclaration: "true" # Is ignore property in companion object ignoreCompanionObjectPropertyDeclaration: "true" # Is ignore numbers in enum ignoreEnums: "false" # Is ignore number in ranges ignoreRanges: "false" # Is ignore number in extension function ignoreExtensionFunctions: "false" # Is ignore number in pairs created using to ignorePairsCreatedUsingTo: "false" # Checks that order of enum values or constant property inside companion is correct - name: WRONG_DECLARATIONS_ORDER enabled: true configuration: # Whether enum members should be sorted alphabetically sortEnum: true # Whether class properties should be sorted alphabetically sortProperty: true # Checks that multiple modifiers sequence is in the correct order - name: WRONG_MULTIPLE_MODIFIERS_ORDER enabled: true # Checks that identifier has appropriate name (See table of rule 1.2 part 6) - name: CONFUSING_IDENTIFIER_NAMING enabled: true # Checks year in the copyright - name: WRONG_COPYRIGHT_YEAR enabled: true # Inspection that checks if local variables are declared close to the first usage site - name: LOCAL_VARIABLE_EARLY_DECLARATION enabled: true # Try to avoid initialize val by null (e.g. val a: Int? = null -> val a: Int = 0) - name: NULLABLE_PROPERTY_TYPE enabled: true # Inspection that checks if there is a blank line before kDoc and none after - name: WRONG_NEWLINES_AROUND_KDOC enabled: true # Inspection that checks if there is no blank lines before first comment - name: FIRST_COMMENT_NO_BLANK_LINE enabled: true # Inspection that checks if there are blank lines between code and comment and between code start token and comment's text - name: COMMENT_WHITE_SPACE enabled: true configuration: maxSpacesBeforeComment: 2 maxSpacesInComment: 1 # Inspection that checks if all comment's are inside if-else code blocks. Exception is general if comment - name: IF_ELSE_COMMENTS enabled: true # Type aliases provide alternative names for existing types when type's reference text is longer 25 chars - name: TYPE_ALIAS enabled: true configuration: typeReferenceLength: '25' # max length of type reference # Checks if casting can be omitted - name: SMART_CAST_NEEDED enabled: true # Checks that variables of generic types have explicit type declaration - name: GENERIC_VARIABLE_WRONG_DECLARATION enabled: true # Inspection that checks if string template has redundant curly braces - name: STRING_TEMPLATE_CURLY_BRACES enabled: true # Variables with `val` modifier - are immutable (read-only). Usage of such variables instead of `var` variables increases # robustness and readability of code, because `var` variables can be reassigned several times in the business logic. # This rule prohibits usage of `var`s as local variables - the only exception is accumulators and counters - name: SAY_NO_TO_VAR enabled: true # Inspection that checks if string template has redundant quotes - name: STRING_TEMPLATE_QUOTES enabled: true # Check if there are redundant nested if-statements, which could be collapsed into a single one by concatenating their conditions - name: COLLAPSE_IF_STATEMENTS enabled: true configuration: startCollapseFromNestedLevel: 2 # Checks that floating-point values are not used in arithmetic expressions - name: FLOAT_IN_ACCURATE_CALCULATIONS enabled: true # Checks that function length isn't too long - name: TOO_LONG_FUNCTION enabled: true configuration: maxFunctionLength: '30' # max length of function isIncludeHeader: 'false' # count function's header # Warns if there are nested functions - name: AVOID_NESTED_FUNCTIONS enabled: true # Checks that lambda inside function parameters is in the end - name: LAMBDA_IS_NOT_LAST_PARAMETER enabled: true # Checks that function doesn't contains too many parameters - name: TOO_MANY_PARAMETERS enabled: true configuration: maxParameterListSize: '5' # max parameters size # Checks that function doesn't have too many nested blocks - name: NESTED_BLOCK enabled: true configuration: maxNestedBlockQuantity: '4' # Checks that function use default values, instead overloading - name: WRONG_OVERLOADING_FUNCTION_ARGUMENTS enabled: true # Checks that using runBlocking inside async block code - name: RUN_BLOCKING_INSIDE_ASYNC enabled: true # Checks that property in constructor doesn't contain comment - name: KDOC_NO_CONSTRUCTOR_PROPERTY enabled: true configuration: isParamTagsForParameters: true # create param tags for parameters without val or var isParamTagsForPrivateProperties: true # create param tags for private properties isParamTagsForGenericTypes: true # create param tags for generic types # Checks that the long lambda has parameters - name: TOO_MANY_LINES_IN_LAMBDA enabled: true configuration: maxLambdaLength: 10 # max length of lambda without parameters # Checks that using unnecessary, custom label - name: CUSTOM_LABEL enabled: true # Check that lambda with inner lambda doesn't use implicit parameter - name: PARAMETER_NAME_IN_OUTER_LAMBDA enabled: true configuration: strictMode: true # don't let outer lambdas have `it` as parameter # Checks that property in KDoc present in class - name: KDOC_EXTRA_PROPERTY enabled: true # There's a property in KDoc which is already present - name: KDOC_DUPLICATE_PROPERTY enabled: true # Checks that KDoc in constructor has property tag but with comment inside constructor - name: KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT enabled: true # if a class has single constructor, it should be converted to a primary constructor - name: SINGLE_CONSTRUCTOR_SHOULD_BE_PRIMARY enabled: true # Checks if class can be made as data class - name: USE_DATA_CLASS enabled: true # Checks that never use the name of a variable in the custom getter or setter - name: WRONG_NAME_OF_VARIABLE_INSIDE_ACCESSOR enabled: true # Checks that classes have only one init block - name: MULTIPLE_INIT_BLOCKS enabled: true # Checks that there are abstract functions in abstract class - name: CLASS_SHOULD_NOT_BE_ABSTRACT enabled: true # Checks if there are any trivial getters or setters - name: TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED enabled: true # Checks that no custom getters and setters are used for properties. It is a more wide rule than TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED # Kotlin compiler automatically generates `get` and `set` methods for properties and also lets the possibility to override it. # But in all cases it is very confusing when `get` and `set` are overridden for a developer who uses this particular class. # Developer expects to get the value of the property, but receives some unknown value and some extra side effect hidden by the custom getter/setter. # Use extra functions for it instead. - name: CUSTOM_GETTERS_SETTERS enabled: true # Checks if null-check was used explicitly (for example: if (a == null)) # Try to avoid explicit null checks (explicit comparison with `null`) # Kotlin is declared as [Null-safe](https://kotlinlang.org/docs/reference/null-safety.html) language. # But Kotlin architects wanted Kotlin to be fully compatible with Java, that's why `null` keyword was also introduced in Kotlin. # There are several code-structures that can be used in Kotlin to avoid null-checks. For example: `?:`, `.let {}`, `.also {}`, e.t.c - name: AVOID_NULL_CHECKS enabled: true # Checks if class instantiation can be wrapped in `apply` for better readability - name: COMPACT_OBJECT_INITIALIZATION enabled: true # Checks explicit supertype qualification - name: USELESS_SUPERTYPE enabled: true # Checks if extension function with the same signature don't have related classes - name: EXTENSION_FUNCTION_SAME_SIGNATURE enabled: true # Checks if there is empty primary constructor - name: EMPTY_PRIMARY_CONSTRUCTOR enabled: true # In case of not using field keyword in property accessors, # there should be explicit backing property with the name of real property # Example: val table get() {if (_table == null) ...} -> table should have _table - name: NO_CORRESPONDING_PROPERTY enabled: true # Checks if there is class/object that can be replace with extension function - name: AVOID_USING_UTILITY_CLASS enabled: true # If there is stateless class it is preferred to use object - name: OBJECT_IS_PREFERRED enabled: true # If there exists negated version of function you should prefer it instead of !functionCall - name: INVERSE_FUNCTION_PREFERRED enabled: true # Checks if class can be converted to inline class - name: INLINE_CLASS_CAN_BE_USED enabled: true # If file contains class, then it can't contain extension functions for the same class - name: EXTENSION_FUNCTION_WITH_CLASS enabled: true # Check if kts script contains other functions except run code - name: RUN_IN_SCRIPT enabled: true # Check if boolean expression can be simplified - name: COMPLEX_BOOLEAN_EXPRESSION enabled: true # Check if range can replace with until or `rangeTo` function with range - name: CONVENTIONAL_RANGE enabled: true configuration: isRangeToIgnore: false # Check if there is a call of print()\println() or console.log(). Assumption that it's a debug print - name: DEBUG_PRINT enabled: true # Check that typealias name is in PascalCase - name: TYPEALIAS_NAME_INCORRECT_CASE enabled: true # Should change property length - 1 to property lastIndex - name: USE_LAST_INDEX enabled: true # Only properties from the primary constructor should be documented in a @property tag in class KDoc - name: KDOC_NO_CLASS_BODY_PROPERTIES_IN_HEADER enabled: true ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter1/EnumValueCaseTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter1 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.common.config.rules.getRuleConfig import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter1.IdentifierNaming import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows class EnumValueCaseTest : FixTestBase("test/paragraph1/naming", ::IdentifierNaming) { private val rulesConfigSnakeCaseEnum: List = listOf( RulesConfig(Warnings.ENUM_VALUE.name, true, mapOf("enumStyle" to "snakeCase")) ) private val rulesConfigPascalCaseEnum: List = listOf( RulesConfig(Warnings.ENUM_VALUE.name, true, mapOf("enumStyle" to "pascalCase")) ) private val rulesConfigEnumUnknownStyle: List = listOf( RulesConfig(Warnings.ENUM_VALUE.name, true, mapOf("enumStyle" to "otherCase")) ) @Test @Tag(WarningNames.ENUM_VALUE) fun `incorrect enum snake case value fix`() { fixAndCompare("enum_/EnumValueSnakeCaseExpected.kt", "enum_/EnumValueSnakeCaseTest.kt", rulesConfigSnakeCaseEnum) } @Test @Tag(WarningNames.ENUM_VALUE) fun `incorrect enum pascal case value fix`() { fixAndCompare("enum_/EnumValuePascalCaseExpected.kt", "enum_/EnumValuePascalCaseTest.kt", rulesConfigPascalCaseEnum) } @Test @Tag(WarningNames.ENUM_VALUE) fun `incorrect enum unknown style`() { assertThrows { IdentifierNaming.IdentifierNamingConfiguration( rulesConfigEnumUnknownStyle.getRuleConfig(Warnings.ENUM_VALUE) ?.configuration ?: emptyMap() ) } } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter1/IdentifierNamingFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter1 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.rules.chapter1.IdentifierNaming import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class IdentifierNamingFixTest : FixTestBase( "test/paragraph1/naming", ::IdentifierNaming, listOf( RulesConfig("PACKAGE_NAME_INCORRECT", false, emptyMap()), RulesConfig("PACKAGE_NAME_INCORRECT_PREFIX", false, emptyMap()) ) ) { @Test @Tag(WarningNames.CLASS_NAME_INCORRECT) fun `regression in VARIABLE_NAME_INCORRECT_FORMAT`() { fixAndCompare("identifiers/IdentifierNameRegressionExpected.kt", "identifiers/IdentifierNameRegressionTest.kt") } @Test @Tag(WarningNames.CLASS_NAME_INCORRECT) fun `incorrect class name fix`() { fixAndCompare("class_/IncorrectClassNameExpected.kt", "class_/IncorrectClassNameTest.kt") } @Test @Tag(WarningNames.OBJECT_NAME_INCORRECT) fun `incorrect object name fix`() { fixAndCompare("object_/IncorrectObjectNameExpected.kt", "object_/IncorrectObjectNameTest.kt") } @Test @Tag(WarningNames.ENUM_VALUE) fun `incorrect enum values case fix`() { fixAndCompare("enum_/EnumValueSnakeCaseExpected.kt", "enum_/EnumValueSnakeCaseTest.kt") } @Test @Tag(WarningNames.CONSTANT_UPPERCASE) fun `incorrect constant name case fix`() { fixAndCompare("identifiers/ConstantValNameExpected.kt", "identifiers/ConstantValNameTest.kt") } @Test @Tag(WarningNames.VARIABLE_NAME_INCORRECT_FORMAT) fun `incorrect variable name case fix`() { fixAndCompare("identifiers/VariableNamingExpected.kt", "identifiers/VariableNamingTest.kt") } @Test @Tag(WarningNames.VARIABLE_HAS_PREFIX) fun `incorrect variable prefix fix`() { fixAndCompare("identifiers/PrefixInNameExpected.kt", "identifiers/PrefixInNameTest.kt") } @Test @Tag(WarningNames.VARIABLE_NAME_INCORRECT_FORMAT) fun `incorrect lambda argument case fix`() { fixAndCompare("identifiers/LambdaArgExpected.kt", "identifiers/LambdaArgTest.kt") } @Test @Tag(WarningNames.TYPEALIAS_NAME_INCORRECT_CASE) fun `typeAlias name incorrect`() { fixAndCompare("identifiers/TypeAliasNameExpected.kt", "identifiers/TypeAliasNameTest.kt") } @Test @Tag(WarningNames.TYPEALIAS_NAME_INCORRECT_CASE) fun `should update property name in KDoc after fixing`() { fixAndCompare("identifiers/PropertyInKdocExpected.kt", "identifiers/PropertyInKdocTest.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter1/IdentifierNamingWarnTest.kt ================================================ @file:Suppress( "LargeClass" ) package com.saveourtool.diktat.ruleset.chapter1 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.BACKTICKS_PROHIBITED import com.saveourtool.diktat.ruleset.constants.Warnings.CLASS_NAME_INCORRECT import com.saveourtool.diktat.ruleset.constants.Warnings.CONFUSING_IDENTIFIER_NAMING import com.saveourtool.diktat.ruleset.constants.Warnings.CONSTANT_UPPERCASE import com.saveourtool.diktat.ruleset.constants.Warnings.ENUM_VALUE import com.saveourtool.diktat.ruleset.constants.Warnings.EXCEPTION_SUFFIX import com.saveourtool.diktat.ruleset.constants.Warnings.FUNCTION_BOOLEAN_PREFIX import com.saveourtool.diktat.ruleset.constants.Warnings.GENERIC_NAME import com.saveourtool.diktat.ruleset.constants.Warnings.IDENTIFIER_LENGTH import com.saveourtool.diktat.ruleset.constants.Warnings.OBJECT_NAME_INCORRECT import com.saveourtool.diktat.ruleset.constants.Warnings.VARIABLE_HAS_PREFIX import com.saveourtool.diktat.ruleset.constants.Warnings.VARIABLE_NAME_INCORRECT import com.saveourtool.diktat.ruleset.constants.Warnings.VARIABLE_NAME_INCORRECT_FORMAT import com.saveourtool.diktat.ruleset.rules.chapter1.IdentifierNaming import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.utils.getFirstChildWithType import com.saveourtool.diktat.ruleset.utils.hasAnyChildOfTypes import com.saveourtool.diktat.ruleset.utils.prettyPrint import generated.WarningNames import org.jetbrains.kotlin.KtNodeTypes import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.lexer.KtTokens import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tags import org.junit.jupiter.api.Test class IdentifierNamingWarnTest : LintTestBase(::IdentifierNaming) { private val ruleId: String = "$DIKTAT_RULE_SET_ID:${IdentifierNaming.NAME_ID}" private val rulesConfigBooleanFunctions: List = listOf( RulesConfig(FUNCTION_BOOLEAN_PREFIX.name, true, mapOf("allowedPrefixes" to "equals, equivalent, foo")) ) private val rulesConfigConstantUpperCase = listOf( RulesConfig(CONSTANT_UPPERCASE.name, true, mapOf( "exceptionConstNames" to "serialVersionUID" )) ) // ======== checks for generics ======== @Test @Tag(WarningNames.GENERIC_NAME) fun `generic class - single capital letter, can be followed by a number (check - positive1)`() { val code = """ package com.saveourtool.diktat.test class TreeNode(val value: T?, val next: TreeNode? = null) """.trimIndent() lintMethod(code) } @Test @Tag(WarningNames.GENERIC_NAME) fun `generic class - single capital letter, can be followed by a number (check - positive2)`() { val code = """ package com.saveourtool.diktat.test class TreeNode(val value: T?, val next: TreeNode? = null) """.trimIndent() lintMethod(code) } @Test @Tag(WarningNames.GENERIC_NAME) fun `anonymous function`() { val code = """ package com.saveourtool.diktat.test fun foo() { val sum: (Int) -> Int = fun(x): Int = x + x } """.trimIndent() lintMethod(code) } @Test @Tag(WarningNames.GENERIC_NAME) fun `generic class - single capital letter, can be followed by a number (check - negative1)`() { val code = """ package com.saveourtool.diktat.test class TreeNode(val value: T?, val next: TreeNode? = null) """.trimIndent() lintMethod(code, DiktatError( 3, 15, ruleId, "${GENERIC_NAME.warnText()} ", false) ) } @Test @Tag(WarningNames.GENERIC_NAME) fun `generic class - single capital letter, can be followed by a number (check - negative2)`() { val code = """ package com.saveourtool.diktat.test class TreeNode(val value: T?, val next: TreeNode? = null) """.trimIndent() lintMethod(code, DiktatError( 3, 15, ruleId, "${GENERIC_NAME.warnText()} ", false) ) } @Test @Tag(WarningNames.GENERIC_NAME) fun `generic method - single capital letter, can be followed by a number (check)`() { val code = """ package com.saveourtool.diktat.test fun makeLinkedList(vararg elements: T): TreeNode? { var node: TreeNode? = null for (element in elements.reversed()) { node = TreeNode(element, node) } return node } """.trimIndent() lintMethod(code) } // ======== checks for variables and class names ======== @Test @Tag(WarningNames.CLASS_NAME_INCORRECT) fun `check class name (check)`() { val code = """ class incorrectNAME {} class IncorrectNAME {} """ lintMethod(code, DiktatError(2, 23, ruleId, "${CLASS_NAME_INCORRECT.warnText()} incorrectNAME", true), DiktatError(3, 23, ruleId, "${CLASS_NAME_INCORRECT.warnText()} IncorrectNAME", true) ) } @Test @Tag(WarningNames.CONSTANT_UPPERCASE) fun `number in middle name`() { lintMethod( """ const val NE04J_STARTUP_DELAY_MILLIS = 200L """.trimIndent() ) } @Test @Tags( Tag(WarningNames.CLASS_NAME_INCORRECT), Tag(WarningNames.VARIABLE_NAME_INCORRECT_FORMAT), Tag(WarningNames.CONSTANT_UPPERCASE) ) fun `check identifiers case format (check - negative)`() { val code = """ var SOMEtest = "TEST" const val thisConstantShouldBeUpper = "CONST" class className { data class badClassName(val FIRST: String, var SECOND: String) companion object { const val incorrect_case = "" val correctCase = "" var INCORRECT = "" } var check_me = "" val CHECK_ME = "" } """.trimIndent() lintMethod(code, DiktatError(1, 5, ruleId, "${VARIABLE_NAME_INCORRECT_FORMAT.warnText()} SOMEtest", false), DiktatError(2, 11, ruleId, "${CONSTANT_UPPERCASE.warnText()} thisConstantShouldBeUpper", false), DiktatError(3, 7, ruleId, "${CLASS_NAME_INCORRECT.warnText()} className", true), DiktatError(4, 16, ruleId, "${CLASS_NAME_INCORRECT.warnText()} badClassName", true), DiktatError(4, 33, ruleId, "${VARIABLE_NAME_INCORRECT_FORMAT.warnText()} FIRST", false), DiktatError(4, 52, ruleId, "${VARIABLE_NAME_INCORRECT_FORMAT.warnText()} SECOND", false), DiktatError(7, 19, ruleId, "${CONSTANT_UPPERCASE.warnText()} incorrect_case", false), DiktatError(9, 13, ruleId, "${VARIABLE_NAME_INCORRECT_FORMAT.warnText()} INCORRECT", false), DiktatError(12, 9, ruleId, "${VARIABLE_NAME_INCORRECT_FORMAT.warnText()} check_me", false), DiktatError(13, 9, ruleId, "${VARIABLE_NAME_INCORRECT_FORMAT.warnText()} CHECK_ME", false) ) } @Test @Tag(WarningNames.CONSTANT_UPPERCASE) fun `serialVersionUID should be ignored`() { lintMethod( """ class TestSerializableClass() : Serializable { companion object { private const val serialVersionUID: Long = -1 } } """.trimIndent(), rulesConfigList = rulesConfigConstantUpperCase ) } @Test @Tag(WarningNames.CONSTANT_UPPERCASE) fun `should trigger when the name is not exception`() { val code = """ class TestSerializableClass() : Serializable { companion object { private const val serialVersion: Long = -1 } } """.trimIndent() lintMethod(code, DiktatError(3, 27, ruleId, "${CONSTANT_UPPERCASE.warnText()} serialVersion", true) ) } @Test @Tags(Tag(WarningNames.IDENTIFIER_LENGTH), Tag(WarningNames.VARIABLE_NAME_INCORRECT)) fun `check variable length (check - negative)`() { val code = """ val r = 0 val x256 = 256 val i = 1 class LongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongName { val veryLongveryLongveryLongveryLongveryLongveryLongveryLongveryLongveryLongName = " " } """.trimIndent() lintMethod(code, DiktatError(1, 5, ruleId, "${IDENTIFIER_LENGTH.warnText()} r"), DiktatError(2, 5, ruleId, "${VARIABLE_NAME_INCORRECT.warnText()} x256"), DiktatError(4, 7, ruleId, "${IDENTIFIER_LENGTH.warnText()} LongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongLongName"), DiktatError(5, 9, ruleId, "${IDENTIFIER_LENGTH.warnText()} veryLongveryLongveryLongveryLongveryLongveryLongveryLongveryLongveryLongName") ) } @Test @Tag(WarningNames.VARIABLE_NAME_INCORRECT_FORMAT) fun `check value parameters in dataclasses (check - negative)`() { val code = """ data class ClassName(val FIRST: String, var SECOND: String) """.trimIndent() lintMethod(code, DiktatError(1, 26, ruleId, "${VARIABLE_NAME_INCORRECT_FORMAT.warnText()} FIRST", false), DiktatError(1, 45, ruleId, "${VARIABLE_NAME_INCORRECT_FORMAT.warnText()} SECOND", false) ) } @Test @Tag(WarningNames.VARIABLE_NAME_INCORRECT_FORMAT) fun `check value parameters in functions (check - negative)`() { val code = """ fun foo(SOMENAME: String) { } """.trimIndent() lintMethod(code, DiktatError(1, 9, ruleId, "${VARIABLE_NAME_INCORRECT_FORMAT.warnText()} SOMENAME", true) ) } @Test @Tags(Tag(WarningNames.ENUM_VALUE), Tag(WarningNames.CLASS_NAME_INCORRECT)) fun `check case for enum values (check - negative)`() { val code = """ enum class TEST_ONE { first_value, secondValue, thirdVALUE, FourthValue } """.trimIndent() lintMethod(code, DiktatError(1, 12, ruleId, "${CLASS_NAME_INCORRECT.warnText()} TEST_ONE", true), DiktatError(2, 5, ruleId, "${ENUM_VALUE.warnText()} first_value (should be in UPPER_SNAKE_CASE)", true), DiktatError(2, 18, ruleId, "${ENUM_VALUE.warnText()} secondValue (should be in UPPER_SNAKE_CASE)", true), DiktatError(2, 31, ruleId, "${ENUM_VALUE.warnText()} thirdVALUE (should be in UPPER_SNAKE_CASE)", true), DiktatError(2, 43, ruleId, "${ENUM_VALUE.warnText()} FourthValue (should be in UPPER_SNAKE_CASE)", true) ) } @Test @Tags(Tag(WarningNames.ENUM_VALUE), Tag(WarningNames.CLASS_NAME_INCORRECT)) fun `check case for pascal case enum values (check - negative)`() { val rulesConfigPascalCaseEnum: List = listOf( RulesConfig(ENUM_VALUE.name, true, mapOf("enumStyle" to "pascalCase")) ) val code = """ enum class TEST_ONE { first_value, secondValue, thirdVALUE, FOURTH_VALUE } """.trimIndent() lintMethod(code, DiktatError(1, 12, ruleId, "${CLASS_NAME_INCORRECT.warnText()} TEST_ONE", true), DiktatError(2, 5, ruleId, "${ENUM_VALUE.warnText()} first_value (should be in PascalCase)", true), DiktatError(2, 18, ruleId, "${ENUM_VALUE.warnText()} secondValue (should be in PascalCase)", true), DiktatError(2, 31, ruleId, "${ENUM_VALUE.warnText()} thirdVALUE (should be in PascalCase)", true), DiktatError(2, 43, ruleId, "${ENUM_VALUE.warnText()} FOURTH_VALUE (should be in PascalCase)", true), rulesConfigList = rulesConfigPascalCaseEnum ) } @Test @Tag(WarningNames.OBJECT_NAME_INCORRECT) fun `check case for object (check - negative)`() { val code = """ object TEST_ONE { } """.trimIndent() lintMethod(code, DiktatError(1, 8, ruleId, "${OBJECT_NAME_INCORRECT.warnText()} TEST_ONE", true) ) } // ======== exception case and suffix ======== @Test @Tag(WarningNames.CLASS_NAME_INCORRECT) fun `check exception case format`() { val code = """ class incorrect_case_Exception(message: String) : Exception(message) """.trimIndent() lintMethod(code, DiktatError(1, 7, ruleId, "${CLASS_NAME_INCORRECT.warnText()} incorrect_case_Exception", true) ) } @Test @Tag(WarningNames.EXCEPTION_SUFFIX) fun `check exception case and suffix (with type call entry) - negative`() { val code = """ class Custom(message: String) : Exception(message) """.trimIndent() lintMethod(code, DiktatError(1, 7, ruleId, "${EXCEPTION_SUFFIX.warnText()} Custom", true) ) } @Test @Tag(WarningNames.EXCEPTION_SUFFIX) fun `check exception case and suffix (only parent name inheritance) - negative`() { val code = """ class Custom: Exception { constructor(msg: String) : super(msg) } """.trimIndent() lintMethod(code, DiktatError(1, 7, ruleId, "${EXCEPTION_SUFFIX.warnText()} Custom", true) ) } @Test @Tag(WarningNames.VARIABLE_HAS_PREFIX) fun `checking that there should be no prefixes in variable name`() { val code = """ const val M_GLOB = "" val aPrefix = "" """.trimIndent() lintMethod(code, DiktatError(1, 11, ruleId, "${VARIABLE_HAS_PREFIX.warnText()} M_GLOB", true), DiktatError(2, 5, ruleId, "${VARIABLE_HAS_PREFIX.warnText()} aPrefix", true) ) } @Test @Tags(Tag(WarningNames.VARIABLE_NAME_INCORRECT_FORMAT), Tag(WarningNames.VARIABLE_HAS_PREFIX)) fun `regression - checking that digit in the end will not raise a warning`() { val code = """ val I_AM_CONSTANT1 = "" const val I_AM_CONSTANT2 = "" """.trimIndent() lintMethod(code, DiktatError(1, 5, ruleId, "${VARIABLE_NAME_INCORRECT_FORMAT.warnText()} I_AM_CONSTANT1", false), DiktatError(1, 5, ruleId, "${VARIABLE_HAS_PREFIX.warnText()} I_AM_CONSTANT1", true), DiktatError(2, 11, ruleId, "${VARIABLE_HAS_PREFIX.warnText()} I_AM_CONSTANT2", true) ) } @Test @Tag(WarningNames.VARIABLE_NAME_INCORRECT_FORMAT) fun `regression - destructing declaration in lambdas - incorrect case `() { val code = """ private fun checkCommentedCode(node: ASTNode) { val eolCommentsOffsetToText = "" val blockCommentsOffsetToText = "" (eolCommentsOffsetToText + blockCommentsOffsetToText) .map { (STRANGECASE, text) -> "" } } """.trimIndent() lintMethod(code, DiktatError(5, 13, ruleId, "${VARIABLE_NAME_INCORRECT_FORMAT.warnText()} STRANGECASE", true) ) } @Test @Tag(WarningNames.VARIABLE_NAME_INCORRECT_FORMAT) fun `regression - lambda argument - incorrect case`() { val code = """ private fun checkCommentedCode(node: ASTNode) { val eolCommentsOffsetToText = "" eolCommentsOffsetToText.map { STRANGECASE -> "" } } """.trimIndent() lintMethod(code, DiktatError(3, 35, ruleId, "${VARIABLE_NAME_INCORRECT_FORMAT.warnText()} STRANGECASE", true) ) } @Test @Tag(WarningNames.FUNCTION_BOOLEAN_PREFIX) fun `FUNCTION_BOOLEAN_PREFIX - positive example`() { lintMethod( """ fun ASTNode.hasEmptyLineAfter(): Boolean { } fun hasEmptyLineAfter(): Boolean { } fun ASTNode.isEmpty(): Boolean { } fun isEmpty(): Boolean { } override fun empty(): Boolean { } override fun ASTNode.empty(): Boolean { } """.trimIndent() ) } @Test @Tag(WarningNames.FUNCTION_BOOLEAN_PREFIX) fun `FUNCTION_BOOLEAN_PREFIX - negative example`() { lintMethod( """ fun ASTNode.emptyLineAfter(): Boolean { } fun emptyLineAfter(): Boolean { } fun ASTNode.empty(): Boolean { } fun empty(): Boolean { } """.trimIndent(), DiktatError(1, 13, ruleId, "${FUNCTION_BOOLEAN_PREFIX.warnText()} emptyLineAfter", false), DiktatError(2, 5, ruleId, "${FUNCTION_BOOLEAN_PREFIX.warnText()} emptyLineAfter", false), DiktatError(3, 13, ruleId, "${FUNCTION_BOOLEAN_PREFIX.warnText()} empty", false), DiktatError(4, 5, ruleId, "${FUNCTION_BOOLEAN_PREFIX.warnText()} empty", false) ) } @Test @Tag(WarningNames.FUNCTION_BOOLEAN_PREFIX) fun `all prefixes for boolean methods`() { lintMethod( """ fun hasEmptyLineAfter(): Boolean { } fun haveEmptyLineAfter(): Boolean { } fun isEmpty(): Boolean { } fun shouldBeEmpty(): Boolean { } fun areEmpty(): Boolean { } """.trimIndent() ) } @Test @Tag(WarningNames.FUNCTION_BOOLEAN_PREFIX) fun `test allowed boolean functions in configuration`() { lintMethod( """ fun equalsSome(): Boolean { } fun fooBar(): Boolean { } fun equivalentToAnother(): Boolean { } """.trimIndent(), rulesConfigList = rulesConfigBooleanFunctions ) } @Test @Tag(WarningNames.FUNCTION_BOOLEAN_PREFIX) fun `fixed false positive result on operator functions`() { lintMethod( """ inline operator fun component3(): Boolean = asDynamic()[2].unsafeCast() """.trimIndent() ) } @Test @Tag(WarningNames.IDENTIFIER_LENGTH) fun `regression - function argument type`() { // valid example, should not cause exceptions lintMethod( """ fun foo(predicate: (Int) -> Boolean) = Unit """.trimIndent() ) // identifier names in function types are still checked if present lintMethod( """ fun foo(predicate: (a: Int) -> Boolean) = Unit """.trimIndent(), DiktatError(1, 21, ruleId, "${IDENTIFIER_LENGTH.warnText()} a", false) ) } @Test @Tag(WarningNames.IDENTIFIER_LENGTH) fun `regression - object parsing should not fail with anonymous objects`() { val code = """ val fakeVal = RuleSet("test", object : Rule("astnode-utils-test") { override fun visit(node: ASTNode) {} }) """.trimIndent() lintMethod(code) } @Test @Tag(WarningNames.IDENTIFIER_LENGTH) fun `exception case for identifier naming in catch statements`() { val code = """ fun foo() { try { } catch (e: IOException) { } } """.trimIndent() lintMethod(code) } @Test @Tag(WarningNames.IDENTIFIER_LENGTH) fun `exception case for identifier naming in catch statements with catch body`() { val code = """ fun foo() { try { } catch (e: IOException) { fun foo(e: Int) { } } } """.trimIndent() lintMethod(code, DiktatError(4, 17, ruleId, "${IDENTIFIER_LENGTH.warnText()} e", false)) } @Test @Tag(WarningNames.IDENTIFIER_LENGTH) fun `exception case for identifier naming - catching exception with type e`() { val code = """ fun foo() { try { } catch (e: e) { } } """.trimIndent() lintMethod(code) } @Test @Tag(WarningNames.BACKTICKS_PROHIBITED) fun `backticks should be used only with functions from tests (function)`() { val code = """ fun `foo function`(`argument with backstick`: String) { val `foo variable` = "" } """.trimIndent() lintMethod(code, DiktatError(1, 5, ruleId, "${BACKTICKS_PROHIBITED.warnText()} `foo function`"), DiktatError(1, 20, ruleId, "${BACKTICKS_PROHIBITED.warnText()} `argument with backstick`"), DiktatError(2, 9, ruleId, "${BACKTICKS_PROHIBITED.warnText()} `foo variable`") ) } @Test @Tag(WarningNames.BACKTICKS_PROHIBITED) fun `backticks should be used only with functions from tests (test method)`() { val code = """ @Test fun `test function with backstick`() { } """.trimIndent() lintMethod(code) } @Test @Tag(WarningNames.BACKTICKS_PROHIBITED) fun `backticks should be used only with functions from tests (test method with variables)`() { val code = """ @Test fun `test function with backstick`() { val `should not be used` = "" } """.trimIndent() lintMethod(code, DiktatError(3, 9, ruleId, "${BACKTICKS_PROHIBITED.warnText()} `should not be used`")) } @Test @Tag(WarningNames.BACKTICKS_PROHIBITED) fun `backticks should be used only with functions from tests (class)`() { val code = """ class `my class name` {} """.trimIndent() lintMethod(code, DiktatError(1, 7, ruleId, "${BACKTICKS_PROHIBITED.warnText()} `my class name`")) } @Test @Tag(WarningNames.BACKTICKS_PROHIBITED) fun `regression - backticks should be forbidden only in declarations`() { lintMethod( """ |fun foo() { | it.assertThat(actual.detail).`as`("Detailed message").isEqualTo(expected.detail) |} """.trimMargin() ) } @Test @Tag(WarningNames.VARIABLE_NAME_INCORRECT_FORMAT) fun `should not trigger on underscore`() { val code = """ class SomeClass { fun bar() { val ast = list.map {(first, _) -> foo(first)} var meanValue: Int = 0 for (( _, _, year ) in cars) { meanValue += year } try { /* ... */ } catch (_: IOException) { /* ... */ } } } """.trimIndent() lintMethod(code) } @Test @Tag(WarningNames.VARIABLE_NAME_INCORRECT_FORMAT) fun `should not trigger on backing field`() { lintMethod( """ |package com.example | |class MutableTableContainer { | private var _table: Map? = null | | val table: Map | get() { | if (_table == null) { | _table = hashMapOf() | } | return _table ?: throw AssertionError("Set to null by another thread") | } | set(value) { | field = value | } | |} """.trimMargin(), ) } @Test @Tag(WarningNames.VARIABLE_NAME_INCORRECT_FORMAT) fun `should trigger on backing field with setter`() { val code = """ |package com.example | |class MutableTableContainer { | private var _table: Map? = null | set(value) { | field = value | } | | val table: Map | get() { | if (_table == null) { | _table = hashMapOf() | } | return _table ?: throw AssertionError("Set to null by another thread") | } | set(value) { | field = value | } | |} """.trimMargin() lintMethod(code, DiktatError(4, 16, ruleId, "${VARIABLE_NAME_INCORRECT_FORMAT.warnText()} _table", true), ) } @Test @Tag(WarningNames.VARIABLE_NAME_INCORRECT_FORMAT) fun `should trigger on backing field with no matching property`() { val code = """ |package com.example | |class MutableTableContainer { | private var __table: Map? = null | | val table: Map | get() { | if (_table == null) { | _table = hashMapOf() | } | return _table ?: throw AssertionError("Set to null by another thread") | } | set(value) { | field = value | } | |} """.trimMargin() lintMethod(code, DiktatError(4, 16, ruleId, "${VARIABLE_NAME_INCORRECT_FORMAT.warnText()} __table", true), ) } @Test @Tag(WarningNames.VARIABLE_NAME_INCORRECT_FORMAT) fun `should trigger on backing field with unmatched type`() { val code = """ |package com.example | |class MutableTableContainer { | private var _table: Map? = null | | val table: Map | get() { | if (_table == null) { | _table = hashMapOf() | } | return _table ?: throw AssertionError("Set to null by another thread") | } | set(value) { | field = value | } | |} """.trimMargin() lintMethod(code, DiktatError(4, 16, ruleId, "${VARIABLE_NAME_INCORRECT_FORMAT.warnText()} _table", true), ) } @Test @Tag(WarningNames.CONFUSING_IDENTIFIER_NAMING) fun `confusing identifier naming`() { val code = """ fun someFunc() { val D = 0 val Z = 2 } enum class Ident { B } """.trimIndent() lintMethod(code, DiktatError(2, 9, ruleId, "${CONFUSING_IDENTIFIER_NAMING.warnText()} better name is: obj, dgt", false), DiktatError(2, 9, ruleId, "${VARIABLE_NAME_INCORRECT_FORMAT.warnText()} D", true), DiktatError(2, 9, ruleId, "${IDENTIFIER_LENGTH.warnText()} D", false), DiktatError(3, 9, ruleId, "${CONFUSING_IDENTIFIER_NAMING.warnText()} better name is: n1, n2", false), DiktatError(3, 9, ruleId, "${VARIABLE_NAME_INCORRECT_FORMAT.warnText()} Z", true), DiktatError(3, 9, ruleId, "${IDENTIFIER_LENGTH.warnText()} Z", false), DiktatError(7, 5, ruleId, "${CONFUSING_IDENTIFIER_NAMING.warnText()} better name is: bt, nxt", false), DiktatError(7, 5, ruleId, "${IDENTIFIER_LENGTH.warnText()} B", false)) } @Test @Tag(WarningNames.GENERIC_NAME) fun `check generic types`() { val code = """ interface Test interface Test1 interface Test2> interface Test3 interface Test3 interface Test4 interface Test5 interface Test6 interface Test6 """.trimIndent() lintMethod(code, DiktatError(1, 15, ruleId, "${GENERIC_NAME.warnText()} ", false), DiktatError(7, 16, ruleId, "${GENERIC_NAME.warnText()} ", false), DiktatError(8, 16, ruleId, "${GENERIC_NAME.warnText()} ", false), DiktatError(9, 16, ruleId, "${GENERIC_NAME.warnText()} ", false), ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter1/MethodNamingWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter1 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings.FUNCTION_BOOLEAN_PREFIX import com.saveourtool.diktat.ruleset.constants.Warnings.FUNCTION_NAME_INCORRECT_CASE import com.saveourtool.diktat.ruleset.constants.Warnings.TYPEALIAS_NAME_INCORRECT_CASE import com.saveourtool.diktat.ruleset.rules.chapter1.IdentifierNaming import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class MethodNamingWarnTest : LintTestBase(::IdentifierNaming) { private val ruleId: String = "$DIKTAT_RULE_SET_ID:${IdentifierNaming.NAME_ID}" @Test @Tag(WarningNames.FUNCTION_NAME_INCORRECT_CASE) fun `method name incorrect, part 1`() { val code = """ class SomeClass { fun /* */ methODTREE(): String { } } """.trimIndent() lintMethod(code, DiktatError(2, 15, ruleId, "${FUNCTION_NAME_INCORRECT_CASE.warnText()} methODTREE", true)) } @Test @Tag(WarningNames.FUNCTION_NAME_INCORRECT_CASE) fun `method name incorrect, part 2`() { val code = """ class TestPackageName { fun method_two(): String { return "" } } """.trimIndent() lintMethod(code, DiktatError(2, 9, ruleId, "${FUNCTION_NAME_INCORRECT_CASE.warnText()} method_two", true)) } @Test @Tag(WarningNames.FUNCTION_NAME_INCORRECT_CASE) fun `method name incorrect, part 3`() { val code = """ fun String.methODTREE(): String { fun TEST(): Unit { return "" } } """.trimIndent() lintMethod(code, DiktatError(1, 12, ruleId, "${FUNCTION_NAME_INCORRECT_CASE.warnText()} methODTREE", true), DiktatError(2, 9, ruleId, "${FUNCTION_NAME_INCORRECT_CASE.warnText()} TEST", true) ) } @Test @Tag(WarningNames.FUNCTION_NAME_INCORRECT_CASE) fun `method name incorrect, part 4`() { val code = """ class TestPackageName { fun methODTREE(): String { } } """.trimIndent() lintMethod(code, DiktatError(2, 9, ruleId, "${FUNCTION_NAME_INCORRECT_CASE.warnText()} methODTREE", true)) } @Test @Tag(WarningNames.FUNCTION_NAME_INCORRECT_CASE) fun `method name incorrect, part 5`() { val code = """ class TestPackageName { fun methODTREE() { } } """.trimIndent() lintMethod(code, DiktatError(2, 9, ruleId, "${FUNCTION_NAME_INCORRECT_CASE.warnText()} methODTREE", true)) } @Test @Tag(WarningNames.TYPEALIAS_NAME_INCORRECT_CASE) fun `typeAlias name incorrect, part 1`() { val code = """ class TestPackageName { typealias relatedClasses = List> } """.trimIndent() lintMethod(code, DiktatError(2, 15, ruleId, "${TYPEALIAS_NAME_INCORRECT_CASE.warnText()} relatedClasses", true)) } @Test @Tag(WarningNames.TYPEALIAS_NAME_INCORRECT_CASE) fun `typeAlias name incorrect, part 2`() { lintMethod( """ class TestPackageName { typealias RelatedClasses = List> } """.trimIndent() ) } @Test @Tag(WarningNames.FUNCTION_BOOLEAN_PREFIX) fun `boolean method name incorrect`() { val code = """ fun someBooleanCheck(): Boolean { return false } """.trimIndent() lintMethod(code, DiktatError(1, 5, ruleId, "${FUNCTION_BOOLEAN_PREFIX.warnText()} someBooleanCheck", false)) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter1/PackageNamingFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter1 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.rules.chapter1.PackageNaming import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class PackageNamingFixTest : FixTestBase( "test/paragraph1/naming/package", ::PackageNaming, listOf(RulesConfig("DIKTAT_COMMON", true, mapOf("domainName" to "com.saveourtool.diktat"))) ) { @Test @Tag(WarningNames.PACKAGE_NAME_INCORRECT_CASE) fun `incorrect case of package name (fix)`() { fixAndCompare("FixUpperExpected.kt", "FixUpperTest.kt") } @Test @Tag(WarningNames.PACKAGE_NAME_INCORRECT_PATH) fun `fixing incorrect domain name (fix)`() { fixAndCompare("MissingDomainNameExpected.kt", "MissingDomainNameTest.kt") } @Test @Tag(WarningNames.INCORRECT_PACKAGE_SEPARATOR) fun `incorrect usage of package separator (fix)`() { fixAndCompare("FixUnderscoreExpected.kt", "FixUnderscoreTest.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter1/PackageNamingWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter1 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.INCORRECT_PACKAGE_SEPARATOR import com.saveourtool.diktat.ruleset.constants.Warnings.PACKAGE_NAME_INCORRECT_CASE import com.saveourtool.diktat.ruleset.constants.Warnings.PACKAGE_NAME_INCORRECT_PATH import com.saveourtool.diktat.ruleset.constants.Warnings.PACKAGE_NAME_INCORRECT_PREFIX import com.saveourtool.diktat.ruleset.constants.Warnings.PACKAGE_NAME_INCORRECT_SYMBOLS import com.saveourtool.diktat.ruleset.constants.Warnings.PACKAGE_NAME_MISSING import com.saveourtool.diktat.ruleset.rules.chapter1.PackageNaming import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.util.TEST_FILE_NAME import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir import java.nio.file.Path class PackageNamingWarnTest : LintTestBase(::PackageNaming) { private val ruleId: String = "$DIKTAT_RULE_SET_ID:${PackageNaming.NAME_ID}" private val rulesConfigList: List = listOf( RulesConfig("DIKTAT_COMMON", true, mapOf("domainName" to "com.saveourtool.diktat")) ) private val rulesConfigListEmptyDomainName: List = listOf( RulesConfig("DIKTAT_COMMON", true, mapOf("domainName" to "")) ) private val rulesConfigSourceDirectories: List = listOf( RulesConfig("DIKTAT_COMMON", true, mapOf( "domainName" to "com.saveourtool.diktat", "srcDirectories" to "nativeMain, mobileMain", "testDirs" to "nativeTest" )) ) @Test @Tag(WarningNames.PACKAGE_NAME_MISSING) fun `missing package name (check)`(@TempDir tempDir: Path) { lintMethodWithFile( """ import com.saveourtool.diktat.a.b.c /** * testComment */ class TestPackageName { } """.trimIndent(), tempDir = tempDir, fileName = TEST_FILE_NAME, DiktatError(1, 1, ruleId, "${PACKAGE_NAME_MISSING.warnText()} $TEST_FILE_NAME", true), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.PACKAGE_NAME_MISSING) fun `missing package name with annotation (check)`(@TempDir tempDir: Path) { lintMethodWithFile( """ @file:Suppress("CONSTANT_UPPERCASE") import com.saveourtool.diktat.a.b.c /** * testComment */ class TestPackageName { } """.trimIndent(), tempDir = tempDir, fileName = TEST_FILE_NAME, DiktatError(1, 37, ruleId, "${PACKAGE_NAME_MISSING.warnText()} $TEST_FILE_NAME", true), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.PACKAGE_NAME_MISSING) fun `don't add the package name to the file in buildSrc path`(@TempDir tempDir: Path) { lintMethodWithFile( """ import com.saveourtool.diktat.a.b.c /** * testComment */ class TestPackageName { } """.trimIndent(), tempDir = tempDir, fileName = "diktat/buildSrc/src/main/kotlin/Version.kt", rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.PACKAGE_NAME_INCORRECT_CASE) fun `package name should be in a lower case (check)`() { lintMethod( """ package /* AAA */ com.saveourtool.diktat.SPECIALTEST.test import com.saveourtool.diktat.a.b.c /** * testComment */ class TestPackageName { } """.trimIndent(), DiktatError(1, 42, ruleId, "${PACKAGE_NAME_INCORRECT_CASE.warnText()} SPECIALTEST", true), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.PACKAGE_NAME_INCORRECT_PREFIX) fun `package name should start from domain name (check)`() { lintMethod( """ package some.incorrect.domain.test import com.saveourtool.diktat.a.b.c /** * testComment */ class TestPackageName { } """.trimIndent(), DiktatError(1, 9, ruleId, "${PACKAGE_NAME_INCORRECT_PREFIX.warnText()} com.saveourtool.diktat", true), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.INCORRECT_PACKAGE_SEPARATOR) fun `underscore exceptions - incorrect underscore case`() { lintMethod( """ package com.saveourtool.diktat.domain.test_ import com.saveourtool.diktat.a.b.c /** * testComment */ class TestPackageName { } """.trimIndent(), DiktatError(1, 39, ruleId, "${INCORRECT_PACKAGE_SEPARATOR.warnText()} test_", true), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.PACKAGE_NAME_INCORRECT_SYMBOLS) fun `incorrect symbol in package name`() { lintMethod( """ package com.saveourtool.diktat.domain.testш import com.saveourtool.diktat.a.b.c /** * testComment */ class TestPackageName { } """.trimIndent(), DiktatError(1, 39, ruleId, "${PACKAGE_NAME_INCORRECT_SYMBOLS.warnText()} testш"), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.PACKAGE_NAME_INCORRECT_SYMBOLS) fun `underscore exceptions - positive case - keyword`() { lintMethod( """ package com.saveourtool.diktat.domain.int_ import com.saveourtool.diktat.a.b.c /** * testComment */ class TestPackageName { } """.trimIndent(), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.PACKAGE_NAME_INCORRECT_SYMBOLS) fun `test source config`(@TempDir tempDir: Path) { lintMethodWithFile( """ package com.saveourtool.diktat.domain import com.saveourtool.diktat.a.b.c /** * testComment */ class TestPackageName { } """.trimIndent(), tempDir = tempDir, fileName = "diktat/diktat-rules/src/nativeMain/kotlin/com/saveourtool/diktat/domain/BlaBla.kt", rulesConfigList = rulesConfigSourceDirectories ) lintMethodWithFile( """ package com.saveourtool.diktat.domain import com.saveourtool.diktat.a.b.c /** * testComment */ class TestPackageName { } """.trimIndent(), tempDir = tempDir, fileName = "diktat/diktat-rules/src/main/kotlin/com/saveourtool/diktat/domain/BlaBla.kt", DiktatError(1, 9, ruleId, "${PACKAGE_NAME_INCORRECT_PATH.warnText()} com.saveourtool.diktat.main.kotlin.com.saveourtool.diktat.domain", true), rulesConfigList = rulesConfigSourceDirectories ) } @Test @Tag(WarningNames.PACKAGE_NAME_INCORRECT_SYMBOLS) fun `test directories for test config`(@TempDir tempDir: Path) { lintMethodWithFile( """ package com.saveourtool.diktat.domain import com.saveourtool.diktat.a.b.c /** * testComment */ class TestPackageName { } """.trimIndent(), tempDir = tempDir, fileName = "diktat/diktat-rules/src/nativeTest/kotlin/com/saveourtool/diktat/domain/BlaBla.kt", rulesConfigList = rulesConfigSourceDirectories ) lintMethodWithFile( """ package com.saveourtool.diktat.domain import com.saveourtool.diktat.a.b.c /** * testComment */ class TestPackageName { } """.trimIndent(), tempDir = tempDir, fileName = "diktat/diktat-rules/src/test/kotlin/com/saveourtool/diktat/domain/BlaBla.kt", DiktatError(1, 9, ruleId, "${PACKAGE_NAME_INCORRECT_PATH.warnText()} com.saveourtool.diktat.test.kotlin.com.saveourtool.diktat.domain", true), rulesConfigList = rulesConfigSourceDirectories ) } @Test @Tag(WarningNames.PACKAGE_NAME_INCORRECT_PATH) fun `regression - incorrect warning on file under test directory`(@TempDir tempDir: Path) { lintMethodWithFile( """ package com.saveourtool.diktat.ruleset.chapter1 """.trimIndent(), tempDir = tempDir, fileName = "diktat/diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter1/EnumValueCaseTest.kt", rulesConfigList = rulesConfigList ) lintMethodWithFile( """ package com.saveourtool.diktat.chapter1 """.trimIndent(), tempDir = tempDir, fileName = "diktat/diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter1/EnumValueCaseTest.kt", DiktatError(1, 9, ruleId, "${PACKAGE_NAME_INCORRECT_PATH.warnText()} com.saveourtool.diktat.ruleset.chapter1", true), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.PACKAGE_NAME_INCORRECT_PATH) fun `regression - should not remove special words from file path`(@TempDir tempDir: Path) { lintMethodWithFile( """ |package com.saveourtool.diktat.test.processing """.trimMargin(), tempDir = tempDir, fileName = "project/module/src/test/kotlin/com/saveourtool/diktat/test/processing/SpecialPackageNaming.kt", rulesConfigList = rulesConfigList ) lintMethodWithFile( """ |package kotlin.collections """.trimMargin(), tempDir = tempDir, fileName = "project/module/src/main/kotlin/kotlin/collections/Collections.kt", rulesConfigList = listOf( RulesConfig("DIKTAT_COMMON", true, mapOf("domainName" to "kotlin")) ) ) } @Test @Tag(WarningNames.PACKAGE_NAME_INCORRECT_PATH) fun `should respect KMP project structure - positive example`(@TempDir tempDir: Path) { listOf("main", "test", "jvmMain", "jvmTest", "androidMain", "androidTest", "iosMain", "iosTest", "jsMain", "jsTest", "commonMain", "commonTest").forEach { lintMethodWithFile( """ |package com.saveourtool.diktat """.trimMargin(), tempDir = tempDir, fileName = "project/src/$it/kotlin/com/saveourtool/diktat/Example.kt", rulesConfigList = rulesConfigList ) } } @Test @Tag(WarningNames.PACKAGE_NAME_INCORRECT_PATH) fun `should respect KMP project structure`(@TempDir tempDir: Path) { listOf("main", "test", "jvmMain", "jvmTest", "androidMain", "androidTest", "iosMain", "iosTest", "jsMain", "jsTest", "commonMain", "commonTest").forEach { lintMethodWithFile( """ |package com.saveourtool.diktat """.trimMargin(), tempDir = tempDir, fileName = "project/src/$it/kotlin/com/saveourtool/diktat/example/Example.kt", DiktatError(1, 9, ruleId, "${PACKAGE_NAME_INCORRECT_PATH.warnText()} com.saveourtool.diktat.example", true), rulesConfigList = rulesConfigList ) } } @Test @Tag(WarningNames.PACKAGE_NAME_INCORRECT_PATH) fun `should respect KMP project structure - illegal source set name`(@TempDir tempDir: Path) { lintMethodWithFile( """ |package com.saveourtool.diktat """.trimMargin(), tempDir = tempDir, fileName = "project/src/myProjectMain/kotlin/com/saveourtool/diktat/example/Example.kt", DiktatError(1, 9, ruleId, "${PACKAGE_NAME_INCORRECT_PATH.warnText()} com.saveourtool.diktat.myProjectMain.kotlin.com.saveourtool.diktat.example", true), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.PACKAGE_NAME_INCORRECT_PATH) fun `should warn if there is empty domain name`(@TempDir tempDir: Path) { lintMethodWithFile( """ |package com.saveourtool.diktat """.trimMargin(), tempDir = tempDir, fileName = "project/src/main/kotlin/com/saveourtool/diktat/example/Example.kt", DiktatError(1, 9, ruleId, "${PACKAGE_NAME_INCORRECT_PREFIX.warnText()} ", true), DiktatError(1, 9, ruleId, "${PACKAGE_NAME_INCORRECT_PATH.warnText()} com.saveourtool.diktat.example", true), rulesConfigList = rulesConfigListEmptyDomainName ) } @Test @Tag(WarningNames.PACKAGE_NAME_INCORRECT_PATH) fun `shouldn't trigger if path contains dot`(@TempDir tempDir: Path) { lintMethodWithFile( """ |package com.saveourtool.diktat.test.utils """.trimMargin(), tempDir = tempDir, fileName = "project/src/main/kotlin/com/saveourtool/diktat/test.utils/Example.kt", ) } @Test @Tag(WarningNames.PACKAGE_NAME_INCORRECT_PATH) fun `shouldn't trigger for gradle script`(@TempDir tempDir: Path) { lintMethodWithFile( """ |import com.saveourtool.diktat.generation.docs.generateAvailableRules """.trimMargin(), tempDir = tempDir, fileName = "project/build.gradle.kts", ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter1/PackagePathFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter1 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.rules.chapter1.PackageNaming import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir import java.nio.file.Path class PackagePathFixTest : FixTestBase( "test/paragraph1/naming/package/src/main/kotlin", ::PackageNaming, listOf(RulesConfig("DIKTAT_COMMON", true, mapOf("domainName" to "com.saveourtool.diktat"))) ) { @Test @Tag(WarningNames.PACKAGE_NAME_INCORRECT_PATH) fun `fixing package name that differs from a path`() { fixAndCompare("com/saveourtool/diktat/some/name/FixIncorrectExpected.kt", "com/saveourtool/diktat/some/name/FixIncorrectTest.kt") } @Test @Tag(WarningNames.PACKAGE_NAME_INCORRECT_PATH) fun `fixing package name that differs from a path - regression one-word package name`() { fixAndCompare("com/saveourtool/diktat/some/name/FixPackageRegressionExpected.kt", "com/saveourtool/diktat/some/name/FixPackageRegressionTest.kt") } @Test @Tag(WarningNames.PACKAGE_NAME_INCORRECT_PATH) fun `fixing package name that differs from a path without domain`() { fixAndCompare("some/FixIncorrectExpected.kt", "some/FixIncorrectTest.kt") } @Test @Tag(WarningNames.PACKAGE_NAME_MISSING) fun `fix missing package name with file annotation`() { fixAndCompare("com/saveourtool/diktat/some/name/FixMissingWithAnnotationExpected.kt", "com/saveourtool/diktat/some/name/FixMissingWithAnnotationTest.kt") } @Test @Tag(WarningNames.PACKAGE_NAME_MISSING) fun `fix missing package name with file annotation and comments`() { fixAndCompare("com/saveourtool/diktat/some/name/FixMissingWithAnnotationExpected2.kt", "com/saveourtool/diktat/some/name/FixMissingWithAnnotationTest2.kt") } @Test @Tag(WarningNames.PACKAGE_NAME_MISSING) fun `fix missing package name with file annotation and comments 2`() { fixAndCompare("com/saveourtool/diktat/some/name/FixMissingWithAnnotationExpected3.kt", "com/saveourtool/diktat/some/name/FixMissingWithAnnotationTest3.kt") } // If there is no import list in code, the node is still present in the AST, but without any whitespaces around // So, this check covered case, when we manually add whitespace before package directive @Test @Tag(WarningNames.PACKAGE_NAME_MISSING) fun `fix missing package name without import list`() { fixAndCompare("com/saveourtool/diktat/some/name/FixMissingWithoutImportExpected.kt", "com/saveourtool/diktat/some/name/FixMissingWithoutImportTest.kt") } @Test @Tag(WarningNames.PACKAGE_NAME_MISSING) fun `fix missing package name with a proper location without domain`() { fixAndCompare("some/FixMissingExpected.kt", "some/FixMissingTest.kt") } @Test @Tag(WarningNames.PACKAGE_NAME_MISSING) fun `fix missing package name with a proper location`() { fixAndCompare("com/saveourtool/diktat/some/name/FixMissingExpected.kt", "com/saveourtool/diktat/some/name/FixMissingTest.kt") } @Test @Tag(WarningNames.PACKAGE_NAME_MISSING) fun `several empty lines after package`(@TempDir tempDir: Path) { fixAndCompareContent( expectedContent = """ package com.saveourtool.diktat /** * @param bar * @return something */ fun foo1(bar: Bar): Baz { // placeholder } """.trimIndent(), actualContent = """ /** * @param bar * @return something */ fun foo1(bar: Bar): Baz { // placeholder } """.trimIndent(), subFolder = "src/main/kotlin/com/saveourtool/diktat", tempDir = tempDir, ).assertSuccessful() } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter2/CommentsFormattingFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter2 import com.saveourtool.diktat.ruleset.chapter2.CommentsFormattingTest.Companion.indentStyleComment import com.saveourtool.diktat.ruleset.rules.chapter2.kdoc.CommentsFormatting import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames.COMMENT_WHITE_SPACE import generated.WarningNames.FIRST_COMMENT_NO_BLANK_LINE import generated.WarningNames.IF_ELSE_COMMENTS import generated.WarningNames.WRONG_NEWLINES_AROUND_KDOC import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tags import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir import java.nio.file.Path class CommentsFormattingFixTest : FixTestBase("test/paragraph2/kdoc/", ::CommentsFormatting) { @Test @Tag(WRONG_NEWLINES_AROUND_KDOC) fun `there should be no blank line between kdoc and it's declaration code`() { fixAndCompare("KdocEmptyLineExpected.kt", "KdocEmptyLineTest.kt") } @Test @Tags( Tag(WRONG_NEWLINES_AROUND_KDOC), Tag(COMMENT_WHITE_SPACE), Tag(IF_ELSE_COMMENTS), Tag(FIRST_COMMENT_NO_BLANK_LINE) ) fun `check lines and spaces in comments`() { fixAndCompare("KdocCodeBlocksFormattingExpected.kt", "KdocCodeBlocksFormattingTest.kt") } @Test @Tags(Tag(WRONG_NEWLINES_AROUND_KDOC), Tag(FIRST_COMMENT_NO_BLANK_LINE)) fun `test example from code style`() { fixAndCompare("KdocCodeBlockFormattingExampleExpected.kt", "KdocCodeBlockFormattingExampleTest.kt") } @Test @Tag(WRONG_NEWLINES_AROUND_KDOC) fun `regression - should not insert newline before the first comment in a file`() { fixAndCompare("NoPackageNoImportExpected.kt", "NoPackageNoImportTest.kt") } /** * `indent(1)` and `style(9)` style comments. */ @Test @Tag(COMMENT_WHITE_SPACE) fun `indent-style header in a block comment should be preserved`(@TempDir tempDir: Path) { val lintResult = fixAndCompareContent(indentStyleComment, tempDir = tempDir) lintResult.assertSuccessful() } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter2/CommentsFormattingTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter2 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.constants.Warnings.IF_ELSE_COMMENTS import com.saveourtool.diktat.ruleset.rules.chapter2.kdoc.CommentsFormatting import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.intellij.lang.annotations.Language import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class CommentsFormattingTest : LintTestBase(::CommentsFormatting) { private val ruleId = "$DIKTAT_RULE_SET_ID:${CommentsFormatting.NAME_ID}" @Test @Tag(WarningNames.COMMENT_WHITE_SPACE) fun `check white space before comment good`() { val code = """ |package com.saveourtool.diktat.ruleset.chapter3 | |class Example { | // First Comment | private val log = LoggerFactory.getLogger(Example.javaClass) |} """.trimMargin() lintMethod(code) } @Test @Tag(WarningNames.COMMENT_WHITE_SPACE) fun `check white space before comment bad 2`() { val code = """ |package com.saveourtool.diktat.ruleset.chapter3 | |class Example { | val s = RulesConfig(WRONG_INDENTATION.name, true, | mapOf( | "newlineAtEnd" to "true", // comment | "extendedIndentOfParameters" to "true", | "alignedParameters" to "true", | "extendedIndentAfterOperators" to "true" | ) | ) |} """.trimMargin() lintMethod(code, DiktatError(6, 51, ruleId, "${Warnings.COMMENT_WHITE_SPACE.warnText()} There should be 2 space(s) before comment text, but there are too many in // comment", true)) } @Test @Tag(WarningNames.COMMENT_WHITE_SPACE) fun `check white space before comment bad 3`() { val code = """ |package com.saveourtool.diktat.ruleset.chapter3 | |@Suppress("RULE") // asdasd |class Example { | |} """.trimMargin() lintMethod(code, DiktatError(3, 22, ruleId, "${Warnings.COMMENT_WHITE_SPACE.warnText()} There should be 2 space(s) before comment text, but there are too many in // asdasd", true)) } @Test @Tag(WarningNames.COMMENT_WHITE_SPACE) fun `check white space before comment good2`() { val code = """ |package com.saveourtool.diktat.ruleset.chapter3 | |/* This is a comment */ |class Example { | /** | * | * Some Comment | */ | private val log = LoggerFactory.getLogger(Example.javaClass) | | fun a() { | // When comment | when(1) { | 1 -> print(1) | } | } | /* | Some Comment | */ |} """.trimMargin() lintMethod(code) } @Test @Tag(WarningNames.COMMENT_WHITE_SPACE) fun `check comment before package good`() { val code = """ |// This is a comment before package |package com.saveourtool.diktat.ruleset.chapter3 | |// This is a comment |class Example { | |} """.trimMargin() lintMethod(code) } @Test @Tag(WarningNames.COMMENT_WHITE_SPACE) fun `check white space before comment bad`() { val code = """ |package com.saveourtool.diktat.ruleset.chapter3 | |class Example { | //First Comment | private val log = LoggerFactory.getLogger(Example.javaClass) | | /** | * Some comment | */ | | /* Comment */ |} """.trimMargin() lintMethod(code, DiktatError(4, 5, ruleId, "${Warnings.COMMENT_WHITE_SPACE.warnText()} There should be 1 space(s) before comment token in //First Comment", true), DiktatError(11, 5, ruleId, "${Warnings.COMMENT_WHITE_SPACE.warnText()} There should be 1 space(s) before comment token in /* Comment */", true)) } @Test @Tag(WarningNames.WRONG_NEWLINES_AROUND_KDOC) fun `check new line above comment good`() { val code = """ |package com.saveourtool.diktat.ruleset.chapter3 | |class Example { | private val log = LoggerFactory.getLogger(Example.javaClass) | | // Another Comment | private val some = 5 | | fun someFunc() { | /* First comment */ | val first = 5 // Some comment | | /** | * kDoc comment | * some text | */ | val second = 6 | | /** | * asdasd | */ | fun testFunc() { | val a = 5 // Some Comment | | // Fun in fun Block | val b = 6 | } | } |} """.trimMargin() lintMethod(code) } @Test @Tag(WarningNames.WRONG_NEWLINES_AROUND_KDOC) fun `check file new line above comment good`() { val code = """ |package com.saveourtool.diktat.ruleset.chapter3 | |// Some comment |class Example { | |} | |// Some comment 2 |class AnotherExample { | |} """.trimMargin() lintMethod(code) } @Test @Tag(WarningNames.WRONG_NEWLINES_AROUND_KDOC) fun `check file new line above comment bad`() { val code = """ |package com.saveourtool.diktat.ruleset.chapter3 |// Some comment |class Example { | |} | |// Some comment 2 |class AnotherExample { | |} """.trimMargin() lintMethod(code, DiktatError(2, 1, ruleId, "${Warnings.WRONG_NEWLINES_AROUND_KDOC.warnText()} // Some comment", true)) } @Test @Tag(WarningNames.WRONG_NEWLINES_AROUND_KDOC) fun `check file new line above comment bad - block and kDOC comments`() { val code = """ |package com.saveourtool.diktat.ruleset.chapter3 |/* Some comment */ |class Example { | |} |/** |* Some comment 2 |*/ | |class AnotherExample { | |} """.trimMargin() lintMethod(code, DiktatError(2, 1, ruleId, "${Warnings.WRONG_NEWLINES_AROUND_KDOC.warnText()} /* Some comment */", true), DiktatError(6, 1, ruleId, "${Warnings.WRONG_NEWLINES_AROUND_KDOC.warnText()} /**...", true), DiktatError(8, 3, ruleId, "${Warnings.WRONG_NEWLINES_AROUND_KDOC.warnText()} redundant blank line after /**...", true)) } @Test @Tag(WarningNames.COMMENT_WHITE_SPACE) fun `check right side comments - good`() { val code = """ |package com.saveourtool.diktat.ruleset.chapter3 | |/* Some comment */ |class Example { | val a = 5 // This is a comment |} """.trimMargin() lintMethod(code) } @Test @Tag(WarningNames.COMMENT_WHITE_SPACE) fun `check right side comments - bad`() { val code = """ |package com.saveourtool.diktat.ruleset.chapter3 | |/* Some comment */ |class Example { | val a = 5// This is a comment |} """.trimMargin() lintMethod(code, DiktatError(5, 13, ruleId, "${Warnings.COMMENT_WHITE_SPACE.warnText()} There should be 2 space(s) before comment text, but are none in // This is a comment", true)) } @Test @Tag(WarningNames.IF_ELSE_COMMENTS) fun `if - else comments good`() { val code = """ |package com.saveourtool.diktat.ruleset.chapter3 | |class Example { | fun someFunc() { | // general if comment | if(a = 5) { | | } | else { | // Good Comment | print(5) | } | } |} """.trimMargin() lintMethod(code) } @Test @Tag(WarningNames.IF_ELSE_COMMENTS) fun `if - else comments good 2`() { val code = """ |package com.saveourtool.diktat.ruleset.chapter3 | |class Example { | fun someFunc() { | // general if comment | if(a = 5) { | | } else | // Good Comment | print(5) | } |} """.trimMargin() lintMethod(code) } @Test @Tag(WarningNames.IF_ELSE_COMMENTS) fun `if - else comments bad`() { val code = """ |package com.saveourtool.diktat.ruleset.chapter3 | |class Example { | fun someFunc() { | // general if comment | if(a = 5) { | | } | // Bad Comment | else { | print(5) | } | } |} """.trimMargin() lintMethod(code, DiktatError(6, 8, ruleId, "${IF_ELSE_COMMENTS.warnText()} // Bad Comment", true)) } @Test @Tag(WarningNames.IF_ELSE_COMMENTS) fun `if - else comments bad 3`() { val code = """ |package com.saveourtool.diktat.ruleset.chapter3 | |class Example { | fun someFunc() { | // general if comment | if(a = 5) { | | } /* Some comment */ else { | print(5) | } | } |} """.trimMargin() lintMethod(code, DiktatError(6, 8, ruleId, "${IF_ELSE_COMMENTS.warnText()} /* Some comment */", true)) } @Test @Tag(WarningNames.IF_ELSE_COMMENTS) fun `if - else comments bad 4`() { val code = """ |package com.saveourtool.diktat.ruleset.chapter3 | |class Example { | fun someFunc() { | // general if comment | if(a = 5) { | | } /* Some comment */ else | print(5) | } |} """.trimMargin() lintMethod(code, DiktatError(6, 8, ruleId, "${IF_ELSE_COMMENTS.warnText()} /* Some comment */", true)) } @Test @Tag(WarningNames.IF_ELSE_COMMENTS) fun `should not trigger on comment`() { val code = """ |package com.saveourtool.diktat.ruleset.chapter3 | |class Example { | fun someFunc() { | // general if comment | if(a = 5) { | /* Some comment */ | } else { | print(5) | } | } |} """.trimMargin() lintMethod(code) } @Test @Tag(WarningNames.FIRST_COMMENT_NO_BLANK_LINE) fun `first comment no space in if - else bad`() { val code = """ |package com.saveourtool.diktat.ruleset.chapter3 | |class Example { | fun someFunc() { | // general if comment | if(a = 5) { | | } else { // Bad Comment | print(5) | } | } |} """.trimMargin() lintMethod(code, DiktatError(8, 18, ruleId, "${Warnings.FIRST_COMMENT_NO_BLANK_LINE.warnText()} // Bad Comment", true)) } @Test @Tag(WarningNames.COMMENT_WHITE_SPACE) fun `check comment in class bad`() { val code = """ |package com.saveourtool.diktat.ruleset.chapter3 | |class Example { | // First Comment | private val log = LoggerFactory.getLogger(Example.javaClass) // secondComment |} """.trimMargin() lintMethod(code) } @Test @Tag(WarningNames.COMMENT_WHITE_SPACE) fun `check comment on file level`() { val code = """ | /* | * heh | */ |package com.saveourtool.diktat.ruleset.chapter3 | |class Example { |} """.trimMargin() lintMethod(code, DiktatError(1, 2, ruleId, "${Warnings.COMMENT_WHITE_SPACE.warnText()} There should be 0 space(s) before comment text, but are 1 in /*...", true)) } @Test @Tag(WarningNames.COMMENT_WHITE_SPACE) fun `check comment on file level without space`() { val code = """ |/* | * heh | */ |package com.saveourtool.diktat.ruleset.chapter3 | |class Example { |} """.trimMargin() lintMethod(code) } /** * `indent(1)` and `style(9)` style comments. */ @Test @Tag(WarningNames.COMMENT_WHITE_SPACE) fun `indent-style header in a block comment should produce no warnings`() = lintMethod(indentStyleComment) internal companion object { @Language("kotlin") internal val indentStyleComment = """ |/*- | * This is an indent-style comment, and it's different from regular | * block comments in C-like languages. | * | * Code formatters should not wrap or reflow its content, so you can | * safely insert code fragments: | * | * ``` | * int i = 42; | * ``` | * | * or ASCII diagrams: | * | * +-----+ | * | Box | | * +-----+ | */ """.trimMargin() } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter2/HeaderCommentRuleFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter2 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.HEADER_MISSING_OR_WRONG_COPYRIGHT import com.saveourtool.diktat.ruleset.constants.Warnings.HEADER_WRONG_FORMAT import com.saveourtool.diktat.ruleset.rules.chapter2.comments.HeaderCommentRule import com.saveourtool.diktat.test.framework.processing.ResourceReader.Companion.withReplacements import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import generated.WarningNames.WRONG_COPYRIGHT_YEAR import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tags import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir import java.nio.file.Path import java.time.LocalDate class HeaderCommentRuleFixTest : FixTestBase( "test/paragraph2/header", ::HeaderCommentRule, listOf( RulesConfig("HEADER_MISSING_OR_WRONG_COPYRIGHT", true, mapOf( "isCopyrightMandatory" to "true", "copyrightText" to "Copyright (c) Huawei Technologies Co., Ltd. 2020-$currentYear. All rights reserved.") ), RulesConfig("HEADER_WRONG_FORMAT", true, emptyMap()) ) ) { @Test @Tag(WarningNames.HEADER_WRONG_FORMAT) fun `new line should be inserted after header KDoc`(@TempDir tempDir: Path) { fixAndCompare("NewlineAfterHeaderKdocExpected.kt", "NewlineAfterHeaderKdocTest.kt", overrideResourceReader = { it.withReplacements(tempDir, currentYearReplacement) }) } @Test @Tag(WarningNames.HEADER_MISSING_OR_WRONG_COPYRIGHT) fun `if no copyright is present and mandatoryCopyright=true, it is added`(@TempDir tempDir: Path) { fixAndCompare("AutoCopyrightExpected.kt", "AutoCopyrightTest.kt", overrideResourceReader = { it.withReplacements(tempDir, currentYearReplacement) }) } @Test @Tag(WarningNames.HEADER_MISSING_OR_WRONG_COPYRIGHT) fun `if no copyright is present, added it and apply pattern for current year`(@TempDir tempDir: Path) { fixAndCompare("AutoCopyrightApplyPatternExpected.kt", "AutoCopyrightApplyPatternTest.kt", listOf( RulesConfig( HEADER_MISSING_OR_WRONG_COPYRIGHT.name, true, mapOf( "isCopyrightMandatory" to "true", "copyrightText" to "Copyright (c) Huawei Technologies Co., Ltd. 2020-;@currYear;. All rights reserved." ) ), RulesConfig(HEADER_WRONG_FORMAT.name, true, emptyMap()) ), overrideResourceReader = { it.withReplacements(tempDir, currentYearReplacement) }, ) } /** * Fixme there shouldn't be an additional blank line after copyright */ @Test @Tag(WarningNames.HEADER_NOT_BEFORE_PACKAGE) fun `header KDoc should be moved before package`(@TempDir tempDir: Path) { fixAndCompare("MisplacedHeaderKdocExpected.kt", "MisplacedHeaderKdocTest.kt", overrideResourceReader = { it.withReplacements(tempDir, currentYearReplacement) }) } @Test @Tags(Tag(WarningNames.HEADER_MISSING_OR_WRONG_COPYRIGHT), Tag(WarningNames.HEADER_WRONG_FORMAT)) fun `header KDoc should be moved before package - no copyright`(@TempDir tempDir: Path) { fixAndCompare("MisplacedHeaderKdocNoCopyrightExpected.kt", "MisplacedHeaderKdocNoCopyrightTest.kt", listOf(RulesConfig(HEADER_MISSING_OR_WRONG_COPYRIGHT.name, false, emptyMap()), RulesConfig(HEADER_WRONG_FORMAT.name, true, emptyMap())), overrideResourceReader = { it.withReplacements(tempDir, currentYearReplacement) }, ) } @Test @Tags(Tag(WarningNames.HEADER_NOT_BEFORE_PACKAGE), Tag(WarningNames.HEADER_MISSING_OR_WRONG_COPYRIGHT)) fun `header KDoc should be moved before package - appended copyright`(@TempDir tempDir: Path) { fixAndCompare( "MisplacedHeaderKdocAppendedCopyrightExpected.kt", "MisplacedHeaderKdocAppendedCopyrightTest.kt", overrideResourceReader = { it.withReplacements(tempDir, currentYearReplacement) }, ) } @Test @Tag(WRONG_COPYRIGHT_YEAR) fun `copyright invalid year should be auto-corrected`(@TempDir tempDir: Path) { fixAndCompare("CopyrightDifferentYearExpected.kt", "CopyrightDifferentYearTest.kt", listOf(RulesConfig(HEADER_MISSING_OR_WRONG_COPYRIGHT.name, true, mapOf( "isCopyrightMandatory" to "true", "copyrightText" to "Copyright (c) My Company., Ltd. 2012-2019. All rights reserved." ))), overrideResourceReader = { it.withReplacements(tempDir, currentYearReplacement) }, ) } @Test @Tag(WRONG_COPYRIGHT_YEAR) fun `copyright invalid year should be auto-corrected 2`(@TempDir tempDir: Path) { fixAndCompare("CopyrightDifferentYearExpected2.kt", "CopyrightDifferentYearTest2.kt", listOf(RulesConfig(HEADER_MISSING_OR_WRONG_COPYRIGHT.name, true, mapOf( "isCopyrightMandatory" to "true", "copyrightText" to "Copyright (c) My Company., Ltd. 2021. All rights reserved." ))), overrideResourceReader = { it.withReplacements(tempDir, currentYearReplacement) }, ) } @Test @Tag(WRONG_COPYRIGHT_YEAR) fun `copyright invalid pattern, but valid in code`(@TempDir tempDir: Path) { fixAndCompare("CopyrightInvalidPatternValidCodeExpected.kt", "CopyrightInvalidPatternValidCodeTest.kt", listOf(RulesConfig(HEADER_MISSING_OR_WRONG_COPYRIGHT.name, true, mapOf( "isCopyrightMandatory" to "true", "copyrightText" to "Copyright (c) My Company., Ltd. 2012-2019. All rights reserved." ))), overrideResourceReader = { it.withReplacements(tempDir, currentYearReplacement) }, ) } @Test @Tag(WRONG_COPYRIGHT_YEAR) fun `copyright invalid pattern, update actual year in it and auto-correct`(@TempDir tempDir: Path) { fixAndCompare("CopyrightAbsentInvalidPatternExpected.kt", "CopyrightAbsentInvalidPatternTest.kt", listOf(RulesConfig(HEADER_MISSING_OR_WRONG_COPYRIGHT.name, true, mapOf( "isCopyrightMandatory" to "true", "copyrightText" to "Copyright (c) My Company., Ltd. 2012-2019. All rights reserved." ))), overrideResourceReader = { it.withReplacements(tempDir, currentYearReplacement) }, ) } @Test @Tag(WRONG_COPYRIGHT_YEAR) fun `should not raise npe`(@TempDir tempDir: Path) { fixAndCompare("CopyrightShouldNotTriggerNPEExpected.kt", "CopyrightShouldNotTriggerNPETest.kt", listOf(RulesConfig(HEADER_MISSING_OR_WRONG_COPYRIGHT.name, true, mapOf( "isCopyrightMandatory" to "true", "copyrightText" to "Copyright (c) My Company., Ltd. 2012-2021. All rights reserved." ))), overrideResourceReader = { it.withReplacements(tempDir, currentYearReplacement) }, ) } @Test @Tag(WarningNames.HEADER_MISSING_OR_WRONG_COPYRIGHT) fun `copyright multiline`(@TempDir tempDir: Path) { fixAndCompare("MultilineCopyrightExample.kt", "MultilineCopyrightTest.kt", listOf(RulesConfig(HEADER_MISSING_OR_WRONG_COPYRIGHT.name, true, mapOf( "isCopyrightMandatory" to "true", "copyrightText" to """ | Copyright 2018-$currentYear John Doe. | | Licensed under the Apache License, Version 2.0 (the "License"); | you may not use this file except in compliance with the License. | You may obtain a copy of the License at | | http://www.apache.org/licenses/LICENSE-2.0 | | Unless required by applicable law or agreed to in writing, software | distributed under the License is distributed on an "AS IS" BASIS, | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | See the License for the specific language governing permissions and | limitations under the License. """.trimMargin() ))), overrideResourceReader = { it.withReplacements(tempDir, currentYearReplacement) }, ) } @Test @Tag(WarningNames.HEADER_MISSING_OR_WRONG_COPYRIGHT) fun `should not trigger if copyright text have different indents`(@TempDir tempDir: Path) { fixAndCompare("MultilineCopyrightNotTriggerExample.kt", "MultilineCopyrightNotTriggerTest.kt", listOf(RulesConfig(HEADER_MISSING_OR_WRONG_COPYRIGHT.name, true, mapOf( "isCopyrightMandatory" to "true", "copyrightText" to """ | Copyright 2018-2020 John Doe. | | Licensed under the Apache License, Version 2.0 (the "License"); | you may not use this file except in compliance with the License. | You may obtain a copy of the License at """.trimMargin() ))), overrideResourceReader = { it.withReplacements(tempDir, currentYearReplacement) }, ) } companion object { private const val PLACEHOLDER = "%%YEAR%%" private val currentYear = LocalDate.now().year.toString() private val currentYearReplacement = mapOf(PLACEHOLDER to currentYear) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter2/HeaderCommentRuleTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter2 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.constants.Warnings.HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE import com.saveourtool.diktat.ruleset.constants.Warnings.HEADER_MISSING_OR_WRONG_COPYRIGHT import com.saveourtool.diktat.ruleset.constants.Warnings.HEADER_NOT_BEFORE_PACKAGE import com.saveourtool.diktat.ruleset.constants.Warnings.HEADER_WRONG_FORMAT import com.saveourtool.diktat.ruleset.rules.chapter2.comments.HeaderCommentRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir import java.nio.file.Path import java.time.LocalDate class HeaderCommentRuleTest : LintTestBase(::HeaderCommentRule) { private val ruleId: String = "$DIKTAT_RULE_SET_ID:${HeaderCommentRule.NAME_ID}" private val curYear = LocalDate.now().year private val rulesConfigList: List = listOf( RulesConfig("HEADER_MISSING_OR_WRONG_COPYRIGHT", true, mapOf("copyrightText" to "Copyright (c) My Company, Ltd. 2012-$curYear. All rights reserved.")) ) private val rulesConfigListWithPattern: List = listOf( RulesConfig("HEADER_MISSING_OR_WRONG_COPYRIGHT", true, mapOf("copyrightText" to "Copyright (c) My Company, Ltd. 2012-;@currYear;. All rights reserved.")) ) private val rulesConfigListInvalidYear: List = listOf( RulesConfig("HEADER_MISSING_OR_WRONG_COPYRIGHT", true, mapOf("copyrightText" to "Copyright (c) My Company, Ltd. 2012-2019. All rights reserved.")) ) private val rulesConfigListInvalidYearBeforeCopyright: List = listOf( RulesConfig("HEADER_MISSING_OR_WRONG_COPYRIGHT", true, mapOf("copyrightText" to "Copyright (c) 2019 My Company, Ltd. All rights reserved.")) ) private val rulesConfigListYear: List = listOf( RulesConfig("HEADER_MISSING_OR_WRONG_COPYRIGHT", true, mapOf("copyrightText" to "Copyright (c) $curYear My Company, Ltd. All rights reserved.")) ) private val rulesConfigListYearWithPattern: List = listOf( RulesConfig("HEADER_MISSING_OR_WRONG_COPYRIGHT", true, mapOf("copyrightText" to "Copyright (c) ;@currYear; My Company, Ltd. All rights reserved.")) ) private val rulesConfigListCn: List = listOf( RulesConfig("HEADER_MISSING_OR_WRONG_COPYRIGHT", true, mapOf("copyrightText" to "版权所有 (c) 华为技术有限公司 2012-$curYear")) ) private val curYearCopyright = "Copyright (c) My Company, Ltd. 2012-$curYear. All rights reserved." private val copyrightBlock = """ /* * $curYearCopyright */ """.trimIndent() @Test @Tag(WarningNames.HEADER_WRONG_FORMAT) fun `file header comment (positive example)`() { lintMethod( """ $copyrightBlock /** * Very useful description, why this file has two classes * foo bar baz */ package com.saveourtool.diktat.example class Example1 { } class Example2 { } """.trimIndent(), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.HEADER_WRONG_FORMAT) fun `file header comment with Chinese version copyright (positive example)`() { lintMethod( """ /* * 版权所有 (c) 华为技术有限公司 2012-$curYear */ /** * Very useful description, why this file has two classes * foo bar baz */ package com.saveourtool.diktat.example class Example1 { } class Example2 { } """.trimIndent(), rulesConfigList = rulesConfigListCn ) } @Test @Tag(WarningNames.HEADER_MISSING_OR_WRONG_COPYRIGHT) fun `copyright should not be placed inside KDoc`() { lintMethod( """ /** * $curYearCopyright */ /** * Very useful description, why this file has two classes * foo bar baz */ package com.saveourtool.diktat.example class Example1 { } class Example2 { } """.trimIndent(), DiktatError(1, 1, ruleId, "${HEADER_MISSING_OR_WRONG_COPYRIGHT.warnText()} copyright is placed inside KDoc, but should be inside a block comment", true), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.HEADER_MISSING_OR_WRONG_COPYRIGHT) fun `copyright should not be placed inside KDoc (Chinese version)`() { lintMethod( """ /** * 版权所有 (c) 华为技术有限公司 2012-2020 */ /** * Very useful description, why this file has two classes * foo bar baz */ package com.saveourtool.diktat.example class Example1 { } class Example2 { } """.trimIndent(), DiktatError(1, 1, ruleId, "${HEADER_MISSING_OR_WRONG_COPYRIGHT.warnText()} copyright is placed inside KDoc, but should be inside a block comment", true), rulesConfigList = rulesConfigListCn ) } @Test @Tag(WarningNames.HEADER_MISSING_OR_WRONG_COPYRIGHT) fun `copyright should not be placed inside single line comment`() { lintMethod( """ // $curYearCopyright /** * Very useful description, why this file has two classes * foo bar baz */ package com.saveourtool.diktat.example class Example1 { } class Example2 { } """.trimIndent(), DiktatError(1, 1, ruleId, "${HEADER_MISSING_OR_WRONG_COPYRIGHT.warnText()} copyright is placed inside KDoc, but should be inside a block comment", true), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.WRONG_COPYRIGHT_YEAR) fun `copyright year good`() { lintMethod( """ /* * $curYearCopyright */ /** * Very useful description, why this file has two classes * foo bar baz */ package com.saveourtool.diktat.example class Example1 { } class Example2 { } """.trimIndent(), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.WRONG_COPYRIGHT_YEAR) fun `copyright year good 2`() { lintMethod( """ /* * Copyright (c) $curYear My Company, Ltd. All rights reserved. */ /** * Very useful description, why this file has two classes * foo bar baz */ package com.saveourtool.diktat.example class Example1 { } class Example2 { } """.trimIndent(), rulesConfigList = rulesConfigListYear ) } @Test @Tag(WarningNames.WRONG_COPYRIGHT_YEAR) fun `copyright year good 3 - apply pattern`() { lintMethod( """ /* * Copyright (c) $curYear My Company, Ltd. All rights reserved. */ /** * Very useful description, why this file has two classes * foo bar baz */ package com.saveourtool.diktat.example class Example1 { } class Example2 { } """.trimIndent(), rulesConfigList = rulesConfigListYearWithPattern ) } @Test @Tag(WarningNames.WRONG_COPYRIGHT_YEAR) fun `copyright year good 4 - apply pattern`() { lintMethod( """ /* * Copyright (c) My Company, Ltd. 2012-$curYear. All rights reserved. */ /** * Very useful description, why this file has two classes * foo bar baz */ package com.saveourtool.diktat.example class Example1 { } class Example2 { } """.trimIndent(), rulesConfigList = rulesConfigListWithPattern ) } @Test @Tag(WarningNames.WRONG_COPYRIGHT_YEAR) fun `copyright year good 5`() { lintMethod( """ /* Copyright (c) My Company, Ltd. 2021-$curYear. All rights reserved. */ /** * Very useful description, why this file has two classes * foo bar baz */ package com.saveourtool.diktat.example class Example1 { } class Example2 { } """.trimIndent(), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.WRONG_COPYRIGHT_YEAR) fun `copyright year good 6`() { lintMethod( """ /* * Copyright (c) My Company, Ltd. 2002-$curYear. All rights reserved. */ /** * Very useful description, why this file has two classes * foo bar baz */ package com.saveourtool.diktat.example class Example1 { } class Example2 { } """.trimIndent(), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.WRONG_COPYRIGHT_YEAR) fun `copyright year bad`() { lintMethod( """ /* * Copyright (c) My Company, Ltd. 2012-2019. All rights reserved. */ /** * Very useful description, why this file has two classes * foo bar baz */ package com.saveourtool.diktat.example class Example1 { } class Example2 { } """.trimIndent(), DiktatError(1, 1, ruleId, """${Warnings.WRONG_COPYRIGHT_YEAR.warnText()} year should be ${LocalDate.now().year}""", true), rulesConfigList = rulesConfigListInvalidYear ) } @Test @Tag(WarningNames.WRONG_COPYRIGHT_YEAR) fun `copyright year bad 2`() { lintMethod( """ /* * Copyright (c) 2019 My Company, Ltd. All rights reserved. */ /** * Very useful description, why this file has two classes * foo bar baz */ package com.saveourtool.diktat.example class Example1 { } class Example2 { } """.trimIndent(), DiktatError(1, 1, ruleId, """${Warnings.WRONG_COPYRIGHT_YEAR.warnText()} year should be ${LocalDate.now().year}""", true), rulesConfigList = rulesConfigListInvalidYearBeforeCopyright ) } @Test @Tag(WarningNames.HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE) fun `file with zero classes should have header KDoc`() { lintMethod( """ package com.saveourtool.diktat.example val CONSTANT = 42 fun foo(): Int = 42 """.trimIndent(), DiktatError(1, 1, ruleId, "${HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE.warnText()} there are 0 declared classes and/or objects", false), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE) fun `file with multiple classes should have header KDoc`() { lintMethod( """ package com.saveourtool.diktat.example class Example1 { } class Example2 { } """.trimIndent(), DiktatError(1, 1, ruleId, "${HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE.warnText()} there are 2 declared classes and/or objects", false), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.HEADER_WRONG_FORMAT) fun `header KDoc should have newline after it`() { lintMethod( """ |$copyrightBlock |/** | * Very useful description | * foo bar baz | */ |package com.saveourtool.diktat.example | |class Example { } """.trimMargin(), DiktatError(4, 1, ruleId, "${HEADER_WRONG_FORMAT.warnText()} header KDoc should have a new line after", true), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.HEADER_NOT_BEFORE_PACKAGE) fun `header KDoc should be placed before package and imports`() { lintMethod( """ |package com.saveourtool.diktat.example | |import com.saveourtool.diktat.example.Foo | |/** | * This is a code snippet for tests | */ | |/** | * This is an example class | */ |class Example { } """.trimMargin(), DiktatError(5, 1, ruleId, "${HEADER_NOT_BEFORE_PACKAGE.warnText()} header KDoc is located after package or imports", true), rulesConfigList = emptyList() ) } @Test @Tag(WarningNames.HEADER_NOT_BEFORE_PACKAGE) fun `header KDoc object check`() { lintMethod( """ |package com.saveourtool.diktat.example | |import com.saveourtool.diktat.example.Foo | |object TestEntry { |@JvmStatic |fun main(args: Array) { | val properties = TestFrameworkProperties("com/saveourtool/diktat/test/framework/test_framework.properties") | TestProcessingFactory(TestArgumentsReader(args, properties, javaClass.classLoader)).processTests() | } |} """.trimMargin(), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.HEADER_NOT_BEFORE_PACKAGE) fun `header KDoc object and class check`() { lintMethod( """ |package com.saveourtool.diktat.example | |import com.saveourtool.diktat.example.Foo | |object TestEntry { |@JvmStatic |fun main(args: Array) { | val properties = TestFrameworkProperties("com/saveourtool/diktat/test/framework/test_framework.properties") | TestProcessingFactory(TestArgumentsReader(args, properties, javaClass.classLoader)).processTests() | } |} | |class Some {} """.trimMargin(), DiktatError(1, 1, ruleId, "${HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE.warnText()} there are 2 declared classes and/or objects"), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.HEADER_NOT_BEFORE_PACKAGE) fun `header KDoc in gradle script`(@TempDir tempDir: Path) { lintMethodWithFile( """ |version = "0.1.0-SNAPSHOT" | """.trimMargin(), tempDir = tempDir, fileName = "src/main/kotlin/com/saveourtool/diktat/builds.gradle.kts" ) } @Test @Tag(WarningNames.HEADER_NOT_BEFORE_PACKAGE) fun `header KDoc in kts script`(@TempDir tempDir: Path) { lintMethodWithFile( """ |val version = "0.1.0-SNAPSHOT" | """.trimMargin(), tempDir = tempDir, fileName = "src/main/kotlin/com/saveourtool/diktat/Example.kts", DiktatError(1, 1, ruleId, "${HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE.warnText()} there are 0 declared classes and/or objects") ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter2/KdocCommentsFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter2 import com.saveourtool.diktat.ruleset.rules.chapter2.kdoc.KdocComments import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tags import org.junit.jupiter.api.Test class KdocCommentsFixTest : FixTestBase("test/paragraph2/kdoc/", ::KdocComments) { @Test @Tag(WarningNames.COMMENTED_BY_KDOC) fun `check fix code block with kdoc comment`() { fixAndCompare("KdocBlockCommentExpected.kt", "KdocBlockCommentTest.kt") } @Test @Tags(Tag(WarningNames.KDOC_NO_CONSTRUCTOR_PROPERTY), Tag(WarningNames.KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT), Tag(WarningNames.MISSING_KDOC_TOP_LEVEL)) fun `check fix without class kdoc`() { fixAndCompare("ConstructorCommentNoKDocExpected.kt", "ConstructorCommentNoKDocTest.kt") } @Test @Tags(Tag(WarningNames.KDOC_NO_CONSTRUCTOR_PROPERTY), Tag(WarningNames.KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT)) fun `check fix with class kdoc`() { fixAndCompare("ConstructorCommentExpected.kt", "ConstructorCommentTest.kt") } @Test @Tags(Tag(WarningNames.KDOC_NO_CONSTRUCTOR_PROPERTY), Tag(WarningNames.KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT)) fun `check fix with properties in class kdoc`() { fixAndCompare("ConstructorCommentPropertiesExpected.kt", "ConstructorCommentPropertiesTest.kt") } @Test @Tags(Tag(WarningNames.KDOC_NO_CONSTRUCTOR_PROPERTY), Tag(WarningNames.KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT)) fun `should preserve newlines when moving comments from value parameters`() { fixAndCompare("ConstructorCommentNewlineExpected.kt", "ConstructorCommentNewlineTest.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter2/KdocCommentsWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter2 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.constants.Warnings.KDOC_DUPLICATE_PROPERTY import com.saveourtool.diktat.ruleset.constants.Warnings.KDOC_EXTRA_PROPERTY import com.saveourtool.diktat.ruleset.constants.Warnings.KDOC_NO_CLASS_BODY_PROPERTIES_IN_HEADER import com.saveourtool.diktat.ruleset.constants.Warnings.KDOC_NO_CONSTRUCTOR_PROPERTY import com.saveourtool.diktat.ruleset.constants.Warnings.KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT import com.saveourtool.diktat.ruleset.constants.Warnings.MISSING_KDOC_CLASS_ELEMENTS import com.saveourtool.diktat.ruleset.constants.Warnings.MISSING_KDOC_TOP_LEVEL import com.saveourtool.diktat.ruleset.rules.chapter2.kdoc.KdocComments import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tags import org.junit.jupiter.api.Test @Suppress("LargeClass") class KdocCommentsWarnTest : LintTestBase(::KdocComments) { private val ruleId: String = "$DIKTAT_RULE_SET_ID:${KdocComments.NAME_ID}" @Test @Tag(WarningNames.COMMENTED_BY_KDOC) fun `Should warn if kdoc comment is inside code block`() { val code = """ |package com.saveourtool.diktat.example | |/** | * right place for kdoc | */ |class Example { |/** | * right place for kdoc | */ | fun doGood(){ | /** | * wrong place for kdoc | */ | 1+2 | /** | * right place for kdoc | */ | fun prettyPrint(level: Int = 0, maxLevel: Int = -1): String { | return "test" | } | } |} """.trimMargin() lintMethod( code, DiktatError( 11, 9, ruleId, "${Warnings.COMMENTED_BY_KDOC.warnText()} Redundant asterisk in block comment: \\**", true ) ) } @Test @Tag(WarningNames.MISSING_KDOC_TOP_LEVEL) fun `all public classes should be documented with KDoc`() { val code = """ class SomeGoodName { private class InternalClass { } } public open class SomeOtherGoodName { } open class SomeNewGoodName { } public class SomeOtherNewGoodName { } """.trimIndent() lintMethod( code, DiktatError(1, 1, ruleId, "${MISSING_KDOC_TOP_LEVEL.warnText()} SomeGoodName"), DiktatError(6, 1, ruleId, "${MISSING_KDOC_TOP_LEVEL.warnText()} SomeOtherGoodName"), DiktatError(9, 1, ruleId, "${MISSING_KDOC_TOP_LEVEL.warnText()} SomeNewGoodName"), DiktatError(12, 1, ruleId, "${MISSING_KDOC_TOP_LEVEL.warnText()} SomeOtherNewGoodName") ) } @Test @Tag(WarningNames.MISSING_KDOC_TOP_LEVEL) fun `all internal classes should be documented with KDoc`() { val code = """ internal class SomeGoodName { } """.trimIndent() lintMethod( code, DiktatError( 1, 1, ruleId, "${MISSING_KDOC_TOP_LEVEL.warnText()} SomeGoodName" ) ) } @Test @Tag(WarningNames.MISSING_KDOC_TOP_LEVEL) fun `all internal and public functions on top-level should be documented with Kdoc`() { val code = """ fun someGoodName() { } internal fun someGoodNameNew(): String { return " "; } fun main() {} """.trimIndent() lintMethod( code, DiktatError(1, 1, ruleId, "${MISSING_KDOC_TOP_LEVEL.warnText()} someGoodName"), DiktatError(4, 1, ruleId, "${MISSING_KDOC_TOP_LEVEL.warnText()} someGoodNameNew") ) } @Test @Tag(WarningNames.MISSING_KDOC_TOP_LEVEL) fun `all internal and public functions on top-level should be documented with Kdoc (positive case)`() { val code = """ private fun someGoodName() { } """.trimIndent() lintMethod(code) } @Test @Tag(WarningNames.MISSING_KDOC_TOP_LEVEL) fun `positive Kdoc case with private class`() { val code = """ private class SomeGoodName { } """.trimIndent() lintMethod(code) } @Test @Tag(WarningNames.MISSING_KDOC_CLASS_ELEMENTS) fun `Kdoc should present for each class element`() { val code = """ /** * class that contains fields, functions and public subclasses **/ class SomeGoodName { val variable: String = "" private val privateVariable: String = "" fun perfectFunction() { } private fun privateFunction() { } class InternalClass { } private class InternalClass { } public fun main() {} } """.trimIndent() lintMethod( code, DiktatError(5, 5, ruleId, "${MISSING_KDOC_CLASS_ELEMENTS.warnText()} variable"), DiktatError(7, 5, ruleId, "${MISSING_KDOC_CLASS_ELEMENTS.warnText()} perfectFunction"), DiktatError(13, 5, ruleId, "${MISSING_KDOC_CLASS_ELEMENTS.warnText()} InternalClass") ) } @Test @Tag(WarningNames.MISSING_KDOC_CLASS_ELEMENTS) fun `Kdoc shouldn't not be mandatory for overridden functions and props`() { val code = """ /** * class that contains fields, functions and public subclasses **/ class SomeGoodName : Another { val variable: String = "" private val privateVariable: String = "" override val someVal: String = "" fun perfectFunction() { } override fun overrideFunction() { } class InternalClass { } private class InternalClass { } public fun main() {} } """.trimIndent() lintMethod( code, DiktatError(5, 5, ruleId, "${MISSING_KDOC_CLASS_ELEMENTS.warnText()} variable"), DiktatError(8, 5, ruleId, "${MISSING_KDOC_CLASS_ELEMENTS.warnText()} perfectFunction"), DiktatError(14, 5, ruleId, "${MISSING_KDOC_CLASS_ELEMENTS.warnText()} InternalClass") ) } @Test @Tag(WarningNames.MISSING_KDOC_CLASS_ELEMENTS) fun `Kdoc shouldn't present for each class element because Test annotation`() { lintMethod( """ /** * class that contains fields, functions and public subclasses **/ @Test class SomeGoodName { val variable: String = "" private val privateVariable: String = "" fun perfectFunction() { } private fun privateFunction() { } class InternalClass { } private class InternalClass { } } """.trimIndent() ) } @Test @Tag(WarningNames.MISSING_KDOC_CLASS_ELEMENTS) fun `Kdoc should present for each class element (positive)`() { val code = """ /** * class that contains fields, functions and public subclasses **/ class SomeGoodName { /** * class that contains fields, functions and public subclasses **/ val variable: String = "" private val privateVariable: String = "" /** * class that contains fields, functions and public subclasses **/ fun perfectFunction() { } private fun privateFunction() { } /** * class that contains fields, functions and public subclasses **/ class InternalClass { } private class InternalClass { } } """.trimIndent() lintMethod(code) } @Test @Tag(WarningNames.MISSING_KDOC_CLASS_ELEMENTS) fun `regression - should not force documentation on standard methods`() { lintMethod( """ |/** | * This is an example class | */ |class Example { | override fun toString() = "" |} """.trimMargin() ) } @Test @Tags(Tag(WarningNames.KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT), Tag(WarningNames.KDOC_EXTRA_PROPERTY)) fun `check simple primary constructor with comment`() { lintMethod( """ |/** | * @property name d | * @param adsf | * @return something | */ |class Example constructor ( | // short | val name: String |) { |} """.trimMargin(), DiktatError(3, 4, ruleId, "${KDOC_EXTRA_PROPERTY.warnText()} @param adsf", false), DiktatError(7, 4, ruleId, "${KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT.warnText()} add comment for property to KDoc", true) ) } @Test @Tag(WarningNames.KDOC_NO_CONSTRUCTOR_PROPERTY) fun `should trigger on override parameter`() { lintMethod( """ |@Suppress("MISSING_KDOC_TOP_LEVEL") |public class Example ( | override val serializersModule: SerializersModule = EmptySerializersModule |) """.trimMargin(), DiktatError(3, 4, ruleId, "${KDOC_NO_CONSTRUCTOR_PROPERTY.warnText()} add property to KDoc", true) ) } @Test @Tag(WarningNames.KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT) fun `shouldn't trigger because not primary constructor`() { lintMethod( """ |/** | * @property name d | * @property anotherName text | */ |class Example { | constructor( | // name | name: String, | anotherName: String, | oneMoreName: String | ) |} """.trimMargin() ) } @Test @Tags(Tag(WarningNames.KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT), Tag(WarningNames.KDOC_NO_CONSTRUCTOR_PROPERTY)) fun `check constructor with comment`() { lintMethod( """ |/** | * @return some | */ |class Example ( | //some descriptions | val name: String, | anotherName: String, | oneMoreName: String | ) { |} """.trimMargin(), DiktatError(5, 4, ruleId, "${KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT.warnText()} add comment for property to KDoc", true), DiktatError(7, 4, ruleId, "${KDOC_NO_CONSTRUCTOR_PROPERTY.warnText()} add param to KDoc", true), DiktatError(8, 4, ruleId, "${KDOC_NO_CONSTRUCTOR_PROPERTY.warnText()} add param to KDoc", true) ) } @Test @Tags(Tag(WarningNames.KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT), Tag(WarningNames.KDOC_NO_CONSTRUCTOR_PROPERTY)) fun `check constructor with block comment`() { lintMethod( """ |/** | * @return some | */ |class Example ( | /*some descriptions*/val name: String, | anotherName: String, | private val oneMoreName: String | ) { |} """.trimMargin(), DiktatError(5, 4, ruleId, "${KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT.warnText()} add comment for property to KDoc", true), DiktatError(6, 4, ruleId, "${KDOC_NO_CONSTRUCTOR_PROPERTY.warnText()} add param to KDoc", true), DiktatError(7, 4, ruleId, "${KDOC_NO_CONSTRUCTOR_PROPERTY.warnText()} add param to KDoc", true) ) } @Test @Tags(Tag(WarningNames.KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT), Tag(WarningNames.KDOC_NO_CONSTRUCTOR_PROPERTY)) fun `check not property but params`() { lintMethod( """ |/** | * @return some | */ |class Example ( | //some descriptions | private val name: String, | anotherName: String, | private val oneMoreName: String | ) { |} """.trimMargin(), DiktatError(5, 4, ruleId, "${KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT.warnText()} add comment for param to KDoc", true), DiktatError(7, 4, ruleId, "${KDOC_NO_CONSTRUCTOR_PROPERTY.warnText()} add param to KDoc", true), DiktatError(8, 4, ruleId, "${KDOC_NO_CONSTRUCTOR_PROPERTY.warnText()} add param to KDoc", true) ) } @Test @Tags(Tag(WarningNames.KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT), Tag(WarningNames.KDOC_NO_CONSTRUCTOR_PROPERTY)) fun `check constructor with kdoc`() { lintMethod( """ |/** | * @return some | */ |class Example ( | /** | * some descriptions | */ | val name: String, | anotherName: String, | private val oneMoreName: String | ) { |} """.trimMargin(), DiktatError(5, 4, ruleId, "${KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT.warnText()} add comment for property to KDoc", true), DiktatError(9, 4, ruleId, "${KDOC_NO_CONSTRUCTOR_PROPERTY.warnText()} add param to KDoc", true), DiktatError(10, 4, ruleId, "${KDOC_NO_CONSTRUCTOR_PROPERTY.warnText()} add param to KDoc", true) ) } @Test @Tag(WarningNames.KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT) fun `shouldn't fix because KDoc comment and property tag inside`() { lintMethod( """ |/** | * @return some | */ |class Example ( | /** | * sdcjkh | * @property name text2 | * fdfdfd | */ | val name: String, | ) { |} """.trimMargin(), DiktatError(5, 4, ruleId, "${KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT.warnText()} add comment for property to KDoc", false) ) } @Test @Tag(WarningNames.KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT) fun `shouldn't fix because KDoc comment and any tag inside`() { lintMethod( """ |/** | * @return some | */ |class Example ( | /** | * sdcjkh | * @return name text2 | * fdfdfd | */ | val name: String, | ) { |} """.trimMargin(), DiktatError(5, 4, ruleId, "${KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT.warnText()} add comment for property to KDoc", false) ) } @Test @Tags(Tag(WarningNames.KDOC_NO_CONSTRUCTOR_PROPERTY), Tag(WarningNames.KDOC_EXTRA_PROPERTY)) fun `no property kdoc`() { lintMethod( """ |/** | * @property Name text | */ |class Example ( | val name: String, | ) { |} """.trimMargin(), DiktatError(2, 4, ruleId, "${KDOC_EXTRA_PROPERTY.warnText()} @property Name text", false), DiktatError(5, 4, ruleId, "${KDOC_NO_CONSTRUCTOR_PROPERTY.warnText()} add property to KDoc", true) ) } @Test @Tag(WarningNames.KDOC_EXTRA_PROPERTY) fun `extra property in kdoc`() { lintMethod( """ |/** | * @property name bla | * @property kek | */ |class Example ( | val name: String | ) { |} """.trimMargin(), DiktatError(3, 4, ruleId, "${KDOC_EXTRA_PROPERTY.warnText()} @property kek", false) ) } @Test @Tag(WarningNames.KDOC_NO_CONSTRUCTOR_PROPERTY) fun `change property to param in kdoc for private parameter`() { lintMethod( """ |/** | * @property name abc | */ |class Example ( | private val name: String, | ) { |} """.trimMargin(), DiktatError(5, 4, ruleId, "${KDOC_NO_CONSTRUCTOR_PROPERTY.warnText()} change `@property` tag to `@param` tag for to KDoc", true) ) } @Test @Tag(WarningNames.KDOC_NO_CONSTRUCTOR_PROPERTY) fun `change param to property in kdoc for property`() { lintMethod( """ |/** | * @param name abc | */ |class Example ( | val name: String, | ) { |} """.trimMargin(), DiktatError(5, 4, ruleId, "${KDOC_NO_CONSTRUCTOR_PROPERTY.warnText()} change `@param` tag to `@property` tag for to KDoc", true), ) } @Test @Tag(WarningNames.KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT) fun `change param to property in kdoc for property with single comment`() { lintMethod( """ |/** | * @param name abc | */ |class Example ( | //some descriptions | val name: String, | ) { |} """.trimMargin(), DiktatError(5, 4, ruleId, "${KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT.warnText()} change `@param` tag to `@property` tag for and add comment to KDoc", true), ) } @Test @Tag(WarningNames.KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT) fun `change param to property in kdoc for property with block comment`() { lintMethod( """ |/** | * @param name abc | */ |class Example ( | /*some descriptions*/ | val name: String, | ) { |} """.trimMargin(), DiktatError(5, 4, ruleId, "${KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT.warnText()} change `@param` tag to `@property` tag for and add comment to KDoc", true), ) } @Test @Tag(WarningNames.KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT) fun `comment on private parameter`() { lintMethod( """ |/** | * abc | */ |class Example ( | // single-line comment | private val name: String, | ) { |} """.trimMargin(), DiktatError(5, 4, ruleId,"${KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT.warnText()} add comment for param to KDoc", true) ) } @Test @Tag(WarningNames.KDOC_NO_CONSTRUCTOR_PROPERTY) fun `should trigger on private parameter`() { lintMethod( """ |/** | * text | */ |class Example ( | private val name: String, | ) { |} """.trimMargin(), DiktatError(5, 4, ruleId,"${KDOC_NO_CONSTRUCTOR_PROPERTY.warnText()} add param to KDoc", true) ) } @Test @Tags(Tag(WarningNames.KDOC_NO_CONSTRUCTOR_PROPERTY), Tag(WarningNames.MISSING_KDOC_TOP_LEVEL)) fun `no property kdoc and class`() { lintMethod( """ |class Example ( | val name: String, | private val surname: String | ) { |} """.trimMargin(), DiktatError(1, 1, ruleId, "${MISSING_KDOC_TOP_LEVEL.warnText()} Example"), DiktatError(2, 4, ruleId, "${KDOC_NO_CONSTRUCTOR_PROPERTY.warnText()} add property to KDoc", true), DiktatError(3, 4, ruleId, "${KDOC_NO_CONSTRUCTOR_PROPERTY.warnText()} add param to KDoc", true) ) } @Test @Tag(WarningNames.KDOC_NO_CLASS_BODY_PROPERTIES_IN_HEADER) fun `property described only in class KDoc`() { lintMethod( """ |/** | * @property foo lorem ipsum | */ |class Example { | val foo: Any |} """.trimMargin(), DiktatError(5, 5, ruleId, "${KDOC_NO_CLASS_BODY_PROPERTIES_IN_HEADER.warnText()} val foo: Any") ) } @Test fun `property described both in class KDoc and own KDoc`() { lintMethod( """ |/** | * @property foo lorem ipsum | */ |class Example { | /** | * dolor sit amet | */ | val foo: Any |} """.trimMargin(), DiktatError(5, 5, ruleId, "${KDOC_NO_CLASS_BODY_PROPERTIES_IN_HEADER.warnText()} /**...") ) } @Test fun `shouldn't trigger kdoc top level on actual methods`() { lintMethod( """ |actual fun foo() {} |expect fun fo() {} |internal actual fun foo() {} |internal expect fun foo() {} | |expect class B{} | |actual class A{} """.trimMargin(), DiktatError(2, 1, ruleId, "${MISSING_KDOC_TOP_LEVEL.warnText()} fo"), DiktatError(4, 1, ruleId, "${MISSING_KDOC_TOP_LEVEL.warnText()} foo"), DiktatError(6, 1, ruleId, "${MISSING_KDOC_TOP_LEVEL.warnText()} B") ) } @Test fun `should find Kdoc after annotation of function`() { lintMethod( """ |@SomeAnnotation |/** | * Just print a string | * | * @param f string to print | * @return 1 | */ |internal fun prnt(f: String) { | println(f) | return 1 |} """.trimMargin() ) } @Test fun `should find Kdoc after annotation of class`() { lintMethod( """ |@SomeAnnotation |/** | * Test class | */ |class example { | |} """.trimMargin() ) } @Test fun `should find Kdoc inside a modifier list`() { lintMethod( """ |public |/** | * foo | */ |actual fun foo() { } """.trimMargin() ) } @Test fun `should warn if there are duplicate tags 1`() { lintMethod( """ |/** | * @param field1 description1 | * @param field2 description2 | * @param field2 | */ |fun foo(field1: Long, field2: Int) { | // |} """.trimMargin(), DiktatError(4, 4, ruleId, "${KDOC_DUPLICATE_PROPERTY.warnText()} @param field2"), ) } @Test fun `should warn if there are duplicate tags 2`() { lintMethod( """ |/** | * @property field1 | * @property field2 | * @property field2 | */ |@Serializable |data class DataClass( | val field1: String, | val field2: String, |) """.trimMargin(), DiktatError(4, 4, ruleId, "${KDOC_DUPLICATE_PROPERTY.warnText()} @property field2"), ) } @Test fun `should warn if there are duplicate tags 3`() { lintMethod( """ |/** | * @property field1 | * @property field2 | * @param field2 | */ |@Serializable |data class DataClass( | val field1: String, | val field2: String, |) """.trimMargin(), DiktatError(4, 4, ruleId, "${KDOC_DUPLICATE_PROPERTY.warnText()} @param field2"), ) } @Test fun `should warn if there are duplicate tags 4`() { lintMethod( """ |/** | * @property field1 | * @property field1 | * @property field2 | * @param field2 | */ |@Serializable |data class DataClass( | val field1: String, | val field2: String, |) """.trimMargin(), DiktatError(3, 4, ruleId, "${KDOC_DUPLICATE_PROPERTY.warnText()} @property field1"), DiktatError(5, 4, ruleId, "${KDOC_DUPLICATE_PROPERTY.warnText()} @param field2"), ) } @Test @Tag(WarningNames.KDOC_EXTRA_PROPERTY) fun `shouldn't warn extra property on generic type`() { lintMethod( """ |/** | * S3 implementation of Storage | * | * @param s3Operations [S3Operations] to operate with S3 | * @param K type of key | */ |abstract class AbstractReactiveStorage constructor( | s3Operations: S3Operations, |) : ReactiveStorage { | // abcd |} """.trimMargin() ) } @Test @Tag(WarningNames.KDOC_NO_CONSTRUCTOR_PROPERTY) fun `should trigger on generic type`() { lintMethod( """ |/** | * S3 implementation of Storage | * | * @param s3Operations [S3Operations] to operate with S3 | */ |abstract class AbstractReactiveStorage( | s3Operations: S3Operations, |) : ReactiveStorage, AnotherStorage

{ | // abcd |} """.trimMargin(), DiktatError(6, 40, ruleId, "${KDOC_NO_CONSTRUCTOR_PROPERTY.warnText()} add param to KDoc", true), DiktatError(6, 49, ruleId, "${KDOC_NO_CONSTRUCTOR_PROPERTY.warnText()} add param

to KDoc", true), ) } @Test @Tag(WarningNames.KDOC_NO_CONSTRUCTOR_PROPERTY) fun `change property to param in kdoc for generic type`() { lintMethod( """ |/** | * S3 implementation of Storage | * | * @param s3Operations [S3Operations] to operate with S3 | * @property K type of key | */ |abstract class AbstractReactiveStorage( | s3Operations: S3Operations, |) : ReactiveStorage, AnotherStorage

{ | // abcd |} """.trimMargin(), DiktatError(7, 40, ruleId, "${KDOC_NO_CONSTRUCTOR_PROPERTY.warnText()} change `@property` tag to `@param` tag for to KDoc", true), DiktatError(7, 49, ruleId, "${KDOC_NO_CONSTRUCTOR_PROPERTY.warnText()} add param

to KDoc", true), ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter2/KdocFormattingFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter2 import com.saveourtool.diktat.ruleset.rules.chapter2.kdoc.KdocFormatting import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tags import org.junit.jupiter.api.Test class KdocFormattingFixTest : FixTestBase("test/paragraph2/kdoc/", ::KdocFormatting) { @Test @Tag(WarningNames.KDOC_WRONG_SPACES_AFTER_TAG) fun `there should be exactly one white space after tag name`() { fixAndCompare("SpacesAfterTagExpected.kt", "SpacesAfterTagTest.kt") } @Test @Tag(WarningNames.KDOC_WRONG_TAGS_ORDER) fun `basic tags should be ordered in KDocs`() { fixAndCompare("OrderedTagsExpected.kt", "OrderedTagsTest.kt") } @Test @Tag(WarningNames.KDOC_WRONG_TAGS_ORDER) fun `extra new line with tags ordering should not cause assert`() { fixAndCompare("OrderedTagsAssertionExpected.kt", "OrderedTagsAssertionTest.kt") } @Test @Tag(WarningNames.KDOC_NO_NEWLINES_BETWEEN_BASIC_TAGS) fun `basic tags should not have empty lines between`() { fixAndCompare("BasicTagsEmptyLinesExpected.kt", "BasicTagsEmptyLinesTest.kt") } @Test @Tag(WarningNames.KDOC_NO_NEWLINE_AFTER_SPECIAL_TAGS) fun `special tags should have newline after them`() { fixAndCompare("SpecialTagsInKdocExpected.kt", "SpecialTagsInKdocTest.kt") } @Test @Tag(WarningNames.KDOC_NO_DEPRECATED_TAG) fun `@deprecated tag should be substituted with annotation`() { fixAndCompare("DeprecatedTagExpected.kt", "DeprecatedTagTest.kt") } @Test @Tag(WarningNames.KDOC_NEWLINES_BEFORE_BASIC_TAGS) fun `Empty line should be added before block of standard tags`() { fixAndCompare("BasicTagsEmptyLineBeforeExpected.kt", "BasicTagsEmptyLineBeforeTest.kt") } @Test @Tags(Tag(WarningNames.KDOC_NO_DEPRECATED_TAG), Tag(WarningNames.KDOC_NO_NEWLINES_BETWEEN_BASIC_TAGS)) fun `KdocFormatting - all warnings`() { fixAndCompare("KdocFormattingFullExpected.kt", "KdocFormattingFullTest.kt") } @Test @Tags(Tag(WarningNames.KDOC_NO_DEPRECATED_TAG), Tag(WarningNames.KDOC_NO_NEWLINES_BETWEEN_BASIC_TAGS)) fun `KdocFormatting - sort order`() { fixAndCompare("KdocFormattingOrderExpected.kt", "KdocFormattingOrderTest.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter2/KdocFormattingTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter2 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.constants.Warnings.KDOC_EMPTY_KDOC import com.saveourtool.diktat.ruleset.constants.Warnings.KDOC_NEWLINES_BEFORE_BASIC_TAGS import com.saveourtool.diktat.ruleset.constants.Warnings.KDOC_NO_DEPRECATED_TAG import com.saveourtool.diktat.ruleset.constants.Warnings.KDOC_NO_EMPTY_TAGS import com.saveourtool.diktat.ruleset.constants.Warnings.KDOC_NO_NEWLINES_BETWEEN_BASIC_TAGS import com.saveourtool.diktat.ruleset.constants.Warnings.KDOC_NO_NEWLINE_AFTER_SPECIAL_TAGS import com.saveourtool.diktat.ruleset.constants.Warnings.KDOC_WRONG_SPACES_AFTER_TAG import com.saveourtool.diktat.ruleset.constants.Warnings.KDOC_WRONG_TAGS_ORDER import com.saveourtool.diktat.ruleset.rules.chapter2.kdoc.KdocFormatting import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class KdocFormattingTest : LintTestBase(::KdocFormatting) { private val ruleId: String = "$DIKTAT_RULE_SET_ID:${KdocFormatting.NAME_ID}" private val funCode = """ fun foo(a: Int): Int { if (false) throw IllegalStateException() return 2 * a } """.trimIndent() @Test @Tag(WarningNames.KDOC_EMPTY_KDOC) fun `empty KDocs are not allowed - example with empty KDOC_SECTION`() { lintMethod( """/** | *${" ".repeat(5)} | */ |fun foo() = Unit """.trimMargin(), DiktatError(1, 1, ruleId, "${KDOC_EMPTY_KDOC.warnText()} foo", false) ) } @Test @Tag(WarningNames.KDOC_EMPTY_KDOC) fun `empty KDocs are not allowed - example with no KDOC_SECTION`() { lintMethod( """/** | */ |fun foo() = Unit """.trimMargin(), DiktatError(1, 1, ruleId, "${KDOC_EMPTY_KDOC.warnText()} foo", false) ) } @Test @Tag(WarningNames.KDOC_EMPTY_KDOC) fun `empty KDocs are not allowed - without bound identifier`() { lintMethod( """/** | * | */ """.trimMargin(), DiktatError(1, 1, ruleId, "${KDOC_EMPTY_KDOC.warnText()} /**...", false) ) } @Test @Tag(WarningNames.KDOC_EMPTY_KDOC) fun `empty KDocs are not allowed - with anonymous entity`() { lintMethod( """class Example { | /** | * | */ | companion object { } |} """.trimMargin(), DiktatError(2, 5, ruleId, "${KDOC_EMPTY_KDOC.warnText()} object", false) ) } @Test @Tag(WarningNames.KDOC_NO_DEPRECATED_TAG) fun `@deprecated tag is not allowed`() { val invalidCode = """ /** * @deprecated use foo instead */ fun bar() = Unit """.trimIndent() lintMethod(invalidCode, DiktatError(2, 4, ruleId, "${KDOC_NO_DEPRECATED_TAG.warnText()} @deprecated use foo instead", true) ) } @Test @Tag(WarningNames.KDOC_NO_EMPTY_TAGS) fun `no empty descriptions in tag blocks are allowed`() { val invalidCode = """ /** * @param a * @return * @throws IllegalStateException */ $funCode """.trimIndent() lintMethod(invalidCode, DiktatError(3, 16, ruleId, "${KDOC_NO_EMPTY_TAGS.warnText()} @return", false)) } @Test fun `KDocs should contain only one white space between tag and its content (positive example)`() { val validCode = """ /** * @param a dummy int * @return doubled value * @throws IllegalStateException */ $funCode """.trimIndent() lintMethod(validCode) } @Test @Tag(WarningNames.KDOC_WRONG_SPACES_AFTER_TAG) fun `KDocs should contain only one white space between tag and its content`() { val invalidCode = """ /** * @param a dummy int * @param b dummy int * @return doubled value * @throws${'\t'}IllegalStateException */ $funCode """.trimIndent() lintMethod(invalidCode, DiktatError(2, 16, ruleId, "${KDOC_WRONG_SPACES_AFTER_TAG.warnText()} @param", true), DiktatError(3, 16, ruleId, "${KDOC_WRONG_SPACES_AFTER_TAG.warnText()} @param", true), DiktatError(4, 16, ruleId, "${KDOC_WRONG_SPACES_AFTER_TAG.warnText()} @return", true), DiktatError(5, 16, ruleId, "${KDOC_WRONG_SPACES_AFTER_TAG.warnText()} @throws", true)) } @Test @Tag(WarningNames.KDOC_WRONG_SPACES_AFTER_TAG) fun `check end of the line after tag isn't error`() { val invalidCode = """ /** * @implNote * implNote text * * @param a dummy int * @param b dummy int * @return doubled value * @throws IllegalStateException */ $funCode """.trimIndent() lintMethod(invalidCode) } @Test @Tag(WarningNames.KDOC_WRONG_TAGS_ORDER) fun `tags should be ordered in KDocs (positive example)`() { val validCode = """ /** * @param a dummy int * @return doubled value * @throws IllegalStateException */ $funCode """.trimIndent() lintMethod(validCode) } @Test @Tag(WarningNames.KDOC_WRONG_TAGS_ORDER) fun `tags should be ordered in KDocs`() { val invalidCode = """ /** * @return doubled value * @throws IllegalStateException * @param a dummy int */ $funCode """.trimIndent() lintMethod(invalidCode, DiktatError(2, 16, ruleId, "${KDOC_WRONG_TAGS_ORDER.warnText()} @return, @throws, @param", true)) } @Test @Tag(WarningNames.KDOC_WRONG_TAGS_ORDER) fun `tags should be ordered assertion issue`() { val invalidCode = """ /** * Reporter that produces a JSON report as a [Report] * * @property out a sink for output * * @param builder additional configuration lambda for serializers module */ class JsonReporter( override val out: BufferedSink, builder: PolymorphicModuleBuilder.() -> Unit = {} ) : Reporter """.trimIndent() lintMethod(invalidCode, DiktatError(4, 4, ruleId, "${KDOC_NO_NEWLINES_BETWEEN_BASIC_TAGS.warnText()} @property", true), DiktatError(4, 4, ruleId, "${KDOC_WRONG_TAGS_ORDER.warnText()} @property, @param", true) ) } @Test @Tag(WarningNames.KDOC_NO_NEWLINES_BETWEEN_BASIC_TAGS) fun `newlines are not allowed between basic tags`() { val invalidCode = """ /** * @param a dummy int * * @return doubled value * @throws IllegalStateException */ $funCode """.trimIndent() lintMethod(invalidCode, DiktatError(2, 16, ruleId, "${KDOC_NO_NEWLINES_BETWEEN_BASIC_TAGS.warnText()} @param", true), DiktatError(4, 16, ruleId, "${KDOC_NO_NEWLINES_BETWEEN_BASIC_TAGS.warnText()} @return", true)) } @Test @Tag(WarningNames.KDOC_NEWLINES_BEFORE_BASIC_TAGS) fun `basic tags block should have empty line before if there is other KDoc content (positive example)`() { lintMethod( """/** | * Lorem ipsum | * dolor sit amet | * | * @param a integer parameter | */ |fun test(a: Int): Unit = Unit """.trimMargin() ) } @Test @Tag(WarningNames.KDOC_NEWLINES_BEFORE_BASIC_TAGS) fun `basic tags block shouldn't have empty line before if there is no other KDoc content`() { lintMethod( """/** | * | * @param a integer parameter | */ |fun test(a: Int): Unit = Unit """.trimMargin(), DiktatError(3, 4, ruleId, "${KDOC_NEWLINES_BEFORE_BASIC_TAGS.warnText()} @param", true) ) } @Test @Tag(WarningNames.KDOC_NEWLINES_BEFORE_BASIC_TAGS) fun `basic tags block should have empty line before if there is other KDoc content`() { lintMethod( """/** | * Lorem ipsum | * dolor sit amet | * @param a integer parameter | */ |fun test(a: Int): Unit = Unit """.trimMargin(), DiktatError(4, 4, ruleId, "${KDOC_NEWLINES_BEFORE_BASIC_TAGS.warnText()} @param", true) ) } @Test @Tag(WarningNames.KDOC_NO_NEWLINE_AFTER_SPECIAL_TAGS) fun `special tags should have exactly one newline after them (positive example)`() { val validCode = """ /** * @implSpec stuff * implementation details * * @apiNote foo * * @implNote bar * */ $funCode """.trimIndent() lintMethod(validCode) } @Test @Tag(WarningNames.KDOC_NO_NEWLINE_AFTER_SPECIAL_TAGS) fun `special tags should have exactly one newline after them (no newline)`() { val invalidCode = """ /** * @implSpec stuff * @apiNote foo * @implNote bar */ $funCode """.trimIndent() lintMethod(invalidCode, DiktatError(2, 16, ruleId, "${KDOC_NO_NEWLINE_AFTER_SPECIAL_TAGS.warnText()} @implSpec, @apiNote, @implNote", true)) } @Test @Tag(WarningNames.KDOC_NO_NEWLINE_AFTER_SPECIAL_TAGS) fun `special tags should have exactly one newline after them (many lines)`() { val invalidCode = """ /** * @implSpec stuff * * @apiNote foo * * * * @implNote bar */ $funCode """.trimIndent() lintMethod(invalidCode, DiktatError(2, 16, ruleId, "${KDOC_NO_NEWLINE_AFTER_SPECIAL_TAGS.warnText()} @implSpec, @apiNote, @implNote", true)) } @Test @Tag(WarningNames.KDOC_CONTAINS_DATE_OR_AUTHOR) fun `@author tag is not allowed in header comment`() { lintMethod( """ |/** | * Description of this file | * @author anonymous | */ | |package com.saveourtool.diktat.example | |/** | * Description of this class | * @author anonymous | */ |class Example { } """.trimMargin(), DiktatError(3, 4, ruleId, "${Warnings.KDOC_CONTAINS_DATE_OR_AUTHOR.warnText()} @author anonymous"), DiktatError(10, 4, ruleId, "${Warnings.KDOC_CONTAINS_DATE_OR_AUTHOR.warnText()} @author anonymous"), ) } @Test @Tag(WarningNames.KDOC_CONTAINS_DATE_OR_AUTHOR) fun `@since tag should only contain versions`() { lintMethod( """ |/** | * Description of this file | * @since 2019-10-11 | * @since 19-10-11 | * @since 2019.10.11 | * @since 2019/10/11 | * @since 11 Oct 2019 | * @since 1.2.3 | * @since 1.2.3-1 | * @since 1.2.3-SNAPSHOT | * @since 1.2.3-rc-1 | * @since 1.2.3.RELEASE | */ | |package com.saveourtool.diktat.example | |/** | * Description of this file | * @since 2019-10-11 | * @since 1.2.3 | */ |class Example { } """.trimMargin(), DiktatError(3, 4, ruleId, "${Warnings.KDOC_CONTAINS_DATE_OR_AUTHOR.warnText()} @since 2019-10-11"), DiktatError(4, 4, ruleId, "${Warnings.KDOC_CONTAINS_DATE_OR_AUTHOR.warnText()} @since 19-10-11"), DiktatError(5, 4, ruleId, "${Warnings.KDOC_CONTAINS_DATE_OR_AUTHOR.warnText()} @since 2019.10.11"), DiktatError(6, 4, ruleId, "${Warnings.KDOC_CONTAINS_DATE_OR_AUTHOR.warnText()} @since 2019/10/11"), DiktatError(7, 4, ruleId, "${Warnings.KDOC_CONTAINS_DATE_OR_AUTHOR.warnText()} @since 11 Oct 2019"), DiktatError(19, 4, ruleId, "${Warnings.KDOC_CONTAINS_DATE_OR_AUTHOR.warnText()} @since 2019-10-11"), rulesConfigList = emptyList() ) } @Test @Tag(WarningNames.KDOC_CONTAINS_DATE_OR_AUTHOR) fun `@since tag should only contain versions - with configured regex`() { lintMethod( """ |/** | * Description of this file | * @since 2019-10-11 | * @since 1.2.3-rc-1 | * @since 1.2.3.RELEASE | */ | |package com.saveourtool.diktat.example | |/** | * Description of this file | * @since 2019-10-11 | * @since 1.2.3 | */ |class Example { } """.trimMargin(), DiktatError(3, 4, ruleId, "${Warnings.KDOC_CONTAINS_DATE_OR_AUTHOR.warnText()} @since 2019-10-11"), DiktatError(5, 4, ruleId, "${Warnings.KDOC_CONTAINS_DATE_OR_AUTHOR.warnText()} @since 1.2.3.RELEASE"), DiktatError(12, 4, ruleId, "${Warnings.KDOC_CONTAINS_DATE_OR_AUTHOR.warnText()} @since 2019-10-11"), rulesConfigList = listOf( RulesConfig( Warnings.KDOC_CONTAINS_DATE_OR_AUTHOR.name, true, mapOf( "versionRegex" to "\\d+\\.\\d+\\.\\d+[-\\w\\d]*" ) ) ) ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter2/KdocMethodsFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter2 import com.saveourtool.diktat.ruleset.rules.chapter2.kdoc.KdocMethods import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tags import org.junit.jupiter.api.Test class KdocMethodsFixTest : FixTestBase("test/paragraph2/kdoc/package/src/main/kotlin/com/saveourtool/diktat/kdoc/methods", ::KdocMethods) { @Test @Tag(WarningNames.MISSING_KDOC_TOP_LEVEL) fun `Rule should suggest KDoc template for missing KDocs`() { fixAndCompare("MissingKdocExpected.kt", "MissingKdocTested.kt") } @Test @Tag(WarningNames.MISSING_KDOC_ON_FUNCTION) fun `KDoc template should be placed before modifiers`() { fixAndCompare("MissingKdocWithModifiersExpected.kt", "MissingKdocWithModifiersTest.kt") } @Test @Tag(WarningNames.MISSING_KDOC_ON_FUNCTION) fun `KDoc should be for function with single line body`() { fixAndCompare("MissingKdocOnFunctionExpected.kt", "MissingKdocOnFunctionTest.kt") } @Test @Tag(WarningNames.KDOC_EMPTY_KDOC) fun `Rule should not suggest empty KDoc templates`() { fixAndCompare("EmptyKdocExpected.kt", "EmptyKdocTested.kt") } @Test @Tag(WarningNames.KDOC_WITHOUT_PARAM_TAG) fun `@param tag should be added to existing KDoc`() { fixAndCompare("ParamTagInsertionExpected.kt", "ParamTagInsertionTested.kt") } @Test @Tag(WarningNames.KDOC_WITHOUT_RETURN_TAG) fun `@return tag should be added to existing KDoc`() { fixAndCompare("ReturnTagInsertionExpected.kt", "ReturnTagInsertionTested.kt") } @Test @Tag(WarningNames.KDOC_WITHOUT_THROWS_TAG) fun `@throws tag should be added to existing KDoc`() { fixAndCompare("ThrowsTagInsertionExpected.kt", "ThrowsTagInsertionTested.kt") } @Test @Tags( Tag(WarningNames.KDOC_WITHOUT_PARAM_TAG), Tag(WarningNames.KDOC_WITHOUT_RETURN_TAG), Tag(WarningNames.KDOC_WITHOUT_THROWS_TAG) ) fun `KdocMethods rule should reformat code (full example)`() { fixAndCompare("KdocMethodsFullExpected.kt", "KdocMethodsFullTested.kt") } @Test @Tag(WarningNames.KDOC_WITHOUT_THROWS_TAG) fun `Should add throws tag only for throw without catch`() { fixAndCompare("KdocWithoutThrowsTagExpected.kt", "KdocWithoutThrowsTagTested.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter2/KdocMethodsTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter2 import com.saveourtool.diktat.common.config.rules.DIKTAT_COMMON import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.KDOC_TRIVIAL_KDOC_ON_FUNCTION import com.saveourtool.diktat.ruleset.constants.Warnings.KDOC_WITHOUT_PARAM_TAG import com.saveourtool.diktat.ruleset.constants.Warnings.KDOC_WITHOUT_RETURN_TAG import com.saveourtool.diktat.ruleset.constants.Warnings.KDOC_WITHOUT_THROWS_TAG import com.saveourtool.diktat.ruleset.constants.Warnings.MISSING_KDOC_ON_FUNCTION import com.saveourtool.diktat.ruleset.rules.chapter2.kdoc.KdocMethods import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tags import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir import java.nio.file.Path class KdocMethodsTest : LintTestBase(::KdocMethods) { private val ruleId: String = "$DIKTAT_RULE_SET_ID:${KdocMethods.NAME_ID}" private val funCode = """ fun doubleInt(a: Int): Int { if (Config.condition) throw IllegalStateException() return 2 * a } """.trimIndent() @Test @Tags( Tag(WarningNames.KDOC_WITHOUT_PARAM_TAG), Tag(WarningNames.KDOC_WITHOUT_RETURN_TAG), Tag(WarningNames.KDOC_WITHOUT_THROWS_TAG) ) fun `Accessible methods with parameters, return type and throws should have proper KDoc (positive example)`() { val validCode = """ /** * Test method * @param a - dummy integer * @return doubled value * @throws IllegalStateException */ $funCode """.trimIndent() lintMethod(validCode) } @Test @Tag(WarningNames.MISSING_KDOC_TOP_LEVEL) fun `Warning should not be triggered for private functions`() { val validCode = "private $funCode" lintMethod(validCode) } @Test @Tag(WarningNames.MISSING_KDOC_TOP_LEVEL) fun `anonymous function`() { val code = """ package com.saveourtool.diktat.test fun foo() { val sum: (Int) -> Int = fun(x): Int = x + x } """.trimIndent() lintMethod(code, DiktatError(3, 1, ruleId, "${MISSING_KDOC_ON_FUNCTION.warnText()} foo", false), ) } @Test @Tags( Tag(WarningNames.KDOC_WITHOUT_PARAM_TAG), Tag(WarningNames.KDOC_WITHOUT_RETURN_TAG), Tag(WarningNames.KDOC_WITHOUT_THROWS_TAG), Tag(WarningNames.MISSING_KDOC_ON_FUNCTION) ) fun `Warning should not be triggered for functions in tests`(@TempDir tempDir: Path) { val validCode = "@Test $funCode" val complexAnnotationCode = "@Anno(test = [\"args\"]) $funCode" // do not force KDoc on annotated function lintMethodWithFile(validCode, tempDir = tempDir, fileName = "src/main/kotlin/com/saveourtool/diktat/Example.kt") // no false positive triggers on annotations lintMethodWithFile(complexAnnotationCode, tempDir = tempDir, fileName = "src/main/kotlin/com/saveourtool/diktat/Example.kt", DiktatError(1, 1, ruleId, "${MISSING_KDOC_ON_FUNCTION.warnText()} doubleInt", true) ) // should check all .kt files unless both conditions on location and name are true lintMethodWithFile(funCode, tempDir = tempDir, fileName = "src/test/kotlin/com/saveourtool/diktat/Example.kt", DiktatError(1, 1, ruleId, "${MISSING_KDOC_ON_FUNCTION.warnText()} doubleInt", true) ) // should allow to set custom test dirs lintMethodWithFile(funCode, tempDir = tempDir, fileName = "src/jvmTest/kotlin/com/saveourtool/diktat/ExampleTest.kt", rulesConfigList = listOf(RulesConfig(DIKTAT_COMMON, true, mapOf("testDirs" to "test,jvmTest"))) ) } @Test @Tag(WarningNames.KDOC_WITHOUT_PARAM_TAG) fun `Empty parameter list should not trigger warning about @param`() { val validCode = """ /** * Test method * @return zero * @throws IllegalStateException */ fun foo(): Int { return 0 } """.trimIndent() lintMethod(validCode) } @Test @Tag(WarningNames.KDOC_WITHOUT_PARAM_TAG) fun `All methods with parameters should have @param KDoc`() { val invalidKdoc = """ /** * Test method * @return doubled value * @throws IllegalStateException */ """.trimIndent() val invalidCode = """ $invalidKdoc $funCode """.trimIndent() lintMethod(invalidCode, DiktatError(1, 13, ruleId, "${KDOC_WITHOUT_PARAM_TAG.warnText()} doubleInt (a)", true) ) } @Test @Tag(WarningNames.KDOC_WITHOUT_PARAM_TAG) fun `All methods with parameters should have @param KDoc for each parameter`() { val invalidKdoc = """ /** * Test method * @param a - dummy integer * @return doubled value * @throws IllegalStateException */ """.trimIndent() val invalidCode = """ $invalidKdoc fun addInts(a: Int, b: Int): Int = a + b """.trimIndent() lintMethod(invalidCode, DiktatError(1, 12, ruleId, "${KDOC_WITHOUT_PARAM_TAG.warnText()} addInts (b)", true) ) } @Test @Tag(WarningNames.KDOC_WITHOUT_RETURN_TAG) fun `All methods with explicit return type excluding Unit should have @return KDoc`() { val invalidKdoc = """ /** * Test method * @param a - dummy integer * @throws IllegalStateException */ """.trimIndent() val invalidCode = """ $invalidKdoc $funCode """.trimIndent() lintMethod(invalidCode, DiktatError(1, 13, ruleId, "${KDOC_WITHOUT_RETURN_TAG.warnText()} doubleInt", true) ) } @Test @Tag(WarningNames.KDOC_WITHOUT_RETURN_TAG) fun `All methods with expression body should have @return tag or explicitly set return type to Unit`() { val kdocWithoutReturn = """ /** * Test method * @param a - dummy integer * @throws IllegalStateException */ """.trimIndent() val invalidCode = """ $kdocWithoutReturn fun foo(a: Int) = bar(2 * a) $kdocWithoutReturn fun bar(a: Int): Unit = this.list.add(a) private val list = mutableListOf() """.trimIndent() lintMethod(invalidCode, DiktatError(1, 12, ruleId, "${KDOC_WITHOUT_RETURN_TAG.warnText()} foo", true) ) } @Test @Tag(WarningNames.KDOC_WITHOUT_THROWS_TAG) fun `All methods with throw in method body should have @throws KDoc`() { val invalidKdoc = """ /** * Test method * @param a - dummy integer * @return doubled value */ """.trimIndent() val invalidCode = """ $invalidKdoc $funCode """.trimIndent() lintMethod(invalidCode, DiktatError(1, 13, ruleId, "${KDOC_WITHOUT_THROWS_TAG.warnText()} doubleInt (IllegalStateException)", true) ) } @Test @Tag(WarningNames.KDOC_WITHOUT_THROWS_TAG) fun `Linter shouldn't detect throws inside comments`() { val invalidKdoc = """ /** * Test method * @param a - dummy integer * @return doubled value */ """.trimIndent() val invalidCode = """ $invalidKdoc fun foo(a: Int) { // throw Exception() return bar } """.trimIndent() lintMethod(invalidCode) } @Test @Tag(WarningNames.KDOC_WITHOUT_THROWS_TAG) fun `All thrown exceptions should be in KDoc`() { val invalidCode = """ /** * Test method * @param a - dummy integer * @return doubled value * @throws IllegalStateException */ fun doubleInt(a: Int): Int { if (Config.condition) throw IllegalStateException() if (Config.condition2) throw IllegalAccessException() return 2 * a } """.trimIndent() lintMethod(invalidCode, DiktatError(1, 1, ruleId, "${KDOC_WITHOUT_THROWS_TAG.warnText()} doubleInt (IllegalAccessException)", true) ) } @Test @Tag(WarningNames.KDOC_WITHOUT_THROWS_TAG) fun `No warning when throw has matching catch`() { lintMethod( """ /** * Test method * @param a: Int - dummy integer * @return doubled value */ fun foo(a: Int): Int { try { if (a < 0) throw NumberFormatExecption() } catch (e: ArrayIndexOutOfBounds) { print(1) } catch (e: NullPointerException) { print(2) } catch (e: NumberFormatExecption) { print(3) } return 2 * a } """.trimIndent()) } @Test @Tag(WarningNames.KDOC_WITHOUT_THROWS_TAG) fun `Warning when throw doesn't have matching catch`() { lintMethod( """ /** * Test method * @param a: Int - dummy integer * @return doubled value */ fun foo(a: Int): Int { try { if (a < 0) throw NumberFormatException() throw NullPointerException() throw NoSuchElementException() } catch (e: NoSuchElementException) { print(1) } catch (e: IllegalArgumentException) { print(2) } return 2 * a } """.trimIndent(), DiktatError(1, 1, ruleId, "${KDOC_WITHOUT_THROWS_TAG.warnText()} foo (NullPointerException)", true)) } @Test @Tag(WarningNames.KDOC_WITHOUT_THROWS_TAG) fun `No warning when throw has matching catch, which is parent exception to throw`() { lintMethod( """ /** * Test method * @param a: Int - dummy integer * @return doubled value */ fun foo(a: Int): Int { try { if (a < 0) throw NumberFormatException() } catch (e: IllegalArgumentException) { print(1) } return 2 * a } """.trimIndent()) } @Test @Tag(WarningNames.MISSING_KDOC_TOP_LEVEL) fun `do not force documentation on standard methods`() { lintMethod( """ |class Example { | override fun toString() = "example" | | override fun equals(other: Any?) = false | | override fun hashCode() = 42 |} | |fun main() { } """.trimMargin() ) lintMethod( """ |class Example { | override fun toString(): String { return "example" } | | override fun equals(other: Any?): Boolean { return false } | | override fun hashCode(): Int { return 42 } |} | |fun main(vararg args: String) { } """.trimMargin() ) } @Test @Tag(WarningNames.MISSING_KDOC_ON_FUNCTION) fun `should not force documentation on single line getters and setters`() { lintMethod( """ |class Example { | fun setX(x: Type) { | this.x = x | } | | fun getX(): Type { | return x | } | | fun getY() = this.y | | fun setY(y: Type) { | this.validate(y) | this.y = y | } | | fun getZ(): TypeZ { | baz(z) | return z | } |} """.trimMargin(), DiktatError(12, 5, ruleId, "${MISSING_KDOC_ON_FUNCTION.warnText()} setY", true), DiktatError(17, 5, ruleId, "${MISSING_KDOC_ON_FUNCTION.warnText()} getZ", true) ) } @Test @Tag(WarningNames.MISSING_KDOC_ON_FUNCTION) fun `regression - warn about missing KDoc even if it cannot be autocorrected`() { lintMethod( """ |fun foo() { } """.trimMargin(), DiktatError(1, 1, ruleId, "${MISSING_KDOC_ON_FUNCTION.warnText()} foo", false) ) } @Test @Tag(WarningNames.KDOC_TRIVIAL_KDOC_ON_FUNCTION) fun `should check if KDoc is not trivial`() { lintMethod( """ |/** | * Returns X | */ |fun getX(): TypeX { return x } """.trimMargin(), DiktatError(2, 3, ruleId, "${KDOC_TRIVIAL_KDOC_ON_FUNCTION.warnText()} Returns X", false) ) } @Test @Tag(WarningNames.MISSING_KDOC_ON_FUNCTION) fun `should not trigger on override funcs`() { lintMethod( """ |class Some : A { | override fun foo() {} | | override fun bar(t: T): U { return U() } |} """.trimMargin() ) } @Test @Tag(WarningNames.MISSING_KDOC_ON_FUNCTION) fun `should check if KfDoc is not trivial`() { lintMethod( """ |fun foo(x: Int): TypeX { | val q = goo() | throw UnsupportedOperationException() | return qwe |} """.trimMargin(), DiktatError(1, 1, ruleId, "${MISSING_KDOC_ON_FUNCTION.warnText()} foo", true) ) } @Test @Tag(WarningNames.MISSING_KDOC_ON_FUNCTION) fun `KDoc should be for function with single line body`() { lintMethod( """ |fun hasNoChildren() = children.size == 0 |fun getFirstChild() = children.elementAtOrNull(0) """.trimMargin(), DiktatError(1, 1, ruleId, "${MISSING_KDOC_ON_FUNCTION.warnText()} hasNoChildren", true), DiktatError(2, 1, ruleId, "${MISSING_KDOC_ON_FUNCTION.warnText()} getFirstChild", true) ) } @Test @Tag(WarningNames.MISSING_KDOC_ON_FUNCTION) fun `KDoc shouldn't be for function with name as method`() { lintMethod( """ |@GetMapping("/projects") |fun getProjects() = projectService.getProjects(x.prop()) """.trimMargin(), ) } @Test @Tag(WarningNames.MISSING_KDOC_ON_FUNCTION) fun `KDoc shouldn't trigger on actual methods`() { lintMethod( """ |actual fun writeToConsoleAc(msg: String, outputType: OutputStreamType) {} |expect fun writeToConsoleEx(msg: String, outputType: OutputStreamType) {} """.trimMargin(), DiktatError(2, 1, ruleId, "${MISSING_KDOC_ON_FUNCTION.warnText()} writeToConsoleEx", true), ) } @Test @Tag(WarningNames.MISSING_KDOC_ON_FUNCTION) fun `KDoc shouldn't trigger on local functions`() { lintMethod( """ |fun printHelloAndBye() { | fun printHello() { | print("Hello") | } | printHello() | val ab = 5 | ab?.let { | fun printBye() { | print("Bye") | } | printBye() | } |} """.trimMargin(), DiktatError(1, 1, ruleId, "${MISSING_KDOC_ON_FUNCTION.warnText()} printHelloAndBye", false), ) } @Test @Tag(WarningNames.MISSING_KDOC_ON_FUNCTION) fun `KDoc shouldn't trigger on functions with KDoc`() { lintMethod( """ |/** | * prints "Hello" and "Bye" | */ |fun printHelloAndBye() { | fun printHello() { | print("Hello") | } | printHello() | val ab = 5 | ab?.let { | fun printBye() { | print("Bye") | } | printBye() | } |} """.trimMargin(), ) } @Test @Tag(WarningNames.MISSING_KDOC_ON_FUNCTION) fun `KDoc shouldn't trigger on nested local functions`() { lintMethod( """ |fun printHelloAndBye() { | fun printHello() { | print("Hello") | fun printBye() { | print("Bye") | } | fun printDots() { | print("...") | } | printBye() | printDots() | } | printHello() |} """.trimMargin(), DiktatError(1, 1, ruleId, "${MISSING_KDOC_ON_FUNCTION.warnText()} printHelloAndBye", false), ) } @Test @Tag(WarningNames.MISSING_KDOC_ON_FUNCTION) fun `KDoc shouldn't trigger on local functions with KDoc`() { lintMethod( """ |fun printHelloAndBye() { | fun printHello() { | print("Hello") | } | printHello() | val ab = 5 | ab?.let { | /** | * prints "Bye" | */ | fun printBye() { | print("Bye") | } | printBye() | } |} """.trimMargin(), DiktatError(1, 1, ruleId, "${MISSING_KDOC_ON_FUNCTION.warnText()} printHelloAndBye", false), ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter2/KdocParamPresentWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter2 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings.KDOC_WITHOUT_PARAM_TAG import com.saveourtool.diktat.ruleset.constants.Warnings.MISSING_KDOC_ON_FUNCTION import com.saveourtool.diktat.ruleset.rules.chapter2.kdoc.KdocMethods import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tags import org.junit.jupiter.api.Test class KdocParamPresentWarnTest : LintTestBase(::KdocMethods) { private val ruleId = "$DIKTAT_RULE_SET_ID:${KdocMethods.NAME_ID}" @Test @Tag(WarningNames.KDOC_WITHOUT_PARAM_TAG) fun `check simple correct example`() { lintMethod( """ |/** |* @param a - leftOffset |*/ |fun foo(a: Int) {} """.trimMargin() ) } @Test @Tags(Tag(WarningNames.KDOC_WITHOUT_PARAM_TAG), Tag(WarningNames.KDOC_WITHOUT_PARAM_TAG)) fun `check wrong example with russian letter name`() { lintMethod( """ |/** |* @param A - leftOffset |* @param В - russian letter |*/ |fun foo(a: Int, B: Int) {} """.trimMargin(), DiktatError(1, 1, ruleId, "${KDOC_WITHOUT_PARAM_TAG.warnText()} foo (a, B)", true), DiktatError(2, 3, ruleId, "${KDOC_WITHOUT_PARAM_TAG.warnText()} A param isn't present in argument list"), DiktatError(3, 3, ruleId, "${KDOC_WITHOUT_PARAM_TAG.warnText()} В param isn't present in argument list") ) } @Test @Tag(WarningNames.KDOC_WITHOUT_PARAM_TAG) fun `check wrong example without param in fun`() { lintMethod( """ |/** |* @param A - leftOffset |*/ |fun foo() {} """.trimMargin(), DiktatError(2, 3, ruleId, "${KDOC_WITHOUT_PARAM_TAG.warnText()} A param isn't present in argument list") ) } @Test @Tag(WarningNames.KDOC_WITHOUT_PARAM_TAG) fun `check empty param`() { lintMethod( """ |/** |* @param |*/ |fun foo() {} | |/** |* @param |*/ |fun foo (a: Int) {} """.trimMargin(), DiktatError(6, 1, ruleId, "${KDOC_WITHOUT_PARAM_TAG.warnText()} foo (a)", true) ) } @Test @Tag(WarningNames.KDOC_WITHOUT_PARAM_TAG) fun `check different order`() { lintMethod( """ |/** |* @param a - qwe |* @param b - qwe |*/ |fun foo(b: Int, a: Int) {} """.trimMargin() ) } @Test @Tags(Tag(WarningNames.KDOC_WITHOUT_PARAM_TAG), Tag(WarningNames.MISSING_KDOC_ON_FUNCTION)) fun `check without kdoc and fun `() { lintMethod( """ |fun foo(b: Int, a: Int) {} | |/** |* @param a - qwe |*/ """.trimMargin(), DiktatError(1, 1, ruleId, "${MISSING_KDOC_ON_FUNCTION.warnText()} foo", true) ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter2/comments/CommentedCodeTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter2.comments import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings.COMMENTED_OUT_CODE import com.saveourtool.diktat.ruleset.rules.chapter2.comments.CommentsRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class CommentedCodeTest : LintTestBase(::CommentsRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${CommentsRule.NAME_ID}" @Test @Tag(WarningNames.COMMENTED_OUT_CODE) fun `Should warn if commented out import or package directive is detected (single line comments)`() { lintMethod( """ |//package com.saveourtool.diktat.example | |import org.junit.Test |// this is an actual comment |//import org.junit.Ignore """.trimMargin(), DiktatError(1, 1, ruleId, "${COMMENTED_OUT_CODE.warnText()} package com.saveourtool.diktat.example", false), DiktatError(5, 1, ruleId, "${COMMENTED_OUT_CODE.warnText()} import org.junit.Ignore", false) ) } @Test @Tag(WarningNames.COMMENTED_OUT_CODE) fun `Should warn if commented out imports are detected (block comments)`() { lintMethod( """ |/*import org.junit.Test |import org.junit.Ignore*/ """.trimMargin(), DiktatError(1, 1, ruleId, "${COMMENTED_OUT_CODE.warnText()} import org.junit.Test", false), DiktatError(1, 1, ruleId, "${COMMENTED_OUT_CODE.warnText()} import org.junit.Ignore", false) ) } @Test @Tag(WarningNames.COMMENTED_OUT_CODE) fun `Should warn if commented out code is detected (block comments)`() { lintMethod( """ |import org.junit.Test | |fun foo(a: Int): Int { | /* println(a + 42) | println("This is a test string") | val b = a*10 | */ | return 0 |} """.trimMargin(), DiktatError(4, 5, ruleId, "${COMMENTED_OUT_CODE.warnText()} println(a + 42)", false) ) } @Test @Tag(WarningNames.COMMENTED_OUT_CODE) fun `Should warn if commented out code is detected (single line comments)`() { lintMethod( """ |import org.junit.Test | |fun foo(a: Int): Int { |// println(a + 42) |// println("This is a test string") | return 0 |} """.trimMargin()) } @Test @Tag(WarningNames.COMMENTED_OUT_CODE) fun `Should warn if commented out function is detected single line comments`() { lintMethod( """ |import org.junit.Test | |//fun foo(a: Int): Int { |// println(a + 42) |// println("This is a test string") |// return 0 |//} """.trimMargin(), DiktatError(3, 1, ruleId, "${COMMENTED_OUT_CODE.warnText()} fun foo(a: Int): Int {", false) ) } @Test @Tag(WarningNames.COMMENTED_OUT_CODE) fun `Should warn if commented out function is detected - single line comments with surrounding text`() { lintMethod( """ |import org.junit.Test | |// this function is disabled for now |//fun foo(a: Int): Int { |// println(a + 42) |// println("This is a test string") |// return 0 |//} """.trimMargin(), DiktatError(4, 1, ruleId, "${COMMENTED_OUT_CODE.warnText()} fun foo(a: Int): Int {", false) ) } @Test @Tag(WarningNames.COMMENTED_OUT_CODE) fun `Should warn if commented out function is detected (block comment)`() { lintMethod( """ |import org.junit.Test | |/*fun foo(a: Int): Int { | println(a + 42) | println("This is a test string") | return 0 |}*/ """.trimMargin(), DiktatError(3, 1, ruleId, "${COMMENTED_OUT_CODE.warnText()} fun foo(a: Int): Int {", false) ) } @Test @Tag(WarningNames.COMMENTED_OUT_CODE) fun `Should warn if detects commented out code (example with indents)`() { lintMethod( """ |//import org.junit.Ignore |import org.junit.Test | |class Example { | // this function is disabled for now | //fun foo(a: Int): Int { | // println(a + 42) | // println("This is a test string") | // return 0 | //} |} """.trimMargin(), DiktatError(1, 1, ruleId, "${COMMENTED_OUT_CODE.warnText()} import org.junit.Ignore", false), DiktatError(6, 5, ruleId, "${COMMENTED_OUT_CODE.warnText()} fun foo(a: Int): Int {", false) ) } @Test @Tag(WarningNames.COMMENTED_OUT_CODE) @Suppress("TOO_LONG_FUNCTION", "LongMethod") fun `very long commented code`() { lintMethod( """ |class ScheduleTest { |/* | fun clickFilters_showFilters() { | checkAnimationsDisabled() | | onView(withId(R.id.filter_fab)).perform(click()) | | val uncheckedFilterContentDesc = | getDisabledFilterContDesc(FakeConferenceDataSource.FAKE_SESSION_TAG_NAME) | val checkedFilterContentDesc = | getActiveFilterContDesc(FakeConferenceDataSource.FAKE_SESSION_TAG_NAME) | | // Scroll to the filter | onView(allOf(withId(R.id.recyclerview_filter), withParent(withId(R.id.filter_sheet)))) | .perform( | RecyclerViewActions.scrollTo( | withContentDescription(uncheckedFilterContentDesc) | ) | ) | | onView(withContentDescription(uncheckedFilterContentDesc)) | .check(matches(isDisplayed())) | .perform(click()) | | // Check that the filter is enabled | onView( | allOf( | withId(R.id.filter_label), | withContentDescription(checkedFilterContentDesc), | not(withParent(withId(R.id.filter_description_tags))) | ) | ) | .check(matches(isDisplayed())) | .perform(click()) | } | | private fun applyFilter(filter: String) { | // Open the filters sheet | onView(withId(R.id.filter_fab)).perform(click()) | | // Get the content description of the view we need to click on | val uncheckedFilterContentDesc = | resources.getString(R.string.a11y_filter_not_applied, filter) | | onView(allOf(withId(R.id.recyclerview_filter), withParent(withId(R.id.filter_sheet)))) | .check(matches(isDisplayed())) | | // Scroll to the filter | onView(allOf(withId(R.id.recyclerview_filter), withParent(withId(R.id.filter_sheet)))) | .perform( | RecyclerViewActions.scrollTo( | withContentDescription(uncheckedFilterContentDesc) | ) | ) | } | */ |} """.trimMargin(), DiktatError(2, 1, ruleId, "${COMMENTED_OUT_CODE.warnText()} fun clickFilters_showFilters() {", false) ) } @Test @Tag(WarningNames.COMMENTED_OUT_CODE) fun `Should warn if detects commented out code example with IDEA style indents`() { lintMethod( """ |//import org.junit.Ignore |import org.junit.Test | |class Example { | // this function is disabled for now |// fun foo(a: Int): Int { |// println(a + 42) |// println("This is a test string") |// return 0 |// } |} """.trimMargin(), DiktatError(1, 1, ruleId, "${COMMENTED_OUT_CODE.warnText()} import org.junit.Ignore", false), DiktatError(6, 1, ruleId, "${COMMENTED_OUT_CODE.warnText()} fun foo(a: Int): Int {", false) ) } @Test @Tag(WarningNames.COMMENTED_OUT_CODE) fun `should trigger on class with one space after comment start token`() { lintMethod( """ |// class Test: Exception() """.trimMargin()) } @Test @Tag(WarningNames.COMMENTED_OUT_CODE) fun `should trigger on class with one space after comment start token and 2 modifiers #1`() { lintMethod( """ |// public data class Test(val some: Int): Exception() """.trimMargin(), DiktatError(1, 1, ruleId, "${COMMENTED_OUT_CODE.warnText()} public data class Test(val some: Int): Exception()", false)) } @Test @Tag(WarningNames.COMMENTED_OUT_CODE) fun `should trigger on one-line comment with var or val`() { lintMethod( """ |// var foo: Int = 1 """.trimMargin(), DiktatError(1, 1, ruleId, "${COMMENTED_OUT_CODE.warnText()} var foo: Int = 1", false)) } @Test @Tag(WarningNames.COMMENTED_OUT_CODE) fun `should trigger on one-line multi comment`() { lintMethod( """ | // fun foo() { | // varfoo adda foofoo | // } """.trimMargin(), DiktatError(1, 2, ruleId, "${COMMENTED_OUT_CODE.warnText()} fun foo() {", false)) } @Test @Tag(WarningNames.COMMENTED_OUT_CODE) fun `should trigger on one-line comment`() { lintMethod( """ | // class A { val a = 2 } """.trimMargin(), DiktatError(1, 2, ruleId, "${COMMENTED_OUT_CODE.warnText()} class A { val a = 2 }", false)) } @Test @Tag(WarningNames.COMMENTED_OUT_CODE) fun `should trigger on one-line block comment`() { lintMethod( """ | /* class A { val a = 2 } */ """.trimMargin(), DiktatError(1, 2, ruleId, "${COMMENTED_OUT_CODE.warnText()} class A { val a = 2 }", false)) } @Test @Tag(WarningNames.COMMENTED_OUT_CODE) fun `should trigger on class with one space after comment start token and 2 modifiers #2`() { lintMethod( """ |// internal sealed class Test: Exception() """.trimMargin()) } @Test @Tag(WarningNames.COMMENTED_OUT_CODE) fun `should trigger on import with one space after comment start token`() { lintMethod( """ |// import some.org """.trimMargin(), DiktatError(1, 1, ruleId, "${COMMENTED_OUT_CODE.warnText()} import some.org", false)) } @Test @Tag(WarningNames.COMMENTED_OUT_CODE) fun `should trigger on package with one space after comment start token`() { lintMethod( """ |// package some.org """.trimMargin(), DiktatError(1, 1, ruleId, "${COMMENTED_OUT_CODE.warnText()} package some.org", false)) } @Test @Tag(WarningNames.COMMENTED_OUT_CODE) fun `should trigger on function with one space after comment start token - { sign`() { lintMethod( """ |// fun someFunc(name: String): Boolean { |// val a = 5 |// } """.trimMargin(), DiktatError(1, 1, ruleId, "${COMMENTED_OUT_CODE.warnText()} fun someFunc(name: String): Boolean {", false)) } @Test @Tag(WarningNames.COMMENTED_OUT_CODE) fun `should trigger on function with one space after comment start token - = sign`() { lintMethod( """ |// fun someFunc(name: String): Boolean = |// name.contains("a") """.trimMargin(), DiktatError(1, 1, ruleId, "${COMMENTED_OUT_CODE.warnText()} fun someFunc(name: String): Boolean =", false)) } @Test @Tag(WarningNames.COMMENTED_OUT_CODE) fun `should trigger on function with one space after comment start token pulbic modifier`() { lintMethod( """ |// public fun someFunc(name: String): Boolean = |// name.contains("a") """.trimMargin(), DiktatError(1, 1, ruleId, "${COMMENTED_OUT_CODE.warnText()} public fun someFunc(name: String): Boolean =", false)) } @Test @Tag(WarningNames.COMMENTED_OUT_CODE) fun `should not trigger on multiline comments #1`() { lintMethod( """ |/* | | Copyright 2018-2020 John Doe. | | Licensed under the Apache License, Version 2.0 (the "License"); | you may not use this file except in compliance with the License. | You may obtain a copy of the License at | | http://www.apache.org/licenses/LICENSE-2.0 | | Unless required by applicable law or agreed to in writing, software | distributed under the License is distributed on an "AS IS" BASIS, | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | See the License for the specific language governing permissions and | limitations under the License. | |*/ """.trimMargin()) } @Test @Tag(WarningNames.COMMENTED_OUT_CODE) fun `should not trigger on multiline comments #2`() { lintMethod( """ | /* | * some text here | maybe even with another line | */ """.trimMargin()) } @Test @Tag(WarningNames.COMMENTED_OUT_CODE) fun `should not trigger on Copyright and another comment`() { lintMethod( """ /* Copyright (c) Your Company Name Here. 2010-2021 */ package com.saveourtool.diktat /* x = 2 + 4 + 1 */ // x = 2+4 // if true make this /* class A { fun foo() } */ """.trimMargin(), DiktatError(7, 13, ruleId, "${COMMENTED_OUT_CODE.warnText()} x = 2 + 4 + 1", false), DiktatError(14, 13, ruleId, "${COMMENTED_OUT_CODE.warnText()} class A {", false) ) } @Test @Tag(WarningNames.COMMENTED_OUT_CODE) fun `should not trigger with suppress`() { lintMethod( """ @Suppress("UnsafeCallOnNullableType", "COMMENTED_OUT_CODE") private fun handleProperty(property: KtProperty) { /* x = 1 */ } @Suppress("COMMENTED_OUT_CODE") class A { // val x = 10 } """.trimMargin() ) } @Test @Tag(WarningNames.COMMENTED_OUT_CODE) fun `should not trigger on 'imports'`() { lintMethod( """ /* Checks if specified imports can be found in classpath. */ class Example /* Checks if specified import can be found in classpath. */ class Example2 /* import this and you died. */ class Example3 """.trimMargin() ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/AnnotationNewLineRuleFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.ruleset.rules.chapter3.AnnotationNewLineRule import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class AnnotationNewLineRuleFixTest : FixTestBase("test/paragraph3/annotations", ::AnnotationNewLineRule) { @Test @Tag(WarningNames.ANNOTATION_NEW_LINE) fun `should fix func and class annotations`() { fixAndCompare("AnnotationSingleLineExpected.kt", "AnnotationSingleLineTest.kt") } @Test @Tag(WarningNames.ANNOTATION_NEW_LINE) fun `should fix constructor annotations`() { fixAndCompare("AnnotationConstructorSingleLineExpected.kt", "AnnotationConstructorSingleLineTest.kt") } @Test @Tag(WarningNames.ANNOTATION_NEW_LINE) fun `shouldn't fix correct annotation with comment`() { fixAndCompare("AnnotationCommentExpected.kt", "AnnotationCommentTest.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/AnnotationNewLineRuleWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter3.AnnotationNewLineRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class AnnotationNewLineRuleWarnTest : LintTestBase(::AnnotationNewLineRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${AnnotationNewLineRule.NAME_ID}" @Test @Tag(WarningNames.ANNOTATION_NEW_LINE) fun `annotation class test good`() { lintMethod( """ |@SomeAnnotation |@SecondAnnotation |class A { | val a = 5 |} """.trimMargin() ) } @Test @Tag(WarningNames.ANNOTATION_NEW_LINE) fun `annotation class test good 2`() { lintMethod( """ |@SomeAnnotation class A { | val a = 5 |} """.trimMargin() ) } @Test @Tag(WarningNames.ANNOTATION_NEW_LINE) fun `annotation class test bad`() { lintMethod( """ |@SomeAnnotation @SecondAnnotation |class A { | val a = 5 |} """.trimMargin(), DiktatError(1, 1, ruleId, "${Warnings.ANNOTATION_NEW_LINE.warnText()} @SomeAnnotation not on a single line", true), DiktatError(1, 17, ruleId, "${Warnings.ANNOTATION_NEW_LINE.warnText()} @SecondAnnotation not on a single line", true) ) } @Test @Tag(WarningNames.ANNOTATION_NEW_LINE) fun `annotation class test bad 2`() { lintMethod( """ |@SomeAnnotation @SecondAnnotation class A { | val a = 5 |} """.trimMargin(), DiktatError(1, 1, ruleId, "${Warnings.ANNOTATION_NEW_LINE.warnText()} @SomeAnnotation not on a single line", true), DiktatError(1, 17, ruleId, "${Warnings.ANNOTATION_NEW_LINE.warnText()} @SecondAnnotation not on a single line", true) ) } @Test @Tag(WarningNames.ANNOTATION_NEW_LINE) fun `annotation fun test good`() { lintMethod( """ |class A { | val a = 5 | | @SomeAnnotation | @SecondAnnotation | fun someFunc() { | val a = 3 | } |} """.trimMargin() ) } @Test @Tag(WarningNames.ANNOTATION_NEW_LINE) fun `annotation fun test good 2`() { lintMethod( """ |class A { | val a = 5 | | @SomeAnnotation fun someFunc() { | val a = 3 | } |} """.trimMargin() ) } @Test @Tag(WarningNames.ANNOTATION_NEW_LINE) fun `annotation fun test bad`() { lintMethod( """ |class A { | val a = 5 | | @SomeAnnotation @SecondAnnotation fun someFunc() { | val a = 3 | } |} """.trimMargin(), DiktatError(4, 3, ruleId, "${Warnings.ANNOTATION_NEW_LINE.warnText()} @SomeAnnotation not on a single line", true), DiktatError(4, 19, ruleId, "${Warnings.ANNOTATION_NEW_LINE.warnText()} @SecondAnnotation not on a single line", true) ) } @Test @Tag(WarningNames.ANNOTATION_NEW_LINE) fun `annotation fun test bad 2`() { lintMethod( """ |class A { | val a = 5 | | @SomeAnnotation @SecondAnnotation | fun someFunc() { | val a = 3 | } |} """.trimMargin(), DiktatError(4, 3, ruleId, "${Warnings.ANNOTATION_NEW_LINE.warnText()} @SomeAnnotation not on a single line", true), DiktatError(4, 19, ruleId, "${Warnings.ANNOTATION_NEW_LINE.warnText()} @SecondAnnotation not on a single line", true) ) } @Test @Tag(WarningNames.ANNOTATION_NEW_LINE) fun `annotation constructor test good`() { lintMethod( """ |public class Conf |@Inject |constructor(conf: Int) { | |} """.trimMargin() ) } @Test @Tag(WarningNames.ANNOTATION_NEW_LINE) fun `annotation constructor test good 2`() { lintMethod( """ |public class Conf @Inject constructor(conf: Int) { | |} """.trimMargin() ) } @Test @Tag(WarningNames.ANNOTATION_NEW_LINE) fun `annotation constructor test good 3`() { lintMethod( """ |public class Conf |@Inject |@SomeAnnotation |constructor(conf: Int) { | |} """.trimMargin() ) } @Test @Tag(WarningNames.ANNOTATION_NEW_LINE) fun `annotation secondary constructor test good`() { lintMethod( """ |public class Conf { | @FirstAnnotation constructor(conf: Conf) { | | } |} """.trimMargin() ) } @Test @Tag(WarningNames.ANNOTATION_NEW_LINE) fun `annotation secondary constructor test bad`() { lintMethod( """ |public class Conf { | @FirstAnnotation @SecondAnnotation constructor(conf: Conf) { | | } |} """.trimMargin(), DiktatError(2, 4, ruleId, "${Warnings.ANNOTATION_NEW_LINE.warnText()} @FirstAnnotation not on a single line", true), DiktatError(2, 21, ruleId, "${Warnings.ANNOTATION_NEW_LINE.warnText()} @SecondAnnotation not on a single line", true) ) } @Test @Tag(WarningNames.ANNOTATION_NEW_LINE) fun `annotation constructor test bad`() { lintMethod( """ |public class Conf @Inject @SomeAnnotation constructor(conf: Int) { | |} """.trimMargin(), DiktatError(1, 19, ruleId, "${Warnings.ANNOTATION_NEW_LINE.warnText()} @Inject not on a single line", true), DiktatError(1, 27, ruleId, "${Warnings.ANNOTATION_NEW_LINE.warnText()} @SomeAnnotation not on a single line", true) ) } @Test @Tag(WarningNames.ANNOTATION_NEW_LINE) fun `annotation constructor test bad 2`() { lintMethod( """ |public class Conf @Inject |@SomeAnnotation constructor(conf: Int) { | |} """.trimMargin(), DiktatError(1, 19, ruleId, "${Warnings.ANNOTATION_NEW_LINE.warnText()} @Inject not on a single line", true), DiktatError(2, 1, ruleId, "${Warnings.ANNOTATION_NEW_LINE.warnText()} @SomeAnnotation not on a single line", true) ) } @Test @Tag(WarningNames.ANNOTATION_NEW_LINE) fun `no warns in func params`() { lintMethod( """ |public class Conf { | fun someFunc(@SomeAnnotation conf: JsonConf, @SecondAnnotation some: Int) { | | } |} """.trimMargin() ) } @Test @Tag(WarningNames.ANNOTATION_NEW_LINE) fun `no warns in func params 2`() { lintMethod( """ |public class Conf { | fun someFunc(@SomeAnnotation @AnotherAnnotation conf: JsonConf, @SecondAnnotation @ThirdAnnotation some: Int) { | | } |} """.trimMargin() ) } @Test @Tag(WarningNames.ANNOTATION_NEW_LINE) fun `no warn in correct annotation with comment`() { lintMethod( """ |@ExperimentalStdlibApi // to use `scan` on sequence | @Suppress("WRONG_NEWLINES") | override fun checkNode() {} """.trimMargin() ) } @Test @Tag(WarningNames.ANNOTATION_NEW_LINE) fun `should warn annotation for several annotations`() { lintMethod( """ |@ExperimentalStdlibApi /* */ @Hello |override fun checkNode() {} | |/* */ @Goo |class A {} | |@A1 |/* */ @A2 |@A3 |class A {} | | |@Foo class Foo {} | |@Foo |class Foo {} | |@Foo @Goo val loader: DataLoader | |@Foo |@goo val loader: DataLoader """.trimMargin(), DiktatError(1, 1, ruleId, "${Warnings.ANNOTATION_NEW_LINE.warnText()} @ExperimentalStdlibApi not on a single line", true), DiktatError(1, 32, ruleId, "${Warnings.ANNOTATION_NEW_LINE.warnText()} @Hello not on a single line", true), ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/BlockStructureBracesFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.ruleset.rules.chapter3.BlockStructureBraces import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class BlockStructureBracesFixTest : FixTestBase ("test/paragraph3/block_brace", ::BlockStructureBraces) { @Test @Tag(WarningNames.BRACES_BLOCK_STRUCTURE_ERROR) fun `should fix open and close brace in if-else expression`() { fixAndCompare("IfElseBracesExpected.kt", "IfElseBracesTest.kt") } @Test @Tag(WarningNames.BRACES_BLOCK_STRUCTURE_ERROR) fun `should fix open and close brace in class expression`() { fixAndCompare("ClassBracesExpected.kt", "ClassBracesTest.kt") } @Test @Tag(WarningNames.BRACES_BLOCK_STRUCTURE_ERROR) fun `should fix open and close brace in do-while expression`() { fixAndCompare("DoWhileBracesExpected.kt", "DoWhileBracesTest.kt") } @Test @Tag(WarningNames.BRACES_BLOCK_STRUCTURE_ERROR) fun `should fix open and close brace in loops expression`() { fixAndCompare("LoopsBracesExpected.kt", "LoopsBracesTest.kt") } @Test @Tag(WarningNames.BRACES_BLOCK_STRUCTURE_ERROR) fun `should fix open and close brace in when expression`() { fixAndCompare("WhenBranchesExpected.kt", "WhenBranchesTest.kt") } @Test @Tag(WarningNames.BRACES_BLOCK_STRUCTURE_ERROR) fun `should fix open and close brace in try-catch-finally expression`() { fixAndCompare("TryBraceExpected.kt", "TryBraceTest.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/BlockStructureBracesWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.BRACES_BLOCK_STRUCTURE_ERROR import com.saveourtool.diktat.ruleset.rules.chapter3.BlockStructureBraces import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class BlockStructureBracesWarnTest : LintTestBase(::BlockStructureBraces) { private val ruleId = "$DIKTAT_RULE_SET_ID:${BlockStructureBraces.NAME_ID}" private val rulesConfigList: List = listOf( RulesConfig(BRACES_BLOCK_STRUCTURE_ERROR.name, true, mapOf("openBraceNewline" to "False", "closeBraceNewline" to "False")) ) private val rulesConfigListIgnoreOpen: List = listOf( RulesConfig(BRACES_BLOCK_STRUCTURE_ERROR.name, true, mapOf("openBraceNewline" to "False")) ) private val rulesConfigListIgnoreClose: List = listOf( RulesConfig(BRACES_BLOCK_STRUCTURE_ERROR.name, true, mapOf("closeBraceNewline" to "False")) ) @Test @Tag(WarningNames.BRACES_BLOCK_STRUCTURE_ERROR) fun `check if expression with new line else`() { lintMethod( """ |fun foo() { | if (x < -5) { | goo() | } | else { | koo()} |} """.trimMargin(), DiktatError(4, 6, ruleId, "${BRACES_BLOCK_STRUCTURE_ERROR.warnText()} incorrect new line after closing brace", true), DiktatError(6, 13, ruleId, "${BRACES_BLOCK_STRUCTURE_ERROR.warnText()} no newline before closing brace", true) ) } @Test @Tag(WarningNames.BRACES_BLOCK_STRUCTURE_ERROR) fun `correct if expression without else`() { val withBrace = """ |fun foo() { | if (x > 5) { | x-- | } |} """.trimMargin() lintMethod(withBrace) val withoutBrace = """ |fun foo() { | if (x > 5) | x-- |} """.trimMargin() lintMethod(withoutBrace) } @Test @Tag(WarningNames.BRACES_BLOCK_STRUCTURE_ERROR) fun `check correct if with else-if expression`() { lintMethod( """ |fun foo() { | if (x < -5) { | goo() | } else if (x > 5) { | hoo() | } else { | koo() | } |} """.trimMargin() ) } @Test @Tag(WarningNames.BRACES_BLOCK_STRUCTURE_ERROR) fun `check lambda with empty block`() { lintMethod( """ |fun foo() { | run { | | } |} """.trimMargin() ) } @Test @Tag(WarningNames.BRACES_BLOCK_STRUCTURE_ERROR) fun `check empty block in else expression`() { lintMethod( """ |fun foo() { | if (x < -5) { | goo() | } else if (x > 5) { | hoo() | } else { | } |} """.trimMargin() ) } @Test @Tag(WarningNames.BRACES_BLOCK_STRUCTURE_ERROR) fun `check wrong empty block in if expression`() { lintMethod( """ |fun foo() { | if (x < -5) { | goo() | } else {} |} """.trimMargin(), DiktatError(4, 13, ruleId, "${BRACES_BLOCK_STRUCTURE_ERROR.warnText()} incorrect same line after opening brace", true), DiktatError(4, 13, ruleId, "${BRACES_BLOCK_STRUCTURE_ERROR.warnText()} no newline before closing brace", true) ) } @Test @Tag(WarningNames.BRACES_BLOCK_STRUCTURE_ERROR) fun `check if expression with wrong opening brace position`() { lintMethod( """ |fun foo() { | if (x < -5) | { | bf() | } else { f() | } |} """.trimMargin(), DiktatError(2, 16, ruleId, "${BRACES_BLOCK_STRUCTURE_ERROR.warnText()} incorrect newline before opening brace", true), DiktatError(5, 13, ruleId, "${BRACES_BLOCK_STRUCTURE_ERROR.warnText()} incorrect same line after opening brace", true) ) } @Test @Tag(WarningNames.BRACES_BLOCK_STRUCTURE_ERROR) fun `check if expression with wrong closing brace position`() { lintMethod( """ |fun foo() { | if (x < -5) { | bf() } | else { | f() } |} """.trimMargin(), DiktatError(3, 13, ruleId, "${BRACES_BLOCK_STRUCTURE_ERROR.warnText()} no newline before closing brace", true), DiktatError(3, 14, ruleId, "${BRACES_BLOCK_STRUCTURE_ERROR.warnText()} incorrect new line after closing brace", true), DiktatError(5, 12, ruleId, "${BRACES_BLOCK_STRUCTURE_ERROR.warnText()} no newline before closing brace", true) ) } @Test @Tag(WarningNames.BRACES_BLOCK_STRUCTURE_ERROR) fun `check wrong brace in if expression but with off configuration`() { lintMethod( """ |fun foo() { | if (x < -5) | { | goo() } | else | { | hoo() } |} """.trimMargin(), DiktatError(4, 15, ruleId, "${BRACES_BLOCK_STRUCTURE_ERROR.warnText()} incorrect new line after closing brace", true), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.BRACES_BLOCK_STRUCTURE_ERROR) fun `check function expression with wrong open brace with configuration`() { lintMethod( """ |fun foo() |{ | pyu() |} """.trimMargin(), rulesConfigList = rulesConfigListIgnoreOpen ) } @Test @Tag(WarningNames.BRACES_BLOCK_STRUCTURE_ERROR) fun `check empty fun expression with override annotation`() { lintMethod( """ |override fun foo() { |} """.trimMargin() ) } @Test @Tag(WarningNames.BRACES_BLOCK_STRUCTURE_ERROR) fun `check one line fun`() { lintMethod( """ |fun foo() = 0 """.trimMargin() ) } @Test @Tag(WarningNames.BRACES_BLOCK_STRUCTURE_ERROR) fun `check fun with empty block won't be processed`() { lintMethod( """ |fun foo() {} """.trimMargin() ) } @Test @Tag(WarningNames.BRACES_BLOCK_STRUCTURE_ERROR) fun `check function expression with wrong close brace`() { lintMethod( """ |fun foo() { | pyu() } """.trimMargin(), DiktatError(2, 10, ruleId, "${BRACES_BLOCK_STRUCTURE_ERROR.warnText()} no newline before closing brace", true) ) } @Test @Tag(WarningNames.BRACES_BLOCK_STRUCTURE_ERROR) fun `check simple wrong open brace when expression`() { lintMethod( """ |fun a(x: Int) { | when (x) | { | 1 -> println(2) | else -> println("df") | } |} """.trimMargin(), DiktatError(2, 12, ruleId, "${BRACES_BLOCK_STRUCTURE_ERROR.warnText()} incorrect newline before opening brace", true) ) } @Test @Tag(WarningNames.BRACES_BLOCK_STRUCTURE_ERROR) fun `check correct simple for without brace `() { lintMethod( """ |fun a(x: Int) { | for (i in 1..3) | println(i) |} """.trimMargin() ) } @Test @Tag(WarningNames.BRACES_BLOCK_STRUCTURE_ERROR) fun `check wrong for expression with empty block but with config`() { lintMethod( """ |fun a(x: Int) { | for (i in 1..3) {} |} """.trimMargin() ) } @Test @Tag(WarningNames.BRACES_BLOCK_STRUCTURE_ERROR) fun `check correct while without brace`() { lintMethod( """ |fun sdf() { | while (x > 0) | x-- |} """.trimMargin() ) } @Test @Tag(WarningNames.BRACES_BLOCK_STRUCTURE_ERROR) fun `check wrong do-while with open brace`() { lintMethod( """ |fun sdf() { | do | { | x-- | } while (x != 0) |} """.trimMargin(), DiktatError(2, 6, ruleId, "${BRACES_BLOCK_STRUCTURE_ERROR.warnText()} incorrect newline before opening brace", true) ) } @Test @Tag(WarningNames.BRACES_BLOCK_STRUCTURE_ERROR) fun `check try-catch-finally with wrong position catch and finally words `() { lintMethod( """ |fun divideOrZero(numerator: Int, denominator: Int): Int { | try { | return numerator / denominator | } catch (e: ArithmeticException) { | return 0 | } | catch (e: Exception) { | return 1 | } | finally { | println("Hello") | } |} """.trimMargin(), DiktatError(6, 5, ruleId, "${BRACES_BLOCK_STRUCTURE_ERROR.warnText()} incorrect new line after closing brace", true), DiktatError(9, 5, ruleId, "${BRACES_BLOCK_STRUCTURE_ERROR.warnText()} incorrect new line after closing brace", true) ) } @Test @Tag(WarningNames.BRACES_BLOCK_STRUCTURE_ERROR) fun `check wrong try-catch with open and close braces`() { lintMethod( """ |fun divideOrZero(numerator: Int, denominator: Int): Int { | try { return numerator / denominator | } catch (e: ArithmeticException) { | return 0 | } catch (e: Exception) { | return 1 } |} """.trimMargin(), DiktatError(2, 9, ruleId, "${BRACES_BLOCK_STRUCTURE_ERROR.warnText()} incorrect same line after opening brace", true), DiktatError(6, 17, ruleId, "${BRACES_BLOCK_STRUCTURE_ERROR.warnText()} no newline before closing brace", true) ) } @Test @Tag(WarningNames.BRACES_BLOCK_STRUCTURE_ERROR) fun `check correct simple class expression`() { lintMethod( """ |class A { | fun foo() { | println("Hello") | } |} """.trimMargin() ) } @Test @Tag(WarningNames.BRACES_BLOCK_STRUCTURE_ERROR) fun `check wrong simple class expression but with config`() { lintMethod( """ |class A |{ | fun foo() { | println("Hello") | } } """.trimMargin(), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.BRACES_BLOCK_STRUCTURE_ERROR) fun `check wrong close brace in object expression but with ignore config`() { lintMethod( """ |object A { | fun foo() { | println("Hello") | } | | val x: Int = 10 } """.trimMargin(), rulesConfigList = rulesConfigListIgnoreClose ) } @Test @Tag(WarningNames.BRACES_BLOCK_STRUCTURE_ERROR) fun `check wrong open brace in object expression`() { lintMethod( """ |object A |{ | fun foo() { | println("Hello") | } | | val x: Int = 10 |} """.trimMargin(), DiktatError(1, 9, ruleId, "${BRACES_BLOCK_STRUCTURE_ERROR.warnText()} incorrect newline before opening brace", true) ) } @Test @Tag(WarningNames.BRACES_BLOCK_STRUCTURE_ERROR) fun `check init expression with wrong opening and closing brace position `() { lintMethod( """ |class A { | init | { | foo() } | | fun foo() { | println("Hello") | } |} """.trimMargin(), DiktatError(2, 8, ruleId, "${BRACES_BLOCK_STRUCTURE_ERROR.warnText()} incorrect newline before opening brace", true), DiktatError(4, 14, ruleId, "${BRACES_BLOCK_STRUCTURE_ERROR.warnText()} no newline before closing brace", true) ) } @Test @Tag(WarningNames.BRACES_BLOCK_STRUCTURE_ERROR) fun `check correct simple constructor expression`() { lintMethod( """ |class Person() { | constructor(id: Int) { | println(id) | } |} """.trimMargin() ) } @Test @Tag(WarningNames.BRACES_BLOCK_STRUCTURE_ERROR) fun `check wrong constructor expression but with ignore opening brace config`() { lintMethod( """ |class Person() |{ | constructor(id: Int) { | println(id) | } |} """.trimMargin(), rulesConfigList = rulesConfigListIgnoreOpen ) } @Test @Tag(WarningNames.BRACES_BLOCK_STRUCTURE_ERROR) fun `check lambda with incorrect close brace position`() { lintMethod( """ |class Person() { | val list = listOf("Hello", "World") | | fun foo(){ | val size = list.map { it.length } | size.forEach { println(it) } | } |} """.trimMargin() ) } @Test @Tag(WarningNames.BRACES_BLOCK_STRUCTURE_ERROR) fun `check lambdas`() { lintMethod( """ |fun foo() { | val x = t.map {it -> it.size} | val y = q | .map { it.treeParent } | .filter { it.elementType == CLASS } | val y = q | .map { it.treeParent } | .filter { it.elementType == CLASS && | it.text == "sdc" } |} """.trimMargin(), DiktatError(9, 33, ruleId, "${BRACES_BLOCK_STRUCTURE_ERROR.warnText()} no newline before closing brace", true) ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/BooleanExpressionsRuleFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.ruleset.rules.chapter3.BooleanExpressionsRule import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class BooleanExpressionsRuleFixTest : FixTestBase("test/paragraph3/boolean_expressions", ::BooleanExpressionsRule) { @Test @Tag(WarningNames.COMPLEX_BOOLEAN_EXPRESSION) fun fixBooleanExpressions() { fixAndCompare("BooleanExpressionsExpected.kt", "BooleanExpressionsTest.kt") } @Test @Tag(WarningNames.COMPLEX_BOOLEAN_EXPRESSION) fun `check distributive law fixing`() { fixAndCompare("DistributiveLawExpected.kt", "DistributiveLawTest.kt") } @Test @Tag(WarningNames.COMPLEX_BOOLEAN_EXPRESSION) fun `check same expressions`() { fixAndCompare("SameExpressionsInConditionExpected.kt", "SameExpressionsInConditionTest.kt") } @Test @Tag(WarningNames.COMPLEX_BOOLEAN_EXPRESSION) fun `check substitution works properly`() { fixAndCompare("SubstitutionIssueExpected.kt", "SubstitutionIssueTest.kt") } @Test @Tag(WarningNames.COMPLEX_BOOLEAN_EXPRESSION) fun `check ordering is persisted`() { fixAndCompare("OrderIssueExpected.kt", "OrderIssueTest.kt") } @Test @Tag(WarningNames.COMPLEX_BOOLEAN_EXPRESSION) fun `check handling of negative expression`() { fixAndCompare("NegativeExpressionExpected.kt", "NegativeExpressionTest.kt") } @Test @Tag(WarningNames.COMPLEX_BOOLEAN_EXPRESSION) fun `check expression simplification`() { fixAndCompare("ExpressionSimplificationExpected.kt", "ExpressionSimplificationTest.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/BooleanExpressionsRuleWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter3.BooleanExpressionsRule import com.saveourtool.diktat.ruleset.utils.KotlinParser import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import java.io.ByteArrayOutputStream import java.io.PrintStream class BooleanExpressionsRuleWarnTest : LintTestBase(::BooleanExpressionsRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${BooleanExpressionsRule.NAME_ID}" @Test @Tag(WarningNames.COMPLEX_BOOLEAN_EXPRESSION) fun `check boolean expression`() { lintMethod( """ |fun foo() { | if (some != null && some != null && some == null) { | goo() | } | | if (some != null && some == 7) { | | } | | if (a > 3 && b > 3 && a > 3) { | | } | | if (a && a && b > 4) { | | } |} """.trimMargin(), DiktatError(2, 9, ruleId, "${Warnings.COMPLEX_BOOLEAN_EXPRESSION.warnText()} some != null && some != null && some == null", true), DiktatError(10, 9, ruleId, "${Warnings.COMPLEX_BOOLEAN_EXPRESSION.warnText()} a > 3 && b > 3 && a > 3", true), DiktatError(14, 9, ruleId, "${Warnings.COMPLEX_BOOLEAN_EXPRESSION.warnText()} a && a && b > 4", true) ) } @Test @Tag(WarningNames.COMPLEX_BOOLEAN_EXPRESSION) fun `check laws#1`() { lintMethod( """ |fun foo() { | if (some != null && (some != null || a > 5)) { | | } | | if (a > 5 || (a > 5 && b > 6)) { | | } | | if (!!(a > 5 && q > 6)) { | | } | | if (a > 5 && false) { | | } | | if (a > 5 || (!(a > 5) && b > 5)) { | | } |} """.trimMargin(), DiktatError(2, 9, ruleId, "${Warnings.COMPLEX_BOOLEAN_EXPRESSION.warnText()} some != null && (some != null || a > 5)", true), DiktatError(6, 9, ruleId, "${Warnings.COMPLEX_BOOLEAN_EXPRESSION.warnText()} a > 5 || (a > 5 && b > 6)", true), DiktatError(10, 9, ruleId, "${Warnings.COMPLEX_BOOLEAN_EXPRESSION.warnText()} !!(a > 5 && q > 6)", true), DiktatError(14, 9, ruleId, "${Warnings.COMPLEX_BOOLEAN_EXPRESSION.warnText()} a > 5 && false", true), DiktatError(18, 9, ruleId, "${Warnings.COMPLEX_BOOLEAN_EXPRESSION.warnText()} a > 5 || (!(a > 5) && b > 5)", true) ) } @Test @Tag(WarningNames.COMPLEX_BOOLEAN_EXPRESSION) fun `check distributive laws`() { lintMethod( """ |fun foo() { | if ((a > 5 || b > 5) && (a > 5 || c > 5)) { | | } | | if (a > 5 && b > 5 || a > 5 && c > 5) { | | } |} """.trimMargin(), DiktatError(2, 9, ruleId, "${Warnings.COMPLEX_BOOLEAN_EXPRESSION.warnText()} (a > 5 || b > 5) && (a > 5 || c > 5)", true), DiktatError(6, 9, ruleId, "${Warnings.COMPLEX_BOOLEAN_EXPRESSION.warnText()} a > 5 && b > 5 || a > 5 && c > 5", true) ) } @Test @Tag(WarningNames.COMPLEX_BOOLEAN_EXPRESSION) fun `check distributive laws #2`() { lintMethod( """ |fun foo() { | if ((a > 5 || b > 5) && (a > 5 || c > 5) && (a > 5 || d > 5)) { | | } | | if (a > 5 && b > 5 || a > 5 && c > 5 || a > 5 || d > 5) { | | } |} """.trimMargin(), DiktatError(2, 9, ruleId, "${Warnings.COMPLEX_BOOLEAN_EXPRESSION.warnText()} (a > 5 || b > 5) && (a > 5 || c > 5) && (a > 5 || d > 5)", true), DiktatError(6, 9, ruleId, "${Warnings.COMPLEX_BOOLEAN_EXPRESSION.warnText()} a > 5 && b > 5 || a > 5 && c > 5 || a > 5 || d > 5", true) ) } @Test @Tag(WarningNames.COMPLEX_BOOLEAN_EXPRESSION) fun `should not trigger on method calls`() { lintMethod( """ |fun foo() { | if (a.and(b)) { | | } |} """.trimMargin() ) } @Test fun `test makeCorrectExpressionString method #1`() { checkExpressionFormatter("a > 5 && b < 6", "(A & B)", 2) } @Test fun `test makeCorrectExpressionString method #2`() { checkExpressionFormatter("a > 5 && b < 6 && c > 7 || a > 5", "(A & B & C | A)", 3) } @Test fun `test makeCorrectExpressionString method #3`() { checkExpressionFormatter("a > 5 && b < 6 && (c > 3 || b < 6) && a > 5", "(A & B & (C | B) & A)", 3) } @Test fun `test makeCorrectExpressionString method #4`() { checkExpressionFormatter("a > 5 && b < 6 && (c > 3 || b < 6) && a > 666", "(A & B & (C | B) & D)", 4) } @Test fun `test makeCorrectExpressionString method #5 - should not convert single expressions`() { checkExpressionFormatter("a.and(b)", "(a.and(b))", 0) } @Test fun `test makeCorrectExpressionString method #6 - should not convert single expressions`() { checkExpressionFormatter("x.isFoo()", "(x.isFoo())", 0) } @Test fun `test makeCorrectExpressionString method #7`() { checkExpressionFormatter("x.isFoo() && true", "(A & true)", 1) } @Test fun `test makeCorrectExpressionString method #8`() { checkExpressionFormatter( "a > 5 && b > 6 || c > 7 && a > 5", "(A & B | C & A)", 3 ) } @Test fun `test makeCorrectExpressionString method #9 - should not account for boolean operators in nested lambdas`() { checkExpressionFormatter( """ nextNode != null && nextNode.findChildByType(CALL_EXPRESSION)?.text?.let { it == "trimIndent()" || it == "trimMargin()" } == true """.trimIndent(), "(A & B)", 2 ) } @Test fun `test makeCorrectExpressionString method #10 - single variable in condition`() { checkExpressionFormatter( "::testContainerId.isInitialized", "(::testContainerId.isInitialized)", 0 ) } @Test fun `test makeCorrectExpressionString method #11 - variable in condition and binary expression`() { checkExpressionFormatter( "::testContainerId.isInitialized || a > 2", "(B | A)", 2 ) } @Test fun `test makeCorrectExpressionString method - comment should be removed`() { checkExpressionFormatter( """ foo && bar && // FixMe: isLetterOrDigit is not supported in Kotlin 1.4, but 1.5 is not compiling right now setOf('_', '-', '.', '"', '\'').baz() && ch.isLetterOrDigit() """.trimIndent(), "(C & D & B & A)", 4 ) } @Test @Suppress("TOO_LONG_FUNCTION", "LongMethod") fun `regression - should not log ANTLR errors when parsing is not required`() { val stream = ByteArrayOutputStream() System.setErr(PrintStream(stream)) lintMethod( """ fun foo() { // nested boolean expressions in lambdas if (currentProperty.nextSibling { it.elementType == PROPERTY } == nextProperty) {} if (rightSide != null && leftSide != null && rightSide.size == leftSide.size && rightSide.zip(leftSide).all { (first, second) -> first.text == second.text }) {} // nested lambda with if-else if (currentProperty.nextSibling { if (it.elementType == PROPERTY) true else false } == nextProperty) {} // nested boolean expressions in lambdas with multi-line expressions if (node.elementType == TYPE_REFERENCE && node .parents() .map { it.elementType } .none { it == SUPER_TYPE_LIST || it == TYPEALIAS }) {} // binary expression with boolean literal if (result?.flag == true) {} if (leftOffset + binaryText.length > wrongBinaryExpression.maximumLineLength && index != 0) {} // with in and !in if (!isImportOrPackage && previousNonWhiteSpaceNode in acc.last()) {} if (node.elementType == LABEL_QUALIFIER && node.text !in labels && node.treeParent.elementType in stopWords) {} if ((node.treeNext.elementType == RBRACE) xor (node.treePrev.elementType == LBRACE)) {} if (listOfNodesBeforeNestedIf.any { it.elementType !in allowedTypes } || listOfNodesAfterNestedIf.any { it.elementType !in allowedTypes }) { return null } if ((parentNode.psi as KtIfExpression).`else` != null || (nestedIfNode.psi as KtIfExpression).`else` != null) {} if (listOfWarnings.add(currNode.startOffset to currNode)) {} } """.trimIndent() ) System.setErr(System.err) val stderr = stream.toString() Assertions.assertTrue(stderr.isEmpty()) { "stderr should be empty, but got the following:${System.lineSeparator()}$stderr" } } private fun checkExpressionFormatter( expression: String, expectedRepresentation: String, expectedMapSize: Int ) { val stream = ByteArrayOutputStream() System.setErr(PrintStream(stream)) val node = KotlinParser().createNode(expression) val rule = BooleanExpressionsRule(emptyList()) val map: BooleanExpressionsRule.ExpressionsReplacement = rule.ExpressionsReplacement() val result = rule.formatBooleanExpressionAsString(node, map) Assertions.assertEquals(expectedRepresentation, result) Assertions.assertEquals(expectedMapSize, map.size()) System.setErr(System.err) val stderr = stream.toString() Assertions.assertTrue(stderr.isEmpty()) { "stderr should be empty, but got the following:${System.lineSeparator()}$stderr" } } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/BracesRuleFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.ruleset.rules.chapter3.BracesInConditionalsAndLoopsRule import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class BracesRuleFixTest : FixTestBase("test/paragraph3/braces", ::BracesInConditionalsAndLoopsRule) { @Test @Tag(WarningNames.NO_BRACES_IN_CONDITIONALS_AND_LOOPS) fun `should add braces to if-else statements - 1`() { fixAndCompare("IfElseBraces1Expected.kt", "IfElseBraces1Test.kt") } @Test @Tag(WarningNames.NO_BRACES_IN_CONDITIONALS_AND_LOOPS) fun `should add braces to loops with single statement`() { fixAndCompare("LoopsBracesExpected.kt", "LoopsBracesTest.kt") } @Test @Tag(WarningNames.NO_BRACES_IN_CONDITIONALS_AND_LOOPS) fun `should add braces to if-else statements inside scope functions`() { fixAndCompare("IfElseBracesInsideScopeFunctionsExpected.kt", "IfElseBracesInsideScopeFunctionsTest.kt") } @Test @Tag(WarningNames.NO_BRACES_IN_CONDITIONALS_AND_LOOPS) fun `should add braces to loops inside scope functions`() { fixAndCompare("LoopsBracesInsideScopeFunctionsExpected.kt", "LoopsBracesInsideScopeFunctionsTest.kt") } @Test @Tag(WarningNames.NO_BRACES_IN_CONDITIONALS_AND_LOOPS) fun `should add braces to do-while loops with empty body`() { fixAndCompare("DoWhileBracesExpected.kt", "DoWhileBracesTest.kt") } @Test @Tag(WarningNames.NO_BRACES_IN_CONDITIONALS_AND_LOOPS) fun `should remove braces from single-line when branches`() { fixAndCompare("WhenBranchesExpected.kt", "WhenBranchesTest.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/BracesRuleWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings.NO_BRACES_IN_CONDITIONALS_AND_LOOPS import com.saveourtool.diktat.ruleset.rules.chapter3.BracesInConditionalsAndLoopsRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class BracesRuleWarnTest : LintTestBase(::BracesInConditionalsAndLoopsRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${BracesInConditionalsAndLoopsRule.NAME_ID}" @Test @Tag(WarningNames.NO_BRACES_IN_CONDITIONALS_AND_LOOPS) fun `should check braces in if statement without else branch - positive example`() { lintMethod( """ |fun foo() { | if (x > 0) { | bar() | } |} """.trimMargin() ) } @Test @Tag(WarningNames.NO_BRACES_IN_CONDITIONALS_AND_LOOPS) fun `should check braces in if statement without else branch`() { lintMethod( """ |fun foo() { | if (x > 0) | bar() | | if (y < 0) baz() |} """.trimMargin(), DiktatError(2, 5, ruleId, "${NO_BRACES_IN_CONDITIONALS_AND_LOOPS.warnText()} IF", true), DiktatError(5, 5, ruleId, "${NO_BRACES_IN_CONDITIONALS_AND_LOOPS.warnText()} IF", true) ) } @Test @Tag(WarningNames.NO_BRACES_IN_CONDITIONALS_AND_LOOPS) fun `should check braces in if statements - positive example`() { lintMethod( """ |fun foo() { | if (x > 0) { | bar() | } else if (x == 0) { | bzz() | } else { | baz() | } |} """.trimMargin() ) } @Test @Tag(WarningNames.NO_BRACES_IN_CONDITIONALS_AND_LOOPS) fun `should check braces in if statements`() { lintMethod( """ |fun foo() { | if (x > 0) | bar() | else if (x == 0) | bzz() | else | baz() |} """.trimMargin(), DiktatError(2, 5, ruleId, "${NO_BRACES_IN_CONDITIONALS_AND_LOOPS.warnText()} IF", true), DiktatError(4, 10, ruleId, "${NO_BRACES_IN_CONDITIONALS_AND_LOOPS.warnText()} IF", true), DiktatError(6, 5, ruleId, "${NO_BRACES_IN_CONDITIONALS_AND_LOOPS.warnText()} ELSE", true) ) } @Test @Tag(WarningNames.NO_BRACES_IN_CONDITIONALS_AND_LOOPS) fun `should check braces in if statements - exception for single line if`() { lintMethod( """ |fun foo() { | if (x > 0) bar() else baz() |} """.trimMargin() ) } @Test @Tag(WarningNames.NO_BRACES_IN_CONDITIONALS_AND_LOOPS) fun `should check braces in if statements - exception for let`() { lintMethod( """ |fun foo() { | if (a) { | bar() | } else b?.let { | baz() | } | ?: run { | qux() | } |} """.trimMargin() ) } @Test @Tag(WarningNames.NO_BRACES_IN_CONDITIONALS_AND_LOOPS) fun `should check braces in if statements - exception for let 2`() { lintMethod( """ |fun foo() { | if (a) { | bar() | } else b?.let { | baz() | } |} """.trimMargin() ) } @Test @Tag(WarningNames.NO_BRACES_IN_CONDITIONALS_AND_LOOPS) fun `should check braces in if statements - exception for run`() { lintMethod( """ |fun foo() { | if (a) { | bar() | } else b!!.run { | baz() | } |} """.trimMargin() ) } @Test @Tag(WarningNames.NO_BRACES_IN_CONDITIONALS_AND_LOOPS) fun `should check braces in if statements - exception for apply`() { lintMethod( """ |fun foo() { | if (a) { | bar() | } else b.apply { | baz() | } |} """.trimMargin() ) } @Test @Tag(WarningNames.NO_BRACES_IN_CONDITIONALS_AND_LOOPS) fun `should check braces in if statements, apply exists, but braces are needed`() { lintMethod( """ |fun foo() { | if (a) { | bar() | } else baz(b.apply { id = 5 }) |} """.trimMargin(), DiktatError(4, 7, ruleId, "${NO_BRACES_IN_CONDITIONALS_AND_LOOPS.warnText()} ELSE", true) ) } @Test @Tag(WarningNames.NO_BRACES_IN_CONDITIONALS_AND_LOOPS) fun `should check braces in if statements, apply exists, but braces are needed 2`() { lintMethod( """ |fun foo() { | if (a) { | bar() | } else | c.baz(b.apply {id = 5}) |} """.trimMargin(), DiktatError(4, 7, ruleId, "${NO_BRACES_IN_CONDITIONALS_AND_LOOPS.warnText()} ELSE", true) ) } @Test @Tag(WarningNames.NO_BRACES_IN_CONDITIONALS_AND_LOOPS) fun `should correctly detect single line if`() { lintMethod( """ |fun foo() { | if (x > 0) { | bar() | } else if (x == 0) bar() else baz() |} """.trimMargin(), DiktatError(4, 12, ruleId, "${NO_BRACES_IN_CONDITIONALS_AND_LOOPS.warnText()} IF", true), DiktatError(4, 30, ruleId, "${NO_BRACES_IN_CONDITIONALS_AND_LOOPS.warnText()} ELSE", true) ) } @Test @Tag(WarningNames.NO_BRACES_IN_CONDITIONALS_AND_LOOPS) fun `should check braces in if statements - empty body`() { lintMethod( """ |fun foo() { | if (x > 0) | else if (x == 0) | else foo() |} """.trimMargin(), DiktatError(2, 5, ruleId, "${NO_BRACES_IN_CONDITIONALS_AND_LOOPS.warnText()} IF", true), DiktatError(3, 10, ruleId, "${NO_BRACES_IN_CONDITIONALS_AND_LOOPS.warnText()} IF", true), DiktatError(4, 5, ruleId, "${NO_BRACES_IN_CONDITIONALS_AND_LOOPS.warnText()} ELSE", true) ) } @Test @Tag(WarningNames.NO_BRACES_IN_CONDITIONALS_AND_LOOPS) fun `should warn if single line branches in when are used with braces`() { lintMethod( """ |fun foo() { | when (x) { | OPTION_1 -> { | foo() | println() | } | OPTION_2 -> { bar() } | OPTION_3 -> { | baz() | } | OPTION_4 -> { | // description | bzz() | } | } |} """.trimMargin(), DiktatError(7, 21, ruleId, "${NO_BRACES_IN_CONDITIONALS_AND_LOOPS.warnText()} WHEN", true), DiktatError(8, 21, ruleId, "${NO_BRACES_IN_CONDITIONALS_AND_LOOPS.warnText()} WHEN", true) ) } @Test @Tag(WarningNames.NO_BRACES_IN_CONDITIONALS_AND_LOOPS) fun `for loops should have braces - positive example`() { lintMethod( """ |fun foo() { | for (i in 1..100) { | println(i) | } |} """.trimMargin() ) } @Test @Tag(WarningNames.NO_BRACES_IN_CONDITIONALS_AND_LOOPS) fun `for loops should have braces`() { lintMethod( """ |fun foo() { | for (i in 1..100) | println(i) |} """.trimMargin(), DiktatError(2, 5, ruleId, "${NO_BRACES_IN_CONDITIONALS_AND_LOOPS.warnText()} FOR", true) ) } @Test @Tag(WarningNames.NO_BRACES_IN_CONDITIONALS_AND_LOOPS) fun `while loops should have braces - positive example`() { lintMethod( """ |fun foo() { | while (condition) { | println("") | } |} """.trimMargin() ) } @Test @Tag(WarningNames.NO_BRACES_IN_CONDITIONALS_AND_LOOPS) fun `while loops should have braces`() { lintMethod( """ |fun foo() { | while (condition) | println("") |} """.trimMargin(), DiktatError(2, 5, ruleId, "${NO_BRACES_IN_CONDITIONALS_AND_LOOPS.warnText()} WHILE", true) ) } @Test @Tag(WarningNames.NO_BRACES_IN_CONDITIONALS_AND_LOOPS) fun `do-while loops should have braces - positive example`() { lintMethod( """ |fun foo() { | do { | println("") | } while (condition) |} """.trimMargin() ) } @Test @Tag(WarningNames.NO_BRACES_IN_CONDITIONALS_AND_LOOPS) fun `do-while loops should have braces`() { lintMethod( """ |fun foo() { | do println(i) while (condition) |} """.trimMargin(), DiktatError(2, 5, ruleId, "${NO_BRACES_IN_CONDITIONALS_AND_LOOPS.warnText()} DO_WHILE", true) ) } @Test @Tag(WarningNames.NO_BRACES_IN_CONDITIONALS_AND_LOOPS) fun `do-while loops should have braces - empty body`() { lintMethod( """ |fun foo() { | do while (condition) |} """.trimMargin(), DiktatError(2, 5, ruleId, "${NO_BRACES_IN_CONDITIONALS_AND_LOOPS.warnText()} DO_WHILE", true) ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/ClassLikeStructuresOrderFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.ruleset.rules.chapter3.ClassLikeStructuresOrderRule import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tags import org.junit.jupiter.api.Test class ClassLikeStructuresOrderFixTest : FixTestBase("test/paragraph3/file_structure", ::ClassLikeStructuresOrderRule) { @Test @Tags(Tag(WarningNames.BLANK_LINE_BETWEEN_PROPERTIES), Tag(WarningNames.WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES)) fun `should fix order and newlines between properties`() { fixAndCompare("DeclarationsInClassOrderExpected.kt", "DeclarationsInClassOrderTest.kt") } @Test @Tags(Tag(WarningNames.BLANK_LINE_BETWEEN_PROPERTIES), Tag(WarningNames.WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES)) fun `should fix order and newlines with comment`() { fixAndCompare("OrderWithCommentExpected.kt", "OrderWithCommentTest.kt") } @Test @Tags(Tag(WarningNames.WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES)) fun `regression - should not remove enum members`() { fixAndCompare("OrderWithEnumsExpected.kt", "OrderWithEnumsTest.kt") } @Test @Tags(Tag(WarningNames.WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES)) fun `regression - should not move loggers that depend on other variables from scope`() { fixAndCompare("LoggerOrderExpected.kt", "LoggerOrderTest.kt") } @Test @Tag(WarningNames.BLANK_LINE_BETWEEN_PROPERTIES) fun `should add new line before the comment`() { fixAndCompare("CompanionObjectWithCommentExpected.kt", "CompanionObjectWithCommentTest.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/ClassLikeStructuresOrderRuleWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings.BLANK_LINE_BETWEEN_PROPERTIES import com.saveourtool.diktat.ruleset.constants.Warnings.WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES import com.saveourtool.diktat.ruleset.rules.chapter3.ClassLikeStructuresOrderRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import com.saveourtool.diktat.test.framework.util.describe import generated.WarningNames import org.assertj.core.api.Assertions.assertThat import org.intellij.lang.annotations.Language import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class ClassLikeStructuresOrderRuleWarnTest : LintTestBase(::ClassLikeStructuresOrderRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${ClassLikeStructuresOrderRule.NAME_ID}" // ===== order of declarations ===== @Language("kotlin") private fun codeTemplate(keyword: String) = """ |$keyword Example { | private val log = LoggerFactory.getLogger(Example.javaClass) | private val FOO = 42 | private lateinit var lateFoo: Int | init { | bar() | } | constructor(baz: Int) | fun foo() { | val nested = Nested() | } | class Nested { | val nestedFoo = 43 | } | companion object { | private const val ZERO = 0 | private var i = 0 | } | class UnusedNested { } |} """.trimMargin() @Test @Tag(WarningNames.WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES) fun `should check order of declarations in classes - positive example`() { listOf("class", "interface", "object").forEach { keyword -> lintMethod(codeTemplate(keyword)) } } @Test @Tag(WarningNames.WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES) fun `should warn if loggers are not on top`() { lintMethod( """ |class Example { | private val FOO = 42 | private val log = LoggerFactory.getLogger(Example.javaClass) |} """.trimMargin(), DiktatError(2, 5, ruleId, "${WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES.warnText()} PROPERTY: FOO", true), DiktatError(3, 5, ruleId, "${WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES.warnText()} PROPERTY: log", true) ) } // ===== comments on properties ====== @Test @Tag(WarningNames.BLANK_LINE_BETWEEN_PROPERTIES) fun `comments and KDocs on properties should be prepended by newline - positive example`() { lintMethod( """ |class Example { | // logger property | private val log = LoggerFactory.getLogger(Example.javaClass) | private val FOO = 42 | | // another property | private val BAR = 43 | | @Annotated | private val qux = 43 | | // annotated property | @Annotated | private val quux = 43 | | /** | * Yet another property. | */ | private val BAZ = 44 | @Annotated private lateinit var lateFoo: Int |} """.trimMargin()) } @Test @Tag(WarningNames.BLANK_LINE_BETWEEN_PROPERTIES) fun `should warn if comments and KDocs on properties are not prepended by newline`() { lintMethod( """ |class Example { | private val log = LoggerFactory.getLogger(Example.javaClass) | private val FOO = 42 | // another property | private val BAR = 43 | @Anno | private val qux = 43 | // annotated property | @Anno | private val quux = 43 | /** | * Yet another property. | */ | private val BAZ = 44 | private lateinit var lateFoo: Int |} """.trimMargin(), DiktatError(4, 5, ruleId, "${BLANK_LINE_BETWEEN_PROPERTIES.warnText()} BAR", true), DiktatError(6, 5, ruleId, "${BLANK_LINE_BETWEEN_PROPERTIES.warnText()} qux", true), DiktatError(8, 5, ruleId, "${BLANK_LINE_BETWEEN_PROPERTIES.warnText()} quux", true), DiktatError(11, 5, ruleId, "${BLANK_LINE_BETWEEN_PROPERTIES.warnText()} BAZ", true) ) } @Test @Tag(WarningNames.BLANK_LINE_BETWEEN_PROPERTIES) fun `regression - should check only class-level and top-level properties`() { lintMethod( """class Example { | fun foo() { | val bar = 0 | | val baz = 1 | } | | class Nested { | val bar = 0 | val baz = 1 | } |} """.trimMargin() ) } @Test @Tag(WarningNames.BLANK_LINE_BETWEEN_PROPERTIES) fun `a single-line comment after annotation`() { lintMethod( """class Example { | private val val0 = Regex(""${'"'}\d+""${'"'}) | | @Deprecated("Deprecation message") // Trailing comment | private val val2 = "" |} """.trimMargin() ) } @Test @Tag(WarningNames.BLANK_LINE_BETWEEN_PROPERTIES) fun `should allow blank lines around properties with custom getters and setters - positive example`() { lintMethod( """ |class Example { | private val foo | get() = 0 | | private var backing = 0 | | var bar | get() = backing | set(value) { backing = value } |} """.trimMargin() ) } @Test @Tag(WarningNames.BLANK_LINE_BETWEEN_PROPERTIES) fun `should allow blank lines around properties with custom getters and setters - positive example without blank lines`() { lintMethod( """ |class Example { | private val foo | get() = 0 | private var backing = 0 | var bar | get() = backing | set(value) { backing = value } |} """.trimMargin() ) } @Test @Tag(WarningNames.BLANK_LINE_BETWEEN_PROPERTIES) fun `should not trigger on EOL comment on the same line with property`() { lintMethod( """ |class ActiveBinsMetric(meterRegistry: MeterRegistry, private val binRepository: BinRepository) { | companion object { | private const val EGG_7_9_BUCKET_LABEL = "7-9" | private const val DELAY = 15000L // 15s | } |} """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES) fun `should warn if order is incorrect and property with comment`() { lintMethod( """ class Example { companion object { val b = "q" // this private const val a = 3 } } """.trimMargin(), DiktatError(3, 29, ruleId, "${WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES.warnText()} PROPERTY: b", true), DiktatError(5, 29, ruleId, "${WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES.warnText()} PROPERTY: a", true) ) } @Test @Tag(WarningNames.WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES) fun `should correctly check class elements order in enum classes`() { lintMethod( """ enum class Enum { FOO, BAR, ; fun f() {} companion object } """.trimMargin() ) } /** * An exception to the "loggers on top" rule. * * See [#1516](https://github.com/saveourtool/diktat/issues/1516). */ @Test @Tag(WarningNames.WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES) fun `logger-like const property should not be reordered`() { val code = """ |object C { | private const val A = "value" | | // Not a logger | private const val LOG = "value" |} """.trimMargin() val actualErrors = lintResult(code) assertThat(actualErrors) .describedAs("lint result for ${code.describe()}") .isEmpty() } /** * An exception to the "loggers on top" rule. * * See [#1516](https://github.com/saveourtool/diktat/issues/1516). */ @Test @Tag(WarningNames.WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES) fun `logger-like lateinit property should not be reordered`() { val code = """ |object C { | private lateinit val A | private lateinit val LOG // Not a logger |} """.trimMargin() val actualErrors = lintResult(code) assertThat(actualErrors) .describedAs("lint result for ${code.describe()}") .isEmpty() } /** * An exception to the "loggers on top" rule. * * See [#1516](https://github.com/saveourtool/diktat/issues/1516). */ @Test @Tag(WarningNames.WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES) fun `property with a name containing 'log' is not a logger`() { val code = """ |object C { | private val a = System.getProperty("os.name") | private val loginName = LoggerFactory.getLogger({}.javaClass) |} """.trimMargin() val actualErrors = lintResult(code) assertThat(actualErrors) .describedAs("lint result for ${code.describe()}") .isEmpty() } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/CollapseIfStatementsRuleFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter3.CollapseIfStatementsRule import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class CollapseIfStatementsRuleFixTest : FixTestBase( "test/paragraph3/collapse_if", ::CollapseIfStatementsRule, listOf( RulesConfig(Warnings.COLLAPSE_IF_STATEMENTS.name, true, emptyMap()) ) ) { @Test @Tag(WarningNames.COLLAPSE_IF_STATEMENTS) fun `collapse if`() { fixAndCompare("CollapseIfStatementsExpected.kt", "CollapseIfStatementsTest.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/CollapseIfStatementsRuleWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings.COLLAPSE_IF_STATEMENTS import com.saveourtool.diktat.ruleset.rules.chapter3.CollapseIfStatementsRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class CollapseIfStatementsRuleWarnTest : LintTestBase(::CollapseIfStatementsRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${CollapseIfStatementsRule.NAME_ID}" @Test @Tag(WarningNames.COLLAPSE_IF_STATEMENTS) fun `one nested if`() { lintMethod( """ |fun foo () { | if (true) { | if (false) { | doSomething() | } | } | | if (false) { | someAction() | } else { | print("some text") | } |} """.trimMargin(), DiktatError(3, 10, ruleId, "${COLLAPSE_IF_STATEMENTS.warnText()} avoid using redundant nested if-statements", true) ) } @Test @Tag(WarningNames.COLLAPSE_IF_STATEMENTS) fun `simple check`() { lintMethod( """ |fun foo () { | if (cond1) { | if (cond2) { | firstAction() | secondAction() | } | } | if (cond3) { | secondAction() | } |} """.trimMargin(), DiktatError(3, 10, ruleId, "${COLLAPSE_IF_STATEMENTS.warnText()} avoid using redundant nested if-statements", true) ) } @Test @Tag(WarningNames.COLLAPSE_IF_STATEMENTS) fun `simple check 2`() { lintMethod( """ |fun foo () { | if (true) { | | if (true) { | doSomething() | } | } |} """.trimMargin(), DiktatError(4, 10, ruleId, "${COLLAPSE_IF_STATEMENTS.warnText()} avoid using redundant nested if-statements", true) ) } @Test @Tag(WarningNames.COLLAPSE_IF_STATEMENTS) fun `nested if with incorrect indention`() { lintMethod( """ |fun foo () { | if (true) { | if (true) {doSomething()} | } |} """.trimMargin(), DiktatError(3, 10, ruleId, "${COLLAPSE_IF_STATEMENTS.warnText()} avoid using redundant nested if-statements", true) ) } @Test @Tag(WarningNames.COLLAPSE_IF_STATEMENTS) fun `eol comment`() { lintMethod( """ |fun foo () { | if (true) { | // Some | // comments | if (true) { | doSomething() | } | } |} """.trimMargin(), DiktatError(5, 10, ruleId, "${COLLAPSE_IF_STATEMENTS.warnText()} avoid using redundant nested if-statements", true) ) } @Test @Tag(WarningNames.COLLAPSE_IF_STATEMENTS) fun `block comment`() { lintMethod( """ |fun foo () { | if (true) { | /* | Some comments | */ | if (true) { | doSomething() | } | } |} """.trimMargin(), DiktatError(6, 10, ruleId, "${COLLAPSE_IF_STATEMENTS.warnText()} avoid using redundant nested if-statements", true) ) } @Test @Tag(WarningNames.COLLAPSE_IF_STATEMENTS) fun `kdoc comment`() { lintMethod( """ |fun foo () { | if (true) { | /** | * Some comments | */ | if (true) { | doSomething() | } | } |} """.trimMargin() ) } @Test @Tag(WarningNames.COLLAPSE_IF_STATEMENTS) fun `combine comments`() { lintMethod( """ |fun foo () { | if (true) { | /* | Some Comments | */ | // More comments | if (true) { | // comment 1 | val a = 5 | // comment 2 | doSomething() | } | // comment 3 | } |} """.trimMargin(), DiktatError(7, 10, ruleId, "${COLLAPSE_IF_STATEMENTS.warnText()} avoid using redundant nested if-statements", true) ) } @Test @Tag(WarningNames.COLLAPSE_IF_STATEMENTS) fun `comments in compound cond`() { lintMethod( """ |fun foo() { | // comment | if (cond1) { | /* | Some comments | */ | // More comments | if (cond2 || cond3) { | doSomething() | } | } |} """.trimMargin(), DiktatError(8, 10, ruleId, "${COLLAPSE_IF_STATEMENTS.warnText()} avoid using redundant nested if-statements", true) ) } @Test @Tag(WarningNames.COLLAPSE_IF_STATEMENTS) fun `comments already in cond`() { lintMethod( """ |fun foo () { | if (/*comment*/ true) { | if (true) { | doSomething() | } | } |} """.trimMargin(), DiktatError(3, 10, ruleId, "${COLLAPSE_IF_STATEMENTS.warnText()} avoid using redundant nested if-statements", true) ) } @Test @Tag(WarningNames.COLLAPSE_IF_STATEMENTS) fun `comments already in complex cond`() { lintMethod( """ |fun foo () { | if (true && (true || false) /*comment*/) { | if (true /*comment*/) { | doSomething() | } | } |} """.trimMargin(), DiktatError(3, 10, ruleId, "${COLLAPSE_IF_STATEMENTS.warnText()} avoid using redundant nested if-statements", true) ) } @Test @Tag(WarningNames.COLLAPSE_IF_STATEMENTS) fun `multiline comments already in cond`() { lintMethod( """ |fun foo () { | if (true | /*comment | * more comments | */ | ) { | if (true /*comment 2*/) { | doSomething() | } | } |} """.trimMargin(), DiktatError(7, 10, ruleId, "${COLLAPSE_IF_STATEMENTS.warnText()} avoid using redundant nested if-statements", true) ) } @Test @Tag(WarningNames.COLLAPSE_IF_STATEMENTS) fun `comments in multiple if-statements`() { lintMethod( """ |fun foo() { | if (cond1) { | // comment | if (cond2) { | // comment 2 | if (cond3) { | doSomething() | } | } | } |} """.trimMargin(), DiktatError(4, 10, ruleId, "${COLLAPSE_IF_STATEMENTS.warnText()} avoid using redundant nested if-statements", true), DiktatError(6, 14, ruleId, "${COLLAPSE_IF_STATEMENTS.warnText()} avoid using redundant nested if-statements", true) ) } @Test @Tag(WarningNames.COLLAPSE_IF_STATEMENTS) fun `not nested if`() { lintMethod( """ |fun foo () { | if (true) { | val someConstant = 5 | if (true) { | doSomething() | } | } |} """.trimMargin() ) } @Test @Tag(WarningNames.COLLAPSE_IF_STATEMENTS) fun `internal if with else branch`() { lintMethod( """ |fun foo () { | if (cond1) { | if (cond2) { | firstAction() | secondAction() | } else { | firstAction() | } | } else { | secondAction() | } |} """.trimMargin(), ) } @Test @Tag(WarningNames.COLLAPSE_IF_STATEMENTS) fun `internal if with multiple else and elif branches`() { lintMethod( """ |fun foo () { | if (cond1) { | if (cond2) { | firstAction() | secondAction() | } else if (cond3) { | firstAction() | } else { | val a = 5 | } | } else { | secondAction() | } |} """.trimMargin() ) } @Test @Tag(WarningNames.COLLAPSE_IF_STATEMENTS) fun `if isn't last child`() { lintMethod( """ |fun foo() { | if (cond1) { | if (cond2) { | doSomething() | } | val a = 5 | } |} """.trimMargin() ) } @Test @Tag(WarningNames.COLLAPSE_IF_STATEMENTS) fun `external if with else branch`() { lintMethod( """ |fun foo() { | if (cond1) { | if (cond2) { | doSomething() | } | } else { | val b = 1 | } |} """.trimMargin() ) } @Test @Tag(WarningNames.COLLAPSE_IF_STATEMENTS) fun `external if with else node, but exist nested if`() { lintMethod( """ |fun foo() { | val a = 0 | if (cond1) { | if (cond2) { | if (cond3) { | doSomething() | } | } | } else { | val b = 1 | } |} """.trimMargin(), DiktatError(5, 12, ruleId, "${COLLAPSE_IF_STATEMENTS.warnText()} avoid using redundant nested if-statements", true), ) } @Test @Tag(WarningNames.COLLAPSE_IF_STATEMENTS) fun `three if statements`() { lintMethod( """ |fun foo () { | if (true) { | if (true) { | if (true) { | doSomething() | } | } | } |} """.trimMargin(), DiktatError(3, 10, ruleId, "${COLLAPSE_IF_STATEMENTS.warnText()} avoid using redundant nested if-statements", true), DiktatError(4, 14, ruleId, "${COLLAPSE_IF_STATEMENTS.warnText()} avoid using redundant nested if-statements", true) ) } @Test @Tag(WarningNames.COLLAPSE_IF_STATEMENTS) fun `many if statements`() { lintMethod( """ |fun foo () { | if (true) { | if (true) { | if (true) { | if (true) { | if (true) { | if (true) { | doSomething() | } | } | } | } | } | } |} """.trimMargin(), DiktatError(3, 10, ruleId, "${COLLAPSE_IF_STATEMENTS.warnText()} avoid using redundant nested if-statements", true), DiktatError(4, 14, ruleId, "${COLLAPSE_IF_STATEMENTS.warnText()} avoid using redundant nested if-statements", true), DiktatError(5, 18, ruleId, "${COLLAPSE_IF_STATEMENTS.warnText()} avoid using redundant nested if-statements", true), DiktatError(6, 22, ruleId, "${COLLAPSE_IF_STATEMENTS.warnText()} avoid using redundant nested if-statements", true), DiktatError(7, 26, ruleId, "${COLLAPSE_IF_STATEMENTS.warnText()} avoid using redundant nested if-statements", true) ) } @Test @Tag(WarningNames.COLLAPSE_IF_STATEMENTS) fun `not nested else if statement`() { lintMethod( """ |fun foo() { | fun1() | if (cond1) { | fun2() | } else if (cond2) { | if (true) { | fun3() | } | } else { | fun4() | } |} """.trimMargin() ) } @Test @Tag(WarningNames.COLLAPSE_IF_STATEMENTS) fun `nested else if statement`() { lintMethod( """ |fun foo() { | fun1() | if (cond1) { | fun2() | } else if (cond2) { | if (true) { | if (true) { | fun3() | } | } | } else { | fun4() | } |} """.trimMargin(), DiktatError(7, 12, ruleId, "${COLLAPSE_IF_STATEMENTS.warnText()} avoid using redundant nested if-statements", true), ) } @Test @Tag(WarningNames.COLLAPSE_IF_STATEMENTS) fun `compound condition`() { lintMethod( """ |fun foo () { | if (cond1) { | if (cond2 || cond3) { | firstAction() | secondAction() | } | } | if (cond4) { | secondAction() | } |} """.trimMargin(), DiktatError(3, 10, ruleId, "${COLLAPSE_IF_STATEMENTS.warnText()} avoid using redundant nested if-statements", true), ) } @Test @Tag(WarningNames.COLLAPSE_IF_STATEMENTS) fun `not nested compound condition`() { lintMethod( """ |fun foo () { | if (cond1) { | if (cond2 || cond3) { | firstAction() | secondAction() | } | if (cond4) { | secondAction() | } | } |} """.trimMargin() ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/ConsecutiveSpacesRuleFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter3.ConsecutiveSpacesRule import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class ConsecutiveSpacesRuleFixTest : FixTestBase("test/paragraph3/spaces", ::ConsecutiveSpacesRule, listOf( RulesConfig(Warnings.TOO_MANY_CONSECUTIVE_SPACES.name, true, mapOf( "maxSpaces" to "1", "saveInitialFormattingForEnums" to "true" ) ) ) ) { @Test @Tag(WarningNames.TOO_MANY_CONSECUTIVE_SPACES) fun `many spaces rule enum case`() { fixAndCompare("TooManySpacesEnumExpected.kt", "TooManySpacesEnumTest.kt") } @Test @Tag(WarningNames.TOO_MANY_CONSECUTIVE_SPACES) fun `many spaces rule`() { fixAndCompare("TooManySpacesExpected.kt", "TooManySpacesTest.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/ConsecutiveSpacesRuleWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.TOO_MANY_CONSECUTIVE_SPACES import com.saveourtool.diktat.ruleset.rules.chapter3.ConsecutiveSpacesRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class ConsecutiveSpacesRuleWarnTest : LintTestBase(::ConsecutiveSpacesRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${ConsecutiveSpacesRule.NAME_ID}" private val rulesConfigListNoSpaces: List = listOf( RulesConfig(TOO_MANY_CONSECUTIVE_SPACES.name, true, mapOf("maxSpaces" to "2")) ) @Test @Tag(WarningNames.TOO_MANY_CONSECUTIVE_SPACES) fun `enum spaces check bad`() { lintMethod( """ |enum class IntArithmetics : BinaryOperator { | PLUS, ASD |} """.trimMargin(), DiktatError(1, 5, ruleId, "${TOO_MANY_CONSECUTIVE_SPACES.warnText()} found: 7. need to be: 1", true) ) } @Test @Tag(WarningNames.TOO_MANY_CONSECUTIVE_SPACES) fun `enum spaces check good`() { lintMethod( """ |enum class SomeEnum { | PLUS |} """.trimMargin() ) } @Test @Tag(WarningNames.TOO_MANY_CONSECUTIVE_SPACES) fun `fun space check good`() { lintMethod( """ |class A { | fun testFunction(val a = 5) { | } |} """.trimMargin() ) } @Test @Tag(WarningNames.TOO_MANY_CONSECUTIVE_SPACES) fun `fun space check bad`() { lintMethod( """ |class A { | fun testFunction(val a = 6) { | } |} """.trimMargin(), DiktatError(2, 7, ruleId, "${TOO_MANY_CONSECUTIVE_SPACES.warnText()} found: 6. need to be: 1", true), DiktatError(2, 33, ruleId, "${TOO_MANY_CONSECUTIVE_SPACES.warnText()} found: 5. need to be: 1", true) ) } @Test @Tag(WarningNames.TOO_MANY_CONSECUTIVE_SPACES) fun `class space check bad`() { lintMethod( """ |class SomeClass { | inner class InnerClass{ | } |} """.trimMargin(), DiktatError(1, 6, ruleId, "${TOO_MANY_CONSECUTIVE_SPACES.warnText()} found: 5. need to be: 1", true), DiktatError(2, 9, ruleId, "${TOO_MANY_CONSECUTIVE_SPACES.warnText()} found: 5. need to be: 1", true) ) } @Test @Tag(WarningNames.TOO_MANY_CONSECUTIVE_SPACES) fun `class space check good`() { lintMethod( """ |class SomeClass { | inner class InnerClass{ | } |} """.trimMargin() ) } @Test @Tag(WarningNames.TOO_MANY_CONSECUTIVE_SPACES) fun `property space check good`() { lintMethod( """ |class SomeClass { | fun someFunc() { | val a = 5 | val b: Int = 3 | val c: Int | } |} """.trimMargin() ) } @Test @Tag(WarningNames.TOO_MANY_CONSECUTIVE_SPACES) fun `property space check bad`() { lintMethod( """ |class SomeClass { | fun someFunc() { | val a = 5 | val b: Int = 3 | val c: Int | } |} """.trimMargin(), DiktatError(3, 15, ruleId, "${TOO_MANY_CONSECUTIVE_SPACES.warnText()} found: 4. need to be: 1", true), DiktatError(4, 18, ruleId, "${TOO_MANY_CONSECUTIVE_SPACES.warnText()} found: 5. need to be: 1", true), DiktatError(5, 14, ruleId, "${TOO_MANY_CONSECUTIVE_SPACES.warnText()} found: 5. need to be: 1", true) ) } @Test @Tag(WarningNames.TOO_MANY_CONSECUTIVE_SPACES) fun `generic space check good`() { lintMethod( """ |class Box(t: T){ | var value = t |} """.trimMargin() ) } @Test @Tag(WarningNames.TOO_MANY_CONSECUTIVE_SPACES) fun `generic space check bad`() { lintMethod( """ |class Box< T>(t: T){ | var value = t |} """.trimMargin(), DiktatError(1, 11, ruleId, "${TOO_MANY_CONSECUTIVE_SPACES.warnText()} found: 3. need to be: 1", true), DiktatError(1, 19, ruleId, "${TOO_MANY_CONSECUTIVE_SPACES.warnText()} found: 4. need to be: 1", true) ) } @Test @Tag(WarningNames.TOO_MANY_CONSECUTIVE_SPACES) fun `interface space check good`() { lintMethod( """ |interface TestInterface{ | fun foo() | fun bar() |} """.trimMargin() ) } @Test @Tag(WarningNames.TOO_MANY_CONSECUTIVE_SPACES) fun `interface space check bad`() { lintMethod( """ |interface TestInterface{ | fun foo() | fun bar() |} """.trimMargin(), DiktatError(1, 10, ruleId, "${TOO_MANY_CONSECUTIVE_SPACES.warnText()} found: 7. need to be: 1", true) ) } @Test @Tag(WarningNames.TOO_MANY_CONSECUTIVE_SPACES) fun `init space check bad`() { lintMethod( """ |class SomeClass{ | init { | print("SomeThing") | } |} """.trimMargin(), DiktatError(2, 8, ruleId, "${TOO_MANY_CONSECUTIVE_SPACES.warnText()} found: 5. need to be: 1", true) ) } @Test @Tag(WarningNames.TOO_MANY_CONSECUTIVE_SPACES) fun `init space check good`() { lintMethod( """ |class SomeClass{ | init { | print("SomeThing") | } |} """.trimMargin() ) } @Test @Tag(WarningNames.TOO_MANY_CONSECUTIVE_SPACES) fun `config check`() { lintMethod( """ |class SomeClass { | init { | print("SomeThing") | } |} """.trimMargin(), rulesConfigList = rulesConfigListNoSpaces ) } @Test @Tag(WarningNames.TOO_MANY_CONSECUTIVE_SPACES) fun `eol comment check`() { lintMethod( """ |class SomeClass{ // this is a comment | val a = 5 // this is another comment |} """.trimMargin() ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/DebugPrintRuleWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter3.DebugPrintRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class DebugPrintRuleWarnTest : LintTestBase(::DebugPrintRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${DebugPrintRule.NAME_ID}" @Test @Tag(WarningNames.DEBUG_PRINT) fun `call of print`() { lintMethod( """ |fun test() { | print("test print") |} """.trimMargin(), DiktatError(2, 5, ruleId, "${Warnings.DEBUG_PRINT.warnText()} found print()", false) ) } @Test @Tag(WarningNames.DEBUG_PRINT) fun `call of println`() { lintMethod( """ |fun test() { | println("test println") |} """.trimMargin(), DiktatError(2, 5, ruleId, "${Warnings.DEBUG_PRINT.warnText()} found println()", false) ) } @Test @Tag(WarningNames.DEBUG_PRINT) fun `call of println without arguments`() { lintMethod( """ |fun test() { | println() |} """.trimMargin(), DiktatError(2, 5, ruleId, "${Warnings.DEBUG_PRINT.warnText()} found println()", false) ) } @Test @Tag(WarningNames.DEBUG_PRINT) fun `custom method print by argument list`() { lintMethod( """ |fun test() { | print("test custom args", 123) |} """.trimMargin() ) } @Test @Tag(WarningNames.DEBUG_PRINT) fun `custom method print with lambda as last parameter`() { lintMethod( """ |fun test() { | print("test custom method") { | foo("") | } |} """.trimMargin() ) } @Test @Tag(WarningNames.DEBUG_PRINT) fun `custom method print in another object`() { lintMethod( """ |fun test() { | foo.print("test custom method") |} """.trimMargin() ) } @Test @Tag(WarningNames.DEBUG_PRINT) fun `call of console`() { lintMethod( """ |fun test() { | console.error("1") | console.info("1", "2") | console.log("1", "2", "3") | console.warn("1", "2", "3", "4") |} """.trimMargin(), DiktatError(2, 5, ruleId, "${Warnings.DEBUG_PRINT.warnText()} found console.error()", false), DiktatError(3, 5, ruleId, "${Warnings.DEBUG_PRINT.warnText()} found console.info()", false), DiktatError(4, 5, ruleId, "${Warnings.DEBUG_PRINT.warnText()} found console.log()", false), DiktatError(5, 5, ruleId, "${Warnings.DEBUG_PRINT.warnText()} found console.warn()", false) ) } @Test @Tag(WarningNames.DEBUG_PRINT) fun `custom method console with lambda as last parameter`() { lintMethod( """ |fun test() { | console.error("1") { | foo("") | } | console.info("1", "2") { | foo("") | } | console.log("1", "2", "3") { | foo("") | } | console.warn("1", "2", "3", "4") { | foo("") | } |} """.trimMargin() ) } @Test @Tag(WarningNames.DEBUG_PRINT) fun `call parameter from console`() { lintMethod( """ |fun test() { | val foo = console.size |} """.trimMargin() ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/EmptyBlockFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter3.EmptyBlock import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class EmptyBlockFixTest : FixTestBase("test/paragraph3/empty_block", ::EmptyBlock) { private val rulesConfigListEmptyBlockExist: List = listOf( RulesConfig(Warnings.EMPTY_BLOCK_STRUCTURE_ERROR.name, true, mapOf("allowEmptyBlocks" to "True")) ) @Test @Tag(WarningNames.EMPTY_BLOCK_STRUCTURE_ERROR) fun `should fix open and close brace in different expression`() { fixAndCompare("EmptyBlockExpected.kt", "EmptyBlockTest.kt", rulesConfigListEmptyBlockExist) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/EmptyBlockWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.EMPTY_BLOCK_STRUCTURE_ERROR import com.saveourtool.diktat.ruleset.rules.chapter3.EmptyBlock import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class EmptyBlockWarnTest : LintTestBase(::EmptyBlock) { private val ruleId = "$DIKTAT_RULE_SET_ID:${EmptyBlock.NAME_ID}" private val rulesConfigListIgnoreEmptyBlock: List = listOf( RulesConfig(EMPTY_BLOCK_STRUCTURE_ERROR.name, true, mapOf("styleEmptyBlockWithNewline" to "False")) ) private val rulesConfigListEmptyBlockExist: List = listOf( RulesConfig(EMPTY_BLOCK_STRUCTURE_ERROR.name, true, mapOf("allowEmptyBlocks" to "True")) ) @Test @Tag(WarningNames.EMPTY_BLOCK_STRUCTURE_ERROR) fun `check if expression with empty else block`() { lintMethod( """ |fun foo() { | if (x < -5) { | goo() | } | else { | } |} """.trimMargin(), DiktatError(5, 10, ruleId, "${EMPTY_BLOCK_STRUCTURE_ERROR.warnText()} empty blocks are forbidden unless it is function with override keyword", false) ) } @Test @Tag(WarningNames.EMPTY_BLOCK_STRUCTURE_ERROR) fun `check if WHEN element node text is empty`() { lintMethod( """ |fun foo() { | when (a) { | } |} """.trimMargin(), DiktatError(2, 5, ruleId, "${EMPTY_BLOCK_STRUCTURE_ERROR.warnText()} empty blocks are forbidden unless it is function with override keyword", false) ) } @Test @Tag(WarningNames.EMPTY_BLOCK_STRUCTURE_ERROR) fun `check if expression with empty else block with config`() { lintMethod( """ |fun foo() { | if (x < -5) { | goo() | } | else {} |} """.trimMargin(), DiktatError(5, 10, ruleId, "${EMPTY_BLOCK_STRUCTURE_ERROR.warnText()} empty blocks are forbidden unless it is function with override keyword", false), rulesConfigList = rulesConfigListIgnoreEmptyBlock ) } @Test @Tag(WarningNames.EMPTY_BLOCK_STRUCTURE_ERROR) fun `check fun expression with empty block and override annotation`() { lintMethod( """ |override fun foo() { |} """.trimMargin() ) } @Test @Tag(WarningNames.EMPTY_BLOCK_STRUCTURE_ERROR) fun `check if expression with empty else block but with permission to use empty block`() { lintMethod( """ |fun foo() { | if (x < -5) { | goo() | } | else { | } |} """.trimMargin(), rulesConfigList = rulesConfigListEmptyBlockExist ) } @Test fun `check if expression without block`() { lintMethod( """ |fun foo() { | if (node.treeParent != null) return |} """.trimMargin() ) } @Test fun `empty lambda`() { lintMethod( """ |fun foo() { | run { } |} """.trimMargin(), rulesConfigList = rulesConfigListEmptyBlockExist ) } @Test fun `check if-else expression without block`() { lintMethod( """ |fun foo() { | if (node.treeParent != null) return else println(true) |} """.trimMargin() ) } @Test fun `check for expresion and while without block`() { lintMethod( """ |fun foo() { | for(x in 0..10) println(x) | val x = 10 | while (x > 0) | --x |} """.trimMargin() ) } @Test @Tag(WarningNames.EMPTY_BLOCK_STRUCTURE_ERROR) fun `check empty lambda with config`() { lintMethod( """ |fun foo() { | val y = listOf().map { | | } |} """.trimMargin(), DiktatError(2, 30, ruleId, "${EMPTY_BLOCK_STRUCTURE_ERROR.warnText()} do not put newlines in empty lambda", true), rulesConfigList = rulesConfigListEmptyBlockExist ) } @Test fun `check empty lambda`() { lintMethod( """ |fun foo() { | val y = listOf().map { } |} """.trimMargin(), DiktatError(2, 30, ruleId, "${EMPTY_BLOCK_STRUCTURE_ERROR.warnText()} empty blocks are forbidden unless it is function with override keyword", false) ) } @Test fun `should not trigger on anonymous SAM classes #1`() { lintMethod( """ |fun foo() { | val proj = some.create( | Disposable {}, | config | ).project |} """.trimMargin(), rulesConfigList = rulesConfigListEmptyBlockExist ) } @Test fun `should not trigger on anonymous SAM classes #2`() { lintMethod( """ |fun foo() { | val some = Disposable {} | val proj = some.create( | some, | config | ).project |} """.trimMargin(), rulesConfigList = rulesConfigListEmptyBlockExist ) } @Test @Tag(WarningNames.EMPTY_BLOCK_STRUCTURE_ERROR) fun `should trigger on implementing anonymous SAM classes`() { lintMethod( """ |interface A | |val some = object : A{} """.trimMargin(), DiktatError(3, 22, ruleId, "${EMPTY_BLOCK_STRUCTURE_ERROR.warnText()} different style for empty block", true), rulesConfigList = rulesConfigListEmptyBlockExist ) } @Test @Tag(WarningNames.EMPTY_BLOCK_STRUCTURE_ERROR) fun `should not trigger on empty lambdas as a functions`() { lintMethod( """ |fun foo(bar: () -> Unit = {}) | |class Some { | fun bar() { | A({}) | } |} """.trimMargin(), rulesConfigList = rulesConfigListEmptyBlockExist ) } @Test @Tag(WarningNames.EMPTY_BLOCK_STRUCTURE_ERROR) fun `should not trigger on empty lambdas as a functions #2`() { lintMethod( """ |fun some() { | val project = KotlinCoreEnvironment.createForProduction( | { }, | compilerConfiguration, | EnvironmentConfigFiles.JVM_CONFIG_FILES | ).project |} """.trimMargin(), rulesConfigList = rulesConfigListEmptyBlockExist ) } @Test @Tag(WarningNames.EMPTY_BLOCK_STRUCTURE_ERROR) fun `should not trigger on KotlinLogging logger`() { lintMethod( """ |import io.github.oshai.kotlinlogging.KotlinLogging | |fun some() { | val log = KotlinLogging.logger {} | log.info { "test" } |} """.trimMargin(), ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/EnumsSeparatedFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.ruleset.rules.chapter3.EnumsSeparated import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class EnumsSeparatedFixTest : FixTestBase("test/paragraph3/enum_separated", ::EnumsSeparated) { @Test @Tag(WarningNames.ENUMS_SEPARATED) fun `test enums split`() { fixAndCompare("EnumSeparatedExpected.kt", "EnumSeparatedTest.kt") } @Test @Tag(WarningNames.ENUMS_SEPARATED) fun `last element with comment`() { fixAndCompare("LastElementCommentExpected.kt", "LastElementCommentTest.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/EnumsSeparatedWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings.ENUMS_SEPARATED import com.saveourtool.diktat.ruleset.rules.chapter3.EnumsSeparated import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class EnumsSeparatedWarnTest : LintTestBase(::EnumsSeparated) { private val ruleId = "$DIKTAT_RULE_SET_ID:${EnumsSeparated.NAME_ID}" @Test @Tag(WarningNames.ENUMS_SEPARATED) fun `check simple correct enum with new line`() { lintMethod( """ |enum class ENUM { | A, | B, | C, | ; |} """.trimMargin() ) } @Test @Tag(WarningNames.ENUMS_SEPARATED) fun `check simple correct enum with comments`() { lintMethod( """ |enum class ENUM { | A, // this is A | B, | C, | ; |} """.trimMargin() ) } @Test @Tag(WarningNames.ENUMS_SEPARATED) fun `check simple enum but with fun`() { lintMethod( """ |enum class ENUM { | A, B, C; | fun foo() {} |} """.trimMargin(), DiktatError(2, 4, ruleId, "${ENUMS_SEPARATED.warnText()} enum entries must end with a line break", true), DiktatError(2, 7, ruleId, "${ENUMS_SEPARATED.warnText()} enum entries must end with a line break", true), DiktatError(2, 10, ruleId, "${ENUMS_SEPARATED.warnText()} semicolon must be on a new line", true), DiktatError(2, 10, ruleId, "${ENUMS_SEPARATED.warnText()} last enum entry must end with a comma", true) ) } @Test @Tag(WarningNames.ENUMS_SEPARATED) fun `check one line enum`() { lintMethod( """ |enum class ENUM { | A, B, C |} """.trimMargin() ) } @Test @Tag(WarningNames.ENUMS_SEPARATED) fun `check wrong simple enum with new line last value`() { lintMethod( """ |enum class ENUM { | A, B, | C |} """.trimMargin(), DiktatError(2, 4, ruleId, "${ENUMS_SEPARATED.warnText()} enum entries must end with a line break", true), DiktatError(3, 4, ruleId, "${ENUMS_SEPARATED.warnText()} enums must end with semicolon", true), DiktatError(3, 4, ruleId, "${ENUMS_SEPARATED.warnText()} last enum entry must end with a comma", true) ) } @Test @Tag(WarningNames.ENUMS_SEPARATED) fun `check wrong simple enum with new line last value but with same line semicolon`() { lintMethod( """ |enum class ENUM { | A, B, | C, ; |} """.trimMargin(), DiktatError(2, 4, ruleId, "${ENUMS_SEPARATED.warnText()} enum entries must end with a line break", true), DiktatError(3, 4, ruleId, "${ENUMS_SEPARATED.warnText()} semicolon must be on a new line", true) ) } @Test @Tag(WarningNames.ENUMS_SEPARATED) fun `check correct enum but with initialize entries`() { lintMethod( """ |enum class ENUM { | RED(0xFF0000), | GREEN(0x00FF00), | BLUE(0x0000FF), | ; |} """.trimMargin() ) } @Test @Tag(WarningNames.ENUMS_SEPARATED) fun `check wrong enum with initialize entries and without last comma`() { lintMethod( """ |enum class ENUM { | RED(0xFF0000), | GREEN(0x00FF00), | BLUE(0x0000FF) | ; |} """.trimMargin(), DiktatError(4, 4, ruleId, "${ENUMS_SEPARATED.warnText()} last enum entry must end with a comma", true) ) } @Test @Tag(WarningNames.ENUMS_SEPARATED) fun `check correct enum with method`() { lintMethod( """ |enum class Warnings { | WAITING { | override fun signal() = TALKING | }, | TALKING { | override fun signal() = TALKING | }, | ; | abstract fun signal(): ProtocolState |} """.trimMargin() ) } @Test @Tag(WarningNames.ENUMS_SEPARATED) fun `check wrong enum without last comma and line break`() { lintMethod( """ |enum class Warnings { | WAITING { | override fun signal() = TALKING | }, | TALKING { | override fun signal() = TALKING | }; | abstract fun signal(): ProtocolState |} """.trimMargin(), DiktatError(5, 4, ruleId, "${ENUMS_SEPARATED.warnText()} semicolon must be on a new line", true), DiktatError(5, 4, ruleId, "${ENUMS_SEPARATED.warnText()} last enum entry must end with a comma", true) ) } @Test @Tag(WarningNames.ENUMS_SEPARATED) fun `check wrong enum without last comma, line break and semicolon`() { lintMethod( """ |enum class Warnings { | WAITING { | override fun signal() = TALKING | }, | TALKING { | override fun signal() = TALKING | } |} """.trimMargin(), DiktatError(5, 4, ruleId, "${ENUMS_SEPARATED.warnText()} enums must end with semicolon", true), DiktatError(5, 4, ruleId, "${ENUMS_SEPARATED.warnText()} last enum entry must end with a comma", true) ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/FileSizeWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter3.files.FileSize import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import java.io.File class FileSizeWarnTest : LintTestBase(::FileSize) { private val ruleId = "$DIKTAT_RULE_SET_ID:${FileSize.NAME_ID}" private val rulesConfigListLarge: List = listOf( RulesConfig(Warnings.FILE_IS_TOO_LONG.name, true, mapOf("maxSize" to "5")) ) private val rulesConfigListSmall: List = listOf( RulesConfig(Warnings.FILE_IS_TOO_LONG.name, true, mapOf("maxSize" to "10")) ) private val rulesConfigListEmpty: List = listOf( RulesConfig(Warnings.FILE_IS_TOO_LONG.name, true, emptyMap()) ) @Test @Tag(WarningNames.FILE_IS_TOO_LONG) fun `file smaller then max`() { val path = javaClass.classLoader.getResource("test/paragraph3/src/main/FileSizeLarger.kt") val file = File(path!!.file) lintMethodWithFile(file.toPath(), rulesConfigList = rulesConfigListSmall) } @Test @Tag(WarningNames.FILE_IS_TOO_LONG) fun `file larger then max`() { val path = javaClass.classLoader.getResource("test/paragraph3/src/main/FileSizeLarger.kt") val file = File(path!!.file) val size = file .readText() .split("\n") .size lintMethodWithFile(file.toPath(), DiktatError(1, 1, ruleId, "${Warnings.FILE_IS_TOO_LONG.warnText()} $size", false), rulesConfigList = rulesConfigListLarge) } @Test @Tag(WarningNames.FILE_IS_TOO_LONG) fun `use default values`() { val path = javaClass.classLoader.getResource("test/paragraph3/src/main/FileSizeLarger.kt") val file = File(path!!.file) lintMethodWithFile(file.toPath(), rulesConfigList = rulesConfigListEmpty) } @Test @Tag(WarningNames.FILE_IS_TOO_LONG) fun `file has more than 2000 lines`() { val path = javaClass.classLoader.getResource("test/paragraph3/src/main/A/FileSize2000.kt") val file = File(path!!.file) val size = generate2000lines() + 1 lintMethodWithFile(file.toPath(), DiktatError(1, 1, ruleId, "${Warnings.FILE_IS_TOO_LONG.warnText()} $size", false), rulesConfigList = rulesConfigListLarge) } private fun generate2000lines(): Int { val path = javaClass.classLoader.getResource("test/paragraph3/src/main/A/FileSize2000.kt") val file = File(path!!.file) file.writeText("//hello \n".repeat(2000)) return 2000 } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/FileStructureRuleFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.common.config.rules.DIKTAT_COMMON import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter3.files.FileStructureRule import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class FileStructureRuleFixTest : FixTestBase("test/paragraph3/file_structure", ::FileStructureRule) { @Test @Tag(WarningNames.FILE_INCORRECT_BLOCKS_ORDER) fun `should move @file targeted annotations after header KDoc`() { fixAndCompare("FileAnnotationExpected.kt", "FileAnnotationTest.kt") } @Test @Tag(WarningNames.FILE_INCORRECT_BLOCKS_ORDER) fun `should move copyright comment before @file targeted annotations`() { fixAndCompare("CopyrightCommentPositionExpected.kt", "CopyrightCommentPositionTest.kt") } @Test @Tag(WarningNames.FILE_INCORRECT_BLOCKS_ORDER) fun `should move header kdoc before package directive`() { fixAndCompare("HeaderKdocAfterPackageExpected.kt", "HeaderKdocAfterPackageTest.kt") } @Test @Tag(WarningNames.FILE_NO_BLANK_LINE_BETWEEN_BLOCKS) fun `should insert blank lines between code blocks`() { fixAndCompare("BlankLinesBetweenBlocksExpected.kt", "MissingBlankLinesBetweenBlocksTest.kt") } @Test @Tag(WarningNames.FILE_NO_BLANK_LINE_BETWEEN_BLOCKS) fun `should remove redundant blank lines`() { fixAndCompare("BlankLinesBetweenBlocksExpected.kt", "RedundantBlankLinesBetweenBlocksTest.kt") } @Test @Tag(WarningNames.FILE_UNORDERED_IMPORTS) fun `should reorder imports alphabetically with saving of EOL comments`() { fixAndCompare( "ReorderingImportsExpected.kt", "ReorderingImportsTest.kt", overrideRulesConfigList = listOf( RulesConfig( Warnings.FILE_UNORDERED_IMPORTS.name, true, mapOf("useRecommendedImportsOrder" to "false") ) ) ) } @Test @Tag(WarningNames.FILE_UNORDERED_IMPORTS) fun `should reorder imports according to recommendation 3_1`() { fixAndCompare( "ReorderingImportsRecommendedExpected.kt", "ReorderingImportsRecommendedTest.kt", overrideRulesConfigList = listOf( RulesConfig( DIKTAT_COMMON, true, mapOf("domainName" to "com.saveourtool.diktat") ), RulesConfig( Warnings.FILE_UNORDERED_IMPORTS.name, true, mapOf("useRecommendedImportsOrder" to "true") ) ) ) } @Test @Tag(WarningNames.FILE_UNORDERED_IMPORTS) fun `should still work with default package and some imports`() { fixAndCompare("DefaultPackageWithImportsExpected.kt", "DefaultPackageWithImportsTest.kt") } @Test @Tag(WarningNames.FILE_UNORDERED_IMPORTS) fun `should still work with default package and no imports`() { fixAndCompare("NoImportNoPackageExpected.kt", "NoImportNoPackageTest.kt") } @Test @Tag(WarningNames.FILE_UNORDERED_IMPORTS) fun `should move other comments before package node`() { fixAndCompare("OtherCommentsExpected.kt", "OtherCommentsTest.kt") } @Test @Tag(WarningNames.FILE_INCORRECT_BLOCKS_ORDER) fun `invalid move in kts files`() { fixAndCompare("ScriptPackageDirectiveExpected.kts", "ScriptPackageDirectiveTest.kts") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/FileStructureRuleTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.constants.Warnings.FILE_CONTAINS_ONLY_COMMENTS import com.saveourtool.diktat.ruleset.constants.Warnings.FILE_INCORRECT_BLOCKS_ORDER import com.saveourtool.diktat.ruleset.constants.Warnings.FILE_NO_BLANK_LINE_BETWEEN_BLOCKS import com.saveourtool.diktat.ruleset.constants.Warnings.FILE_UNORDERED_IMPORTS import com.saveourtool.diktat.ruleset.constants.Warnings.FILE_WILDCARD_IMPORTS import com.saveourtool.diktat.ruleset.rules.chapter3.files.FileStructureRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir import java.nio.file.Path class FileStructureRuleTest : LintTestBase(::FileStructureRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${FileStructureRule.NAME_ID}" private val rulesConfigListWildCardImport: List = listOf( RulesConfig(FILE_WILDCARD_IMPORTS.name, true, mapOf("allowedWildcards" to "com.saveourtool.diktat.*")) ) private val rulesConfigListWildCardImports: List = listOf( RulesConfig(FILE_WILDCARD_IMPORTS.name, true, mapOf("allowedWildcards" to "com.saveourtool.diktat.*, com.saveourtool.diktat.ruleset.constants.Warnings.*")) ) private val rulesConfigListEmptyDomainName: List = listOf( RulesConfig("DIKTAT_COMMON", true, mapOf("domainName" to "")) ) @Test @Tag(WarningNames.FILE_CONTAINS_ONLY_COMMENTS) fun `should warn if file contains only comments`() { lintMethod( """ |package com.saveourtool.diktat.example | |/** | * This file appears to be empty | */ | | |// lorem ipsum """.trimMargin(), DiktatError(1, 1, ruleId, "${FILE_CONTAINS_ONLY_COMMENTS.warnText()} file contains no code", false) ) } @Test @Tag(WarningNames.FILE_INCORRECT_BLOCKS_ORDER) fun `should warn if file annotations are not immediately before package directive`() { lintMethod( """ |@file:JvmName("Foo") | |/** | * This is an example file | */ | |package com.saveourtool.diktat.example | |class Example { } """.trimMargin(), DiktatError(1, 1, ruleId, "${FILE_INCORRECT_BLOCKS_ORDER.warnText()} @file:JvmName(\"Foo\")", true), DiktatError(3, 1, ruleId, "${FILE_INCORRECT_BLOCKS_ORDER.warnText()} /**", true) ) } @Test @Tag(WarningNames.FILE_UNORDERED_IMPORTS) fun `should warn if imports are not sorted alphabetically`() { lintMethod( """ |package com.saveourtool.diktat.example | |import org.junit.Test |import com.saveourtool.diktat.Foo | |class Example { |val x: Test = null |val y: Foo = null |} """.trimMargin(), DiktatError(3, 1, ruleId, "${FILE_UNORDERED_IMPORTS.warnText()} import com.saveourtool.diktat.Foo...", true) ) } @Test @Tag(WarningNames.FILE_WILDCARD_IMPORTS) fun `should warn if wildcard imports are used`() { lintMethod( """ |package com.saveourtool.diktat.example | |import com.saveourtool.diktat.* | |class Example { } """.trimMargin(), DiktatError(3, 1, ruleId, "${FILE_WILDCARD_IMPORTS.warnText()} import com.saveourtool.diktat.*", false), ) } @Test @Tag(WarningNames.FILE_NO_BLANK_LINE_BETWEEN_BLOCKS) fun `should warn if blank lines are wrong between code blocks`() { lintMethod( """ |/** | * This is an example | */ |@file:JvmName("Foo") | | |package com.saveourtool.diktat.example |import com.saveourtool.diktat.Foo |class Example{ |val x: Foo = null |} """.trimMargin(), DiktatError(1, 1, ruleId, "${FILE_NO_BLANK_LINE_BETWEEN_BLOCKS.warnText()} /**", true), DiktatError(4, 1, ruleId, "${FILE_NO_BLANK_LINE_BETWEEN_BLOCKS.warnText()} @file:JvmName(\"Foo\")", true), DiktatError(7, 1, ruleId, "${FILE_NO_BLANK_LINE_BETWEEN_BLOCKS.warnText()} package com.saveourtool.diktat.example", true), DiktatError(8, 1, ruleId, "${FILE_NO_BLANK_LINE_BETWEEN_BLOCKS.warnText()} import com.saveourtool.diktat.Foo", true) ) } @Test @Tag(WarningNames.FILE_WILDCARD_IMPORTS) fun `wildcard imports are used but with config`() { lintMethod( """ |package com.saveourtool.diktat.example | |import com.saveourtool.diktat.* | |class Example { } """.trimMargin(), rulesConfigList = rulesConfigListWildCardImport ) } @Test @Tag(WarningNames.FILE_WILDCARD_IMPORTS) fun `wildcard imports are used but with several imports in config`() { lintMethod( """ |package com.saveourtool.diktat.example | |import com.saveourtool.diktat.* |import com.saveourtool.diktat.ruleset.constants.Warnings.* | |class Example { } """.trimMargin(), rulesConfigList = rulesConfigListWildCardImports ) } @Test @Tag(WarningNames.FILE_INCORRECT_BLOCKS_ORDER) fun `should warn if there are other misplaced comments before package - positive example`() { lintMethod( """ |/** | * This is an example | */ | |// some notes on this file |package com.saveourtool.diktat.example | |import com.saveourtool.diktat.Foo | |class Example{ |val x: Foo = null |} """.trimMargin() ) } @Test @Tag(WarningNames.FILE_INCORRECT_BLOCKS_ORDER) fun `should warn if there are other misplaced comments before package`() { lintMethod( """ |// some notes on this file |/** | * This is an example | */ | |package com.saveourtool.diktat.example | |import com.saveourtool.diktat.Foo | |class Example{ |val x: Foo = null |} """.trimMargin(), DiktatError(1, 1, ruleId, "${FILE_INCORRECT_BLOCKS_ORDER.warnText()} // some notes on this file", true), DiktatError(2, 1, ruleId, "${FILE_INCORRECT_BLOCKS_ORDER.warnText()} /**", true), ) } @Test @Tag(WarningNames.FILE_INCORRECT_BLOCKS_ORDER) fun `block comment should be detected as copyright - positive example`() { lintMethod( """ |/* | * Copyright Example Inc. (c) | */ | |@file:Annotation | |package com.saveourtool.diktat.example | |import com.saveourtool.diktat.Foo | |class Example{ |val x: Foo = null |} """.trimMargin() ) } @Test @Tag(WarningNames.FILE_INCORRECT_BLOCKS_ORDER) fun `block comment shouldn't be detected as copyright without keywords`() { lintMethod( """ |/* | * Just a regular block comment | */ |@file:Annotation | |package com.saveourtool.diktat.example | |import com.saveourtool.diktat.Foo | |class Example{ |val x: Foo = null |} """.trimMargin(), DiktatError(4, 1, ruleId, "${FILE_INCORRECT_BLOCKS_ORDER.warnText()} @file:Annotation", true) ) } @Test @Tag(WarningNames.FILE_INCORRECT_BLOCKS_ORDER) fun `test with empty domain name in config`() { lintMethod( """ |package com.saveourtool.diktat.example | |import com.saveourtool.diktat.Foo |import com.pinterest.ktlint.core.LintError | |class Example{ |val x: LintError = null |val x: Foo = null |} """.trimMargin(), DiktatError(3, 1, ruleId, "${FILE_UNORDERED_IMPORTS.warnText()} import com.pinterest.ktlint.core.LintError...", true), rulesConfigList = rulesConfigListEmptyDomainName ) } @Test @Tag(WarningNames.UNUSED_IMPORT) fun `import from the package`() { lintMethod( """ |package com.saveourtool.diktat.example | |import com.saveourtool.diktat.example.Foo | |class Example { |} """.trimMargin(), DiktatError(1, 1, ruleId, "${Warnings.UNUSED_IMPORT.warnText()} com.saveourtool.diktat.example.Foo - unused import", true) ) } @Test @Tag(WarningNames.UNUSED_IMPORT) fun `unused import`() { lintMethod( """ |package com.saveourtool.diktat.example | |import com.saveourtool.diktat.Foo | |class Example { |} """.trimMargin(), DiktatError(1, 1, ruleId, "${Warnings.UNUSED_IMPORT.warnText()} com.saveourtool.diktat.Foo - unused import", true) ) } @Test @Tag(WarningNames.UNUSED_IMPORT) fun `used import`() { lintMethod( """ |package com.saveourtool.diktat.example | |import com.saveourtool.diktat.Foo | |class Example { |val x: Foo = null |} """.trimMargin(), ) } @Test @Tag(WarningNames.UNUSED_IMPORT) fun `Operator overloading`() { lintMethod( """ |package com.saveourtool.diktat.example | |import kotlin.io.path.div | |class Example { |val pom = kotlin.io.path.createTempFile().toFile() |val x = listOf(pom.parentFile.toPath() / "src/main/kotlin/exclusion") |} """.trimMargin(), ) } @Test @Tag(WarningNames.UNUSED_IMPORT) fun `Should correctly check infix functions`() { lintMethod( """ |package com.saveourtool.diktat.example | |import com.saveourtool.diktat.utils.logAndExit | |fun main() { |"Type is not supported yet" logAndExit 1 |} """.trimMargin(), ) } @Test @Tag(WarningNames.UNUSED_IMPORT) fun `unused import to infix functions`() { lintMethod( """ |package com.saveourtool.diktat.example | |import com.saveourtool.diktat.utils.logAndExit | |fun main() { |println("Type is not supported yet") |} """.trimMargin(), DiktatError(1, 1, ruleId, "${Warnings.UNUSED_IMPORT.warnText()} com.saveourtool.diktat.utils.logAndExit - unused import", true) ) } @Test @Tag(WarningNames.UNUSED_IMPORT) fun `Acute import`() { lintMethod( """ |package com.saveourtool.diktat.example | |import js.externals.jquery.`${'$'}` | |fun main() { | `${'$'}`("document").ready {} |} """.trimMargin(), ) } @Test @Tag(WarningNames.UNUSED_IMPORT) fun `Ignore Imports`() { lintMethod( """ |package com.saveourtool.diktat.example | |import com.example.get |import com.example.invoke |import com.example.set | |fun main() { | val a = list[1] |} """.trimMargin(), ) } @Test @Tag(WarningNames.UNUSED_IMPORT) fun `check by #1`() { lintMethod( """ |package com.saveourtool.diktat.example | |import com.example.get |import com.example.invoke |import com.example.set |import kotlin.properties.Delegates | |fun main() { | val a by Delegates.observable() |} """.trimMargin(), ) } @Test @Tag(WarningNames.UNUSED_IMPORT) fun `check by #2 should not trigger`() { lintMethod( """ |package com.saveourtool.diktat.example | |import com.example.equals |import com.example.get |import com.example.invoke |import com.example.set |import org.gradle.kotlin.dsl.provideDelegate |import tasks.getValue |import tasks.setValue | |fun main() { | val a by tasks.getting // `getValue` is used | val b by project.tasks // `provideDelegate` is used | a != 0 |} """.trimMargin(), ) } @Test @Tag(WarningNames.UNUSED_IMPORT) fun `check by #3 should trigger`() { lintMethod( """ |package com.saveourtool.diktat.example | |import Delegate |import com.example.equals |import com.example.get |import com.example.invoke |import com.example.set | |fun main() { | val a: Foo |} """.trimMargin(), DiktatError(1, 1, ruleId, "${Warnings.UNUSED_IMPORT.warnText()} Delegate - unused import", true), DiktatError(1, 1, ruleId, "${Warnings.UNUSED_IMPORT.warnText()} com.example.equals - unused import", true) ) } // Fixme: This test is not passing because for now we don't have type resolution @Disabled @Test @Tag(WarningNames.UNUSED_IMPORT) fun `check by #4 should trigger`() { lintMethod( """ |package com.saveourtool.diktat.example | |import com.example.get |import com.example.invoke |import com.example.set |import tasks.getValue | |fun main() { | val a |} """.trimMargin(), DiktatError(1, 1, ruleId, "${Warnings.UNUSED_IMPORT.warnText()} tasks.getValue - unused import", true) ) } @Test @Tag(WarningNames.UNUSED_IMPORT) fun `import in KDoc #1`() { lintMethod( """ |import java.io.IOException | |interface BluetoothApi { | | /** | * Send array of bytes to bluetooth output stream. | * This call is asynchronous. | * | * Note that this operation can still throw an [IOException] if the remote device silently | * closes the connection so the pipe gets broken. | * | * @param bytes data to send | * @return true if success, false if there was an error or device has been disconnected | */ | fun trySend(bytes: ByteArray): Boolean |} """.trimMargin(), ) } @Test @Tag(WarningNames.UNUSED_IMPORT) fun `import in KDoc #2`() { lintMethod( """ |import java.io.IOException |import java.io.IOException as IOE |import java.io.UncheckedIOException |import java.io.UncheckedIOException as UIOE | |interface BluetoothApi { | /** | * @see IOException | * @see [UncheckedIOException] | * @see IOE | * @see [UIOE] | */ | fun trySend(bytes: ByteArray): Boolean |} """.trimMargin(), ) } @Test @Tag(WarningNames.UNUSED_IMPORT) fun `import in KDoc #3`() { lintMethod( """ |package com.example | |import com.example.Library1 as Lib1 |import com.example.Library1.doSmth as doSmthElse1 |import com.example.Library2 as Lib2 |import com.example.Library2.doSmth as doSmthElse2 | |object Library1 { | fun doSmth(): Unit = TODO() |} | |object Library2 { | fun doSmth(): Unit = TODO() |} | |/** | * @see Lib1.doSmth | * @see doSmthElse1 | * @see [Lib2.doSmth] | * @see [doSmthElse2] | */ |class Client """.trimMargin(), ) } @Test @Tag(WarningNames.FILE_INCORRECT_BLOCKS_ORDER) fun `error in moving blocking at the first place`(@TempDir tempDir: Path) { lintMethodWithFile( """ // Without suppressing these, version catalog usage in `plugins` is marked as an error in IntelliJ: // https://youtrack.jetbrains.com/issue/KTIJ-19369 @file:Suppress("DSL_SCOPE_VIOLATION") plugins { id(libs.plugins.kotlinJvm.get().pluginId) } """.trimIndent(), fileName = "build.gradle.kts", tempDir = tempDir, expectedLintErrors = arrayOf(DiktatError(3, 1, ruleId, "${Warnings.FILE_INCORRECT_BLOCKS_ORDER.warnText()} @file:Suppress(\"DSL_SCOPE_VIOLATION\")", true)), ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/LineLengthFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.LONG_LINE import com.saveourtool.diktat.ruleset.rules.chapter3.LineLength import com.saveourtool.diktat.util.FixTestBase import org.junit.jupiter.api.Test class LineLengthFixTest : FixTestBase("test/paragraph3/long_line", ::LineLength) { private val rulesConfigListLongLineLength: List = listOf( RulesConfig(LONG_LINE.name, true, mapOf("lineLength" to "175")) ) private val rulesConfigListDefaultLineLength: List = listOf( RulesConfig(LONG_LINE.name, true, mapOf("lineLength" to "120")) ) private val rulesConfigListLineLength: List = listOf( RulesConfig(LONG_LINE.name, true, mapOf("lineLength" to "50")) ) private val rulesConfigListShortLineLength: List = listOf( RulesConfig(LONG_LINE.name, true, mapOf("lineLength" to "20")) ) private val rulesConfigListErrorLineLength1: List = listOf( RulesConfig(LONG_LINE.name, true, mapOf("lineLength" to "151")) ) @Test fun `should fix long comment`() { fixAndCompare("LongLineCommentExpected.kt", "LongLineCommentTest.kt", rulesConfigListLineLength) } @Test fun `should fix long inline comments`() { fixAndCompare("LongInlineCommentsExpected.kt", "LongInlineCommentsTest.kt", rulesConfigListLineLength) } @Test fun `should fix long comment 2`() { fixAndCompare("LongLineCommentExpected2.kt", "LongLineCommentTest2.kt", rulesConfigListDefaultLineLength) } @Test fun `should fix long string template while some fix is already done`() { fixAndCompare("LongStringTemplateExpected.kt", "LongStringTemplateTest.kt", rulesConfigListDefaultLineLength) } @Test fun `should fix long binary expression`() { fixAndCompare("LongLineExpressionExpected.kt", "LongLineExpressionTest.kt", rulesConfigListLineLength) } @Test fun `should fix complex long binary expressions`() { fixAndCompare("LongBinaryExpressionExpected.kt", "LongBinaryExpressionTest.kt", rulesConfigListLineLength) } @Test fun `should fix long function`() { fixAndCompare("LongLineFunExpected.kt", "LongLineFunTest.kt", rulesConfigListLineLength) } @Test fun `should fix long right value`() { fixAndCompare("LongLineRValueExpected.kt", "LongLineRValueTest.kt", rulesConfigListLineLength) } @Test fun `should fix short long right value`() { fixAndCompare("LongShortRValueExpected.kt", "LongShortRValueTest.kt", rulesConfigListShortLineLength) } @Test fun `shouldn't fix`() { fixAndCompare("LongExpressionNoFixExpected.kt", "LongExpressionNoFixTest.kt", rulesConfigListShortLineLength) } @Test fun `should fix annotation`() { fixAndCompare("LongLineAnnotationExpected.kt", "LongLineAnnotationTest.kt", rulesConfigListLineLength) } @Test fun `fix condition in small function with long length`() { fixAndCompare("LongConditionInSmallFunctionExpected.kt", "LongConditionInSmallFunctionTest.kt", rulesConfigListLongLineLength) } @Test fun `fix expression in condition`() { fixAndCompare("LongExpressionInConditionExpected.kt", "LongExpressionInConditionTest.kt", rulesConfigListLineLength) } @Test fun `fix long Dot Qualified Expression`() { fixAndCompare("LongDotQualifiedExpressionExpected.kt", "LongDotQualifiedExpressionTest.kt", rulesConfigListLineLength) } @Test fun `fix long value arguments list`() { fixAndCompare("LongValueArgumentsListExpected.kt", "LongValueArgumentsListTest.kt", rulesConfigListLineLength) } @Test fun `fix bin expression first symbol last word`() { fixAndCompare("LongBinaryExpressionLastWordExpected.kt", "LongBinaryExpressionLastWordTest.kt", rulesConfigListErrorLineLength1) } @Test fun `fix bin 1expression first symbol last word`() { fixAndCompare("LongComplexExpressionExpected.kt", "LongComplexExpressionTest.kt", rulesConfigListErrorLineLength1) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/LineLengthWarnTest.kt ================================================ @file:Suppress("LONG_LINE") package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.LONG_LINE import com.saveourtool.diktat.ruleset.rules.chapter3.LineLength import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class LineLengthWarnTest : LintTestBase(::LineLength) { private val ruleId = "$DIKTAT_RULE_SET_ID:${LineLength.NAME_ID}" private val rulesConfigListLineLength: List = listOf( RulesConfig(LONG_LINE.name, true, mapOf("lineLength" to "163")) ) private val shortLineLength: List = listOf( RulesConfig(LONG_LINE.name, true, mapOf("lineLength" to "40")) ) private val wrongUrl = "dhttps://www.google.com/search?q=djfhvkdfhvkdh+gthtdj%" + "3Bb&rlz=1C1GCEU_enRU909RU909&oq=posible+gthtdj%3Bb&aqs=chrome.." + "69i57j0l3.2680j1j7&sourceid=chrome&ie=UTF-8" private val correctUrl = "https://www.google.com/search?q=djfhvkdfhvkdh+gthtdj%" + "3Bb&rlz=1C1GCEU_enRU909RU909&oq=posible+gthtdj%3Bb&aqs=chrome.." + "69i57j0l3.2680j1j7&sourceid=chrome&ie=UTF-8" @Test @Tag(WarningNames.LONG_LINE) fun `check correct example with long URL in KDOC and long import`() { lintMethod( """ |package com.saveourtool.diktat.ruleset.chapter3 | |import com.saveourtool.diktat.ruleset.rules.LineLength.sdfsdfsf.sdfsdfsdfsdfdghdf.gfhdf.hdstst.dh.dsgfdfgdgs.rhftheryryj.cgh |import com.saveourtool.diktat.util.lintMethod | |/** | * https://www.google.com/search?q=djfhvkdfhvkdh+gthtdj%3Bb&rlz=1C1GCEU_enRU909RU909&oq=posible+gthtdj%3Bb&aqs=chrome..69i57j0l3.2680j1j7&sourceid=chrome&ie=UTF-8 | * https://www.google.com/search?q=djfhvkdfhvkdh+gthtdj%3Bb&rlz=1C1GCEU_enRU909RU909&oq=posible+gthtdj%3Bb&aqs=chrome..69i57j0l3.2680j1j7&sourceid=chrome&ie=UTF-8 | * @param a |*/ | |class A{ | companion object { | } | | fun foo() { | } |} """.trimMargin(), rulesConfigList = shortLineLength ) } @Test @Tag(WarningNames.LONG_LINE) fun `check wrong example with wrong URL in KDOC`() { lintMethod( """ |package com.saveourtool.diktat.ruleset.chapter3 | |import com.saveourtool.diktat.ruleset.rules.chapter3.LineLength |import com.saveourtool.diktat.util.lintMethod | |/** | * https://github.com/pinterest/ktlint/blob/master/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/MaxLineLengthRule.kt | * $wrongUrl | * https://www.google.com/search?q=djfhvkdfhvkdh+gthtdj%3Bb&rlz=1C1GCEU_enRU909RU909&oq=posible+gthtdj%3Bb&aqs=chrome..69i57j0l3.2680j1j7&sourceid=chrome&ie=UTF-8 | * @param a |*/ | |class A{ | companion object { | } | | fun foo() { | } |} """.trimMargin(), DiktatError(8, 1, ruleId, "${LONG_LINE.warnText()} max line length 120, but was 163", false) ) } @Test @Tag(WarningNames.LONG_LINE) fun `check wrong example with wrong URL in KDOC with configuration`() { lintMethod( """ |package com.saveourtool.diktat.ruleset.chapter3 | |import com.saveourtool.diktat.ruleset.rules.chapter3.LineLength |import com.saveourtool.diktat.util.lintMethod | |/** | * $wrongUrl | * https://www.google.com/search?q=djfhvkdfhvkdh+gthtdj%3Bb&rlz=1C1GCEU_enRU909RU909&oq=posible+gthtdj%3Bb&aqs=chrome..69i57j0l3.2680j1j7&sourceid=chrome&ie=UTF-8 | * @param a |*/ | |class A{ | companion object { | } | | fun foo() { | } |} """.trimMargin(), rulesConfigList = rulesConfigListLineLength ) } @Test @Tag(WarningNames.LONG_LINE) fun `check wrong example with long line`() { lintMethod( """ |package com.saveourtool.diktat.ruleset.chapter3 | |import com.saveourtool.diktat.ruleset.rules.chapter3.LineLength |import com.saveourtool.diktat.util.lintMethod | |/** | * https://www.google.com/search?q=djfhvkdfhvkdh+gthtdj%3Bb&rlz=1C1GCEU_enRU909RU909&oq=posible+gthtdj%3Bb&aqs=chrome..69i57j0l3.2680j1j7&sourceid=chrome&ie=UTF-8 | * https://www.google.com/search?q=djfhvkdfhvkdh+gthtdj%3Bb&rlz=1C1GCEU_enRU909RU909&oq=posible+gthtdj%3Bb&aqs=chrome..69i57j0l3.2680j1j7&sourceid=chrome&ie=UTF-8 | * @param a |*/ | |class A{ | companion object { | val str = "sdjhkjdfhkjsdhfkshfkjshkfhsdkjfhskjdfhkshdfkjsdhfkjsdhfkshdkfhsdkjfhskdjfhkjsdfhkjsdhfjksdhfkjsdhfjkhsdkjfhskdjfhksdfhskdhf" | } | | fun foo() { | val str = "sdjhkjdfhkjsdhfkshfkjshkfhsdkjfhskjdfhkshdfkjsdhfkjsdhfkshdkfhsdkjfhskdjfhkjsdfhkjsdhfjksdhfkjsdhfjkhsdkjfhskdjfhksdfhskdhf" | } |} """.trimMargin(), DiktatError(14, 1, ruleId, "${LONG_LINE.warnText()} max line length 120, but was 143", true), DiktatError(18, 1, ruleId, "${LONG_LINE.warnText()} max line length 120, but was 142", true) ) } @Test @Tag(WarningNames.LONG_LINE) fun `check wrong example with long line but with configuration`() { lintMethod( """ |package com.saveourtool.diktat.ruleset.chapter3 | |import com.saveourtool.diktat.ruleset.rules.chapter3.LineLength |import com.saveourtool.diktat.util.lintMethod | |/** | * This is very important URL https://www.google.com/search?q=djfhvkdfhvkdh+gthtdj%3Bb&rlz=1C1GCEU_enRU909RU909&oq=posible+gthtdj%3Bb&aqs=chrome..69i57j0l3.2680j1j7&sourceid=chrome&ie=UTF-8 | * https://www.google.com/search?q=djfhvkdfhvkdh+gthtdj%3Bb&rlz=1C1GCEU_enRU909RU909&oq=posible+gthtdj%3Bb&aqs=chrome..69i57j0l3.2680j1j7&sourceid=chrome&ie=UTF-8 | * $correctUrl this text can be on another line | * @param a |*/ | |class A{ | companion object { | } | | fun foo() { | val str = "sdjhkjdfhkjsdhfkshfkjshkfhsdkjfhskjdfhkshdfkjsdhfkjsdhfkshdkfhsdkjfhskdjfhkjsdfhkjsdhfjksdhfkjsdhfjkhsdkjfhskdjfhksdfhskdhf" | } |} """.trimMargin(), DiktatError(9, 1, ruleId, "${LONG_LINE.warnText()} max line length 163, but was 195", false), rulesConfigList = rulesConfigListLineLength ) } @Test @Tag(WarningNames.LONG_LINE) fun `check correct example with long URL in KDOC in class`() { lintMethod( """ |package com.saveourtool.diktat.ruleset.chapter3 | |import com.saveourtool.diktat.ruleset.rules.chapter3.LineLength |import com.saveourtool.diktat.util.lintMethod | | |class A { | fun test() { | /** | * [link]($correctUrl) | * [link]$correctUrl | * https://www.google.com/search?q=djfhvkdfhvkdh+gthtdj%3Bb&rlz=1C1GCEU_enRU909RU909&oq=posible+gthtdj%3Bb&aqs=chrome..69i57j0l3.2680j1j7&sourceid=chrome&ie=UTF-8 | * https://www.google.com/search?q=djfhvkdfhvkdh+gthtdj%3Bb&rlz=1C1GCEU_enRU909RU909&oq=posible+gthtdj%3Bb&aqs=chrome..69i57j0l3.2680j1j7&sourceid=chrome&ie=UTF-8 | * @param a | */ | println(123) | } |} """.trimMargin() ) } @Test @Tag(WarningNames.LONG_LINE) fun `check wrong examples with long function name and properties`() { lintMethod( """ |package com.saveourtool.diktat.ruleset.chapter3 | |import com.saveourtool.diktat.ruleset.rules.chapter3.LineLength |import com.saveourtool.diktat.util.lintMethod | | |class A { | fun functionNameisTooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooLong() { | val text = "sdfkjhsdhjfgdjghdfjghdkfjghdkjghdfkghdkjhfgkjdfhgkjdfhgkjdhgfkjdfhgkjdhgkhdfkghdiulghfdilughdsdcsdcsdcs" | println("dhfgkjdhfgkjhdkfjghdkjfghdkjfhgkdfhgdkghkghdkjfhgdkghfkdjhfgkjdhfgjkdhfgkjddhfgkdhfgjkdh") | } |} """.trimMargin(), DiktatError(8, 1, ruleId, "${LONG_LINE.warnText()} max line length 120, but was 130", false), DiktatError(9, 1, ruleId, "${LONG_LINE.warnText()} max line length 120, but was 123", true) ) } @Test @Tag(WarningNames.LONG_LINE) fun `check annotation and fun with expr body`() { lintMethod( """ |@Query(value = "ASDAASDASDASDASDASDASDASDAASDASDASDASDASDASDASDAASDASDASDASDASDASD") |fun foo() = println("ASDAASDASDASDASDASDASDASDAASDASDASDASDASDASDASDAASDASDASDASDASDASD") """.trimMargin(), DiktatError(1, 1, ruleId, "${LONG_LINE.warnText()} max line length 40, but was 84", true), DiktatError(2, 1, ruleId, "${LONG_LINE.warnText()} max line length 40, but was 89", true), rulesConfigList = shortLineLength ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/LocalVariablesWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter3.identifiers.LocalVariablesRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames.LOCAL_VARIABLE_EARLY_DECLARATION import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test @Suppress("LargeClass") class LocalVariablesWarnTest : LintTestBase(::LocalVariablesRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${LocalVariablesRule.NAME_ID}" private fun warnMessage(name: String, declared: Int, used: Int ) = "<$name> is declared on line <$declared> and is used for the first time on line <$used>" @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) fun `should not check top-level and member properties`() { lintMethod( """ |const val foo = 0 | |class Example { | val bar = 0 |} """.trimMargin() ) } @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) fun `local variables used only in this scope - positive example`() { lintMethod( """ |import org.diktat.Some as test |class Example { | fun foo() { | val bar = 0 | baz(bar) | println() | } |} """.trimMargin() ) } @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) fun `local variables used only in this scope - positive example with blank lines`() { lintMethod( """ |class Example { | fun foo() { | val bar = 0 | | baz(bar) | println() | } |} """.trimMargin() ) } @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) fun `local variables used only in this scope - positive example with comments`() { lintMethod( """ |class Example { | fun foo() { | val bar = 0 | // comment | baz(bar) | println() | } |} """.trimMargin() ) } @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) fun `local var used only in this scope - positive example`() { lintMethod( """ |fun foo() { | var bar: MutableList | baz(bar) | println() |} """.trimMargin() ) } @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) fun `local var used only in this scope with multiline usage - positive example`() { lintMethod( """ |fun foo(obj: Type?) { | var bar: MutableList | obj | ?.baz(bar) | println() |} """.trimMargin() ) } @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) fun `local variables used only in this scope`() { lintMethod( """ |fun foo() { | val bar = 0 | println() | baz(bar) |} """.trimMargin(), DiktatError(2, 5, ruleId, "${Warnings.LOCAL_VARIABLE_EARLY_DECLARATION.warnText()} ${warnMessage("bar", 2, 4)}", false) ) } @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) fun `local variables used only in this scope - multiline declaration, positive example`() { lintMethod( """ |fun foo() { | val bar = obj | .foo() | baz(bar) |} """.trimMargin() ) } @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) fun `local variables used only in this scope - multiline declaration with binary expression`() { lintMethod( """ |fun foo() { | val bar = 1 + | 2 | println() | baz(bar) |} """.trimMargin(), DiktatError(2, 5, ruleId, "${Warnings.LOCAL_VARIABLE_EARLY_DECLARATION.warnText()} ${warnMessage("bar", 2, 5)}", false) ) } @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) fun `local variables used only in this scope - multiline declaration with dot qualified property access`() { lintMethod( """ |fun foo() { | val bar = "string" | .size | println() | baz(bar) |} """.trimMargin(), DiktatError(2, 5, ruleId, "${Warnings.LOCAL_VARIABLE_EARLY_DECLARATION.warnText()} ${warnMessage("bar", 2, 5)}", false) ) } @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) fun `local variables used only in this scope - multiline declaration with dot qualified method call`() { lintMethod( """ |fun foo() { | val bar = "string" | .count() | println() | baz(bar) |} """.trimMargin(), DiktatError(2, 5, ruleId, "${Warnings.LOCAL_VARIABLE_EARLY_DECLARATION.warnText()} ${warnMessage("bar", 2, 5)}", false) ) } @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) @Disabled("Checking of variable from outer scope is not supported yet") fun `local variables defined in outer scope and used only in nested scope`() { lintMethod( """ |fun foo() { | val bar = 0 | try { | baz(bar) | } catch (e: Exception) { | println() | } |} """.trimMargin(), DiktatError(2, 5, ruleId, "${Warnings.LOCAL_VARIABLE_EARLY_DECLARATION.warnText()} val bar = 0", false) ) } @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) fun `local variables defined in outer scope and used in several scopes - positive example`() { lintMethod( """ |fun foo() { | val bar = 0 | try { | baz(bar) | println() | } catch (e: Exception) { | qux(bar) | } |} """.trimMargin() ) } @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) @Disabled("Checking of variable from outer scope is not supported yet") fun `local variables defined in outer scope and used in several scopes`() { lintMethod( """ |fun foo() { | val bar = 0 | if (condition) { | try { | baz(bar) | println() | } catch (e: Exception) { | qux(bar) | } | } else { | println() | } |} """.trimMargin(), DiktatError(2, 5, ruleId, "${Warnings.LOCAL_VARIABLE_EARLY_DECLARATION.warnText()} val bar = 0", false) ) } @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) fun `should not trigger on other objects fields with same name`() { lintMethod( """ |fun foo() { | val size = list.size | if (size > maxSize) { | bar() | } |} """.trimMargin() ) } @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) @Disabled("Checking of variable from outer scope is not supported yet") fun `need to allow declaring vars outside of loops`() { lintMethod( """ |fun foo() { | var offset = 0 | for (x in 1..100) { | if (condition) { | bar(offset) | } | offset += it.length | } |} """.trimMargin() ) } @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) fun `discovered during testing`() { lintMethod( """ |fun foo() { | var offset = 0 | for (x in 1..100) { | offset += it.length | } | return offset |} """.trimMargin() ) } @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) @Disabled("Checking of variable from outer scope is not supported yet") fun `need to allow declaring vars outside collection methods`() { lintMethod( """ |fun foo() { | var offset = 0 | list.forEach { | if (condition) { | bar(offset) | } | offset += it.length | } |} """.trimMargin() ) } @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) fun `should not trigger on properties with same name in different scopes`() { lintMethod( """ |fun foo(): Bar { | if (condition) { | val x = bar() | return Bar(x) | } else { | val x = baz() | return Bar(x) | } |} """.trimMargin() ) } @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) fun `should not trigger on properties with same name in different scopes - 2`() { lintMethod( """ |fun foo(): Bar { | for (x in A) { | val y = bar() | qux(y) | } | val y = bar() | qux(y) |} """.trimMargin() ) } @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) fun `should not trigger when variables from outer scope are shadowed by lambda parameters`() { lintMethod( """ |fun foo(): Bar { | val x = 0 | list.forEach { x -> | println() | bar(x) | } |} """.trimMargin() ) } @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) fun `should check usage inside lambdas - positive example`() { lintMethod( """ |fun foo(): Bar { | val x = 0 | list.forEach { | bar(x) | } |} """.trimMargin() ) } @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) fun `should check usage inside lambdas`() { lintMethod( """ |fun foo(): Bar { | val x = 0 | println() | list.forEach { | bar(x) | } |} """.trimMargin(), DiktatError(2, 5, ruleId, "${Warnings.LOCAL_VARIABLE_EARLY_DECLARATION.warnText()} ${warnMessage("x", 2, 5)}", false) ) } @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) fun `should check usage inside lambdas with line break`() { lintMethod( """ |fun foo(): Bar { | val x = 0 | println() | list | .forEach { | bar(x) | } |} """.trimMargin(), DiktatError(2, 5, ruleId, "${Warnings.LOCAL_VARIABLE_EARLY_DECLARATION.warnText()} ${warnMessage("x", 2, 6)}", false) ) } @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) fun `should check properties declared inside lambda`() { lintMethod( """ |fun foo() { | list.map { | val foo = "bar" | println() | foo.baz(it) | } |} """.trimMargin(), DiktatError(3, 9, ruleId, "${Warnings.LOCAL_VARIABLE_EARLY_DECLARATION.warnText()} ${warnMessage("foo", 3, 5)}", false) ) } @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) fun `should not trigger when more than one variables need to be declared`() { lintMethod( """ |fun foo() { | val x = 0 | val y = 1 | foo(x, y) |} """.trimMargin() ) } @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) fun `should not raise warning if there is property not in propertyToUsages between other properties declaration`() { lintMethod( """ |fun foo() { | val x = 0 | val a = -1 | val y = 1 | val z = 2 | foo(x, y, z) |} """.trimMargin() ) } @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) fun `should not trigger on properties`() { lintMethod( """ |private fun checkDoc(node: ASTNode, warning: Warnings) { | val a = 0 | val b = 1 | val c = 2 | | if (predicate(a) && predicate(b)) { | foo(a, b, c) | } |} """.trimMargin() ) } @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) @Disabled("Constructors are not handled separately yet") fun `should check variables initialized with constructor with no parameters`() { lintMethod( """ |fun foo(isRequired: Boolean): Type { | val resOption = Type() | println() | resOption.isRequired = isRequired | return resOption |} """.trimMargin() ) } @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) fun `check properties initialized with some selected methods`() { lintMethod( """ |fun foo() { | val list = emptyList() | println() | bar(list) |} """.trimMargin(), DiktatError(2, 5, ruleId, "${Warnings.LOCAL_VARIABLE_EARLY_DECLARATION.warnText()} ${warnMessage("list", 2, 4)}", false) ) } @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) fun `should properly detect containing scope of lambdas`() { lintMethod( """ |fun foo() { | val res = mutableListOf() | Foo.bar( | Foo.baz( | cb = { e, _ -> res.add(e) } | ) | ) | Assertions.assertThat(res).isEmpty() |} """.trimMargin() ) } @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) fun `should not trigger when there is a property in a class and same variable name in function and lambda`() { lintMethod( """ |class Example { | val a = "a1" | fun foo() { | val a = "a2" | listOf().forEach { a -> println(a) } | } |} """.trimMargin() ) } @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) fun `should not trigger on triple quoted strings`() { lintMethod( """ |class Example { | fun some() { | val code = ${"\"\"\""} | class Some { | fun for() : String { | } | } | ${"\"\"\""}.trimIndent() | bar(code) | } |} """.trimMargin() ) } @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) fun `should not trigger on space after last val`() { lintMethod( """ | private fun collectAllExtensionFunctions(node: ASTNode): SimilarSignatures { | val extensionFunctionList = node.findAllNodesWithSpecificType(FUN).filter { it.hasChildOfType(TYPE_REFERENCE) && it.hasChildOfType(DOT) } | val distinctFunctionSignatures = mutableMapOf() // maps function signatures on node it is used by | val extensionFunctionsPairs = mutableListOf>() // pairs extension functions with same signature | | extensionFunctionList.forEach { func -> | if (distinctFunctionSignatures.contains(signature)) { | val secondFuncClassName = distinctFunctionSignatures[signature]!!.findChildBefore(DOT, TYPE_REFERENCE)!!.text | extensionFunctionsPairs.add(Pair( | ExtensionFunction(secondFuncClassName, signature, distinctFunctionSignatures[signature]!!), | ExtensionFunction(className, signature, func))) | } else { | distinctFunctionSignatures[signature] = func | } | } | return extensionFunctionsPairs | } """.trimMargin() ) } @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) fun `should not trigger on var nodes which have initializer`() { lintMethod( """ | private fun collectAllExtensionFunctions(astNode: ASTNode): SimilarSignatures { | var text = "" | var node = astNode | var prevNode: ASTNode | do { | prevNode = node | node = node.treeParent | if (node.elementType == ElementType.PARENTHESIZED) { | text += getTextFromParenthesized(node) | } | } while (node.elementType != BINARY_EXPRESSION) | } """.trimMargin() ) } @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) fun `should skip comments`() { lintMethod( """ | private fun collectAllExtensionFunctions(astNode: ASTNode): SimilarSignatures { | var copyrightComment = "" | var headerKdoc = listOf() | // Annotations with target`file` can only be placed before `package` directive. | var fileAnnotations = node.findChildByType(FILE_ANNOTATION_LIST) | // We also collect all other elements that are placed on top of the file. | // These may be other comments, so we just place them before the code starts. | val otherNodesBeforeCode = firstCodeNode.siblings(forward = false) | .filterNot { | it.isWhiteSpace() || | it == copyrightComment || it == headerKdoc || it == fileAnnotations | } | .toList() | .reversed() | } """.trimMargin() ) } @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) fun `should skip val nodes between considered nodes`() { lintMethod( """ | private fun collectAllExtensionFunctions(astNode: ASTNode): SimilarSignatures { | val text = "" | val node = astNode | var prevNode: ASTNode | some(text, node, prevNode) | } """.trimMargin() ) } @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) fun `should skip val and var nodes between considered nodes`() { // Only text goes to propertyToUsages here lintMethod( """ | private fun collectAllExtensionFunctions(astNode: ASTNode): SimilarSignatures { | val text = "" | val node = astNode | val prevNode: ASTNode = astNode | some(text, node, prevNode) | } """.trimMargin() ) } @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) fun `shouldn't fail on double invocations`() { lintMethod( """ |fun bar() { | val x = foo()() | val x = foo()()() |} """.trimMargin() ) } @Test @Tag(LOCAL_VARIABLE_EARLY_DECLARATION) fun `shouldn't fail on semicolon`() { lintMethod( """ |fun bar() { | var a = 0; | a++ |} """.trimMargin() ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/LongNumericalValuesSeparatedFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter3.LongNumericalValuesSeparatedRule import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames.LONG_NUMERICAL_VALUES_SEPARATED import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class LongNumericalValuesSeparatedFixTest : FixTestBase("test/paragraph3/long_numbers", ::LongNumericalValuesSeparatedRule) { private val rulesConfig: List = listOf( RulesConfig(Warnings.LONG_NUMERICAL_VALUES_SEPARATED.name, true, mapOf("maxNumberLength" to "5", "maxBlockLength" to "3")) ) @Test @Tag(LONG_NUMERICAL_VALUES_SEPARATED) fun `should add delimiters`() { fixAndCompare("LongNumericalValuesExpected.kt", "LongNumericalValuesTest.kt", rulesConfig) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/LongNumericalValuesSeparatedWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter3.LongNumericalValuesSeparatedRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames.LONG_NUMERICAL_VALUES_SEPARATED import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class LongNumericalValuesSeparatedWarnTest : LintTestBase(::LongNumericalValuesSeparatedRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${LongNumericalValuesSeparatedRule.NAME_ID}" private val rulesConfig: List = listOf( RulesConfig(Warnings.LONG_NUMERICAL_VALUES_SEPARATED.name, true, mapOf("maxNumberLength" to "2")) ) @Test @Tag(LONG_NUMERICAL_VALUES_SEPARATED) fun `check properties test bad`() { lintMethod( """ |fun foo() { | val oneMillion = 100000000000 | val creditCardNumber = 1234567890123456L | val socialSecurityNumber = 999999999L | val hexBytes = 0xFFECDE5E | val hexBytes2 = 0xF | val bytes = 0b110100110_01101001_10010100_10010010 | val flo = 192.312341341344355345 | val flo2 = 192.31234134134435_5345 | val hundred = 100 |} """.trimMargin(), DiktatError(2, 21, ruleId, "${Warnings.LONG_NUMERICAL_VALUES_SEPARATED.warnText()} 100000000000", true), DiktatError(3, 27, ruleId, "${Warnings.LONG_NUMERICAL_VALUES_SEPARATED.warnText()} 1234567890123456L", true), DiktatError(4, 31, ruleId, "${Warnings.LONG_NUMERICAL_VALUES_SEPARATED.warnText()} 999999999L", true), DiktatError(5, 19, ruleId, "${Warnings.LONG_NUMERICAL_VALUES_SEPARATED.warnText()} 0xFFECDE5E", true), DiktatError(7, 16, ruleId, "${Warnings.LONG_NUMERICAL_VALUES_SEPARATED.warnText()} this block is too long 110100110", false), DiktatError(7, 16, ruleId, "${Warnings.LONG_NUMERICAL_VALUES_SEPARATED.warnText()} this block is too long 01101001", false), DiktatError(7, 16, ruleId, "${Warnings.LONG_NUMERICAL_VALUES_SEPARATED.warnText()} this block is too long 10010100", false), DiktatError(7, 16, ruleId, "${Warnings.LONG_NUMERICAL_VALUES_SEPARATED.warnText()} this block is too long 10010010", false), DiktatError(8, 14, ruleId, "${Warnings.LONG_NUMERICAL_VALUES_SEPARATED.warnText()} 192.312341341344355345", true), DiktatError(9, 15, ruleId, "${Warnings.LONG_NUMERICAL_VALUES_SEPARATED.warnText()} this block is too long 31234134134435", false), DiktatError(9, 15, ruleId, "${Warnings.LONG_NUMERICAL_VALUES_SEPARATED.warnText()} this block is too long 5345", false) ) } @Test @Tag(LONG_NUMERICAL_VALUES_SEPARATED) fun `check properties test good`() { lintMethod( """ |fun foo() { | val oneMillion = 1_000_000_000_000 | val creditCardNumber = 1_234_567_890_123_456L | val socialSecurityNumber = 999_999_999L | val hexBytes = 0xFF_EC_DE_5E | val bytes = 0b11_010_010_011_010_011_001_010_010_010_010 | val flo = 192.312_341_341_345 | val ten = 10 |} """.trimMargin() ) } @Test @Tag(LONG_NUMERICAL_VALUES_SEPARATED) fun `check properties test bad 2`() { lintMethod( """ |fun foo() { | val oneMillion = 100 | val creditCardNumber = 1234566L | val socialSecurityNumber = 999L | val hexBytes = 0xFFE | val bytes = 0b110100 | val flo = 192.312 |} """.trimMargin(), DiktatError(2, 21, ruleId, "${Warnings.LONG_NUMERICAL_VALUES_SEPARATED.warnText()} 100", true), DiktatError(3, 27, ruleId, "${Warnings.LONG_NUMERICAL_VALUES_SEPARATED.warnText()} 1234566L", true), DiktatError(4, 31, ruleId, "${Warnings.LONG_NUMERICAL_VALUES_SEPARATED.warnText()} 999L", true), DiktatError(5, 19, ruleId, "${Warnings.LONG_NUMERICAL_VALUES_SEPARATED.warnText()} 0xFFE", true), DiktatError(6, 16, ruleId, "${Warnings.LONG_NUMERICAL_VALUES_SEPARATED.warnText()} 0b110100", true), DiktatError(7, 14, ruleId, "${Warnings.LONG_NUMERICAL_VALUES_SEPARATED.warnText()} 192.312", true), rulesConfigList = rulesConfig ) } @Test @Tag(LONG_NUMERICAL_VALUES_SEPARATED) fun `check func params test good`() { lintMethod( """ |fun foo(val one = 100_000_000) { | |} """.trimMargin() ) } @Test @Tag(LONG_NUMERICAL_VALUES_SEPARATED) fun `check func params test bad`() { lintMethod( """ |fun foo(val one = 100000000) { | |} """.trimMargin(), DiktatError(1, 19, ruleId, "${Warnings.LONG_NUMERICAL_VALUES_SEPARATED.warnText()} 100000000", true) ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/MagicNumberRuleWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.api.DiktatError import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.MAGIC_NUMBER import com.saveourtool.diktat.ruleset.rules.chapter3.MagicNumberRule import com.saveourtool.diktat.util.LintTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class MagicNumberRuleWarnTest : LintTestBase(::MagicNumberRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${MagicNumberRule.NAME_ID}" private val rulesConfigIgnoreNone: List = listOf( RulesConfig( MAGIC_NUMBER.name, true, mapOf( "ignoreHashCodeFunction" to "false", "ignorePropertyDeclaration" to "false", "ignoreLocalVariableDeclaration" to "false", "ignoreValueParameter" to "false", "ignoreConstantDeclaration" to "false", "ignoreCompanionObjectPropertyDeclaration" to "false", "ignoreEnums" to "false", "ignoreRanges" to "false", "ignoreExtensionFunctions" to "false", "ignorePairsCreatedUsingTo" to "false")) ) private val rulesConfigIgnoreNumbers: List = listOf( RulesConfig( MAGIC_NUMBER.name, true, mapOf( "ignoreNumbers" to "50,-240, 128L, -3.5f, 4, 11UL", "ignoreHashCodeFunction" to "false", "ignorePropertyDeclaration" to "false", "ignoreLocalVariableDeclaration" to "false", "ignoreValueParameter" to "false", "ignoreConstantDeclaration" to "false", "ignoreCompanionObjectPropertyDeclaration" to "false", "ignoreEnums" to "false", "ignoreRanges" to "false", "ignoreExtensionFunctions" to "false", "ignorePairsCreatedUsingTo" to "false")) ) private val rulesConfigIgnoreHashCodeFunction: List = listOf( RulesConfig( MAGIC_NUMBER.name, true, mapOf( "ignoreHashCodeFunction" to "true", "ignorePropertyDeclaration" to "false", "ignoreLocalVariableDeclaration" to "false", "ignoreValueParameter" to "false", "ignoreConstantDeclaration" to "false", "ignoreCompanionObjectPropertyDeclaration" to "false", "ignoreEnums" to "false", "ignoreRanges" to "false", "ignoreExtensionFunctions" to "false", "ignorePairsCreatedUsingTo" to "false")) ) private val rulesConfigIgnorePropertyDeclaration: List = listOf( RulesConfig( MAGIC_NUMBER.name, true, mapOf( "ignoreHashCodeFunction" to "false", "ignorePropertyDeclaration" to "true", "ignoreLocalVariableDeclaration" to "false", "ignoreValueParameter" to "false", "ignoreConstantDeclaration" to "false", "ignoreCompanionObjectPropertyDeclaration" to "false", "ignoreEnums" to "false", "ignoreRanges" to "false", "ignoreExtensionFunctions" to "false", "ignorePairsCreatedUsingTo" to "false")) ) private val rulesConfigIgnoreLocalVariableDeclaration: List = listOf( RulesConfig( MAGIC_NUMBER.name, true, mapOf( "ignoreHashCodeFunction" to "false", "ignorePropertyDeclaration" to "false", "ignoreLocalVariableDeclaration" to "true", "ignoreValueParameter" to "false", "ignoreConstantDeclaration" to "false", "ignoreCompanionObjectPropertyDeclaration" to "false", "ignoreEnums" to "false", "ignoreRanges" to "false", "ignoreExtensionFunctions" to "false", "ignorePairsCreatedUsingTo" to "false")) ) private val rulesConfigIgnoreValueParameter: List = listOf( RulesConfig( MAGIC_NUMBER.name, true, mapOf( "ignoreHashCodeFunction" to "false", "ignorePropertyDeclaration" to "false", "ignoreLocalVariableDeclaration" to "false", "ignoreValueParameter" to "true", "ignoreConstantDeclaration" to "false", "ignoreCompanionObjectPropertyDeclaration" to "false", "ignoreEnums" to "false", "ignoreRanges" to "false", "ignoreExtensionFunctions" to "false", "ignorePairsCreatedUsingTo" to "false")) ) private val rulesConfigIgnoreConstantDeclaration: List = listOf( RulesConfig( MAGIC_NUMBER.name, true, mapOf( "ignoreHashCodeFunction" to "false", "ignorePropertyDeclaration" to "false", "ignoreLocalVariableDeclaration" to "false", "ignoreValueParameter" to "false", "ignoreConstantDeclaration" to "true", "ignoreCompanionObjectPropertyDeclaration" to "false", "ignoreEnums" to "false", "ignoreRanges" to "false", "ignoreExtensionFunctions" to "false", "ignorePairsCreatedUsingTo" to "false")) ) private val rulesConfigIgnoreCompanionObjectPropertyDeclaration: List = listOf( RulesConfig( MAGIC_NUMBER.name, true, mapOf( "ignoreHashCodeFunction" to "false", "ignorePropertyDeclaration" to "false", "ignoreLocalVariableDeclaration" to "false", "ignoreValueParameter" to "false", "ignoreConstantDeclaration" to "false", "ignoreCompanionObjectPropertyDeclaration" to "true", "ignoreEnums" to "false", "ignoreRanges" to "false", "ignoreExtensionFunctions" to "false", "ignorePairsCreatedUsingTo" to "false")) ) private val rulesConfigIgnoreEnums: List = listOf( RulesConfig( MAGIC_NUMBER.name, true, mapOf( "ignoreHashCodeFunction" to "false", "ignorePropertyDeclaration" to "false", "ignoreLocalVariableDeclaration" to "false", "ignoreValueParameter" to "false", "ignoreConstantDeclaration" to "false", "ignoreCompanionObjectPropertyDeclaration" to "false", "ignoreEnums" to "true", "ignoreRanges" to "false", "ignoreExtensionFunctions" to "false", "ignorePairsCreatedUsingTo" to "false")) ) private val rulesConfigIgnoreRanges: List = listOf( RulesConfig( MAGIC_NUMBER.name, true, mapOf( "ignoreHashCodeFunction" to "false", "ignorePropertyDeclaration" to "false", "ignoreLocalVariableDeclaration" to "false", "ignoreValueParameter" to "false", "ignoreConstantDeclaration" to "false", "ignoreCompanionObjectPropertyDeclaration" to "false", "ignoreEnums" to "false", "ignoreRanges" to "true", "ignoreExtensionFunctions" to "false", "ignorePairsCreatedUsingTo" to "false")) ) private val rulesConfigIgnoreExtensionFunctions: List = listOf( RulesConfig( MAGIC_NUMBER.name, true, mapOf( "ignoreHashCodeFunction" to "false", "ignorePropertyDeclaration" to "false", "ignoreLocalVariableDeclaration" to "false", "ignoreValueParameter" to "false", "ignoreConstantDeclaration" to "false", "ignoreCompanionObjectPropertyDeclaration" to "false", "ignoreEnums" to "false", "ignoreRanges" to "false", "ignoreExtensionFunctions" to "true", "ignorePairsCreatedUsingTo" to "false")) ) private val rulesConfigIgnorePairsCreatedUsingTo: List = listOf( RulesConfig( MAGIC_NUMBER.name, true, mapOf( "ignoreHashCodeFunction" to "false", "ignorePropertyDeclaration" to "false", "ignoreLocalVariableDeclaration" to "false", "ignoreValueParameter" to "false", "ignoreConstantDeclaration" to "false", "ignoreCompanionObjectPropertyDeclaration" to "false", "ignoreEnums" to "false", "ignoreRanges" to "false", "ignoreExtensionFunctions" to "false", "ignorePairsCreatedUsingTo" to "true")) ) @Test @Tag(WarningNames.MAGIC_NUMBER) @Suppress("LongMethod") fun `check all`() { lintMethod( """ |fun f1oo() { | val a: Byte = 4 | val b = 128L | val e = 3.4f | val g = 4/3 | val h = 0U | val r = 1UL | val f = "qwe\$\{12\}hhe" |} | |@Override |fun hashCode(): Int { | return 32 |} | |val abc = 32 |var abc2 = 32 | |fun foo() { | val a = 3 | var a2 = 3 |} | |class TomlDecoder( | val elementsCount: Int = 100, | var elementsCount2: Int = 100 |) | |fun TomlDecoder(elementsCount: Int = 100) {} | |const val topLevel = 31 | |class A { | companion object { | val b = 3 | var b2 = 4 | } |} | |enum class A(b:Int) { | A(3), | B(4), | C(5), |} | |val tagLengthRange = 3..15 |var tagLengthRange2 = 3..15 | |fun Int.foo() = 3 | |val fg = abc to 3 |var fg2 = abc to 4 """.trimMargin(), DiktatError(2, 18, ruleId, "${MAGIC_NUMBER.warnText()} 4", false), DiktatError(3, 12, ruleId, "${MAGIC_NUMBER.warnText()} 128L", false), DiktatError(4, 12, ruleId, "${MAGIC_NUMBER.warnText()} 3.4f", false), DiktatError(5, 12, ruleId, "${MAGIC_NUMBER.warnText()} 4", false), DiktatError(5, 14, ruleId, "${MAGIC_NUMBER.warnText()} 3", false), DiktatError(13, 11, ruleId, "${MAGIC_NUMBER.warnText()} 32", false), DiktatError(16, 11, ruleId, "${MAGIC_NUMBER.warnText()} 32", false), DiktatError(17, 12, ruleId, "${MAGIC_NUMBER.warnText()} 32", false), DiktatError(20, 12, ruleId, "${MAGIC_NUMBER.warnText()} 3", false), DiktatError(21, 13, ruleId, "${MAGIC_NUMBER.warnText()} 3", false), DiktatError(25, 30, ruleId, "${MAGIC_NUMBER.warnText()} 100", false), DiktatError(26, 31, ruleId, "${MAGIC_NUMBER.warnText()} 100", false), DiktatError(29, 38, ruleId, "${MAGIC_NUMBER.warnText()} 100", false), DiktatError(31, 22, ruleId, "${MAGIC_NUMBER.warnText()} 31", false), DiktatError(35, 16, ruleId, "${MAGIC_NUMBER.warnText()} 3", false), DiktatError(36, 17, ruleId, "${MAGIC_NUMBER.warnText()} 4", false), DiktatError(41, 6, ruleId, "${MAGIC_NUMBER.warnText()} 3", false), DiktatError(42, 6, ruleId, "${MAGIC_NUMBER.warnText()} 4", false), DiktatError(43, 6, ruleId, "${MAGIC_NUMBER.warnText()} 5", false), DiktatError(46, 22, ruleId, "${MAGIC_NUMBER.warnText()} 3", false), DiktatError(46, 25, ruleId, "${MAGIC_NUMBER.warnText()} 15", false), DiktatError(47, 23, ruleId, "${MAGIC_NUMBER.warnText()} 3", false), DiktatError(47, 26, ruleId, "${MAGIC_NUMBER.warnText()} 15", false), DiktatError(49, 17, ruleId, "${MAGIC_NUMBER.warnText()} 3", false), DiktatError(51, 17, ruleId, "${MAGIC_NUMBER.warnText()} 3", false), DiktatError(52, 18, ruleId, "${MAGIC_NUMBER.warnText()} 4", false), rulesConfigList = rulesConfigIgnoreNone ) } @Test @Tag(WarningNames.MAGIC_NUMBER) fun `check ignore numbers`() { lintMethod( """ |fun f1oo() { | val m = -1 | var m2 = -1 | val a: Byte = 4 | var a2: Byte = 4 | val b = 0xff | var b2 = 0xff |} | |enum class A(b:Int) { | A(-240), | B(50), | C(3), |} """.trimMargin(), DiktatError(2, 13, ruleId, "${MAGIC_NUMBER.warnText()} -1", false), DiktatError(3, 14, ruleId, "${MAGIC_NUMBER.warnText()} -1", false), DiktatError(6, 12, ruleId, "${MAGIC_NUMBER.warnText()} 0xff", false), DiktatError(7, 13, ruleId, "${MAGIC_NUMBER.warnText()} 0xff", false), DiktatError(13, 6, ruleId, "${MAGIC_NUMBER.warnText()} 3", false), rulesConfigList = rulesConfigIgnoreNumbers ) } @Test @Tag(WarningNames.MAGIC_NUMBER) fun `check ignore hash code function`() { lintMethod( """ |@Override |fun hashCode(): Int { | return 32 |} """.trimMargin(), rulesConfigList = rulesConfigIgnoreHashCodeFunction ) } @Test @Tag(WarningNames.MAGIC_NUMBER) fun `check ignore property declaration`() { lintMethod( """ |val abc = 32 |var abc2 = 32 """.trimMargin(), rulesConfigList = rulesConfigIgnorePropertyDeclaration ) } @Test @Tag(WarningNames.MAGIC_NUMBER) fun `check ignore local variable declaration`() { lintMethod( """ |fun foo() { | val a = 3 | var a2 = 3 |} """.trimMargin(), rulesConfigList = rulesConfigIgnoreLocalVariableDeclaration ) } @Test @Tag(WarningNames.MAGIC_NUMBER) fun `check ignore value parameter`() { lintMethod( """ |class TomlDecoder( | val elementsCount: Int = 100, | var elementsCount2: Int = 100 |) | |fun TomlDecoder(elementsCount: Int = 100) {} """.trimMargin(), rulesConfigList = rulesConfigIgnoreValueParameter ) } @Test @Tag(WarningNames.MAGIC_NUMBER) fun `check ignore constant declaration`() { lintMethod( """ |const val topLevel = 31 """.trimMargin(), rulesConfigList = rulesConfigIgnoreConstantDeclaration ) } @Test @Tag(WarningNames.MAGIC_NUMBER) fun `check ignore companion object property declaration`() { lintMethod( """ |class A { | companion object { | val b = 3 | var b2 = 4 | } |} """.trimMargin(), rulesConfigList = rulesConfigIgnoreCompanionObjectPropertyDeclaration ) } @Test @Tag(WarningNames.MAGIC_NUMBER) fun `check ignore enums`() { lintMethod( """ |enum class A(b:Int) { | A(3), | B(4), | C(5), |} """.trimMargin(), rulesConfigList = rulesConfigIgnoreEnums ) } @Test @Tag(WarningNames.MAGIC_NUMBER) fun `check ignore ranges`() { lintMethod( """ |val tagLengthRange = 3..15 |var tagLengthRange2 = 3..15 """.trimMargin(), rulesConfigList = rulesConfigIgnoreRanges ) } @Test @Tag(WarningNames.MAGIC_NUMBER) fun `check ignore extension functions`() { lintMethod( """ |fun Int.foo() = 3 """.trimMargin(), rulesConfigList = rulesConfigIgnoreExtensionFunctions ) } @Test @Tag(WarningNames.MAGIC_NUMBER) fun `check ignore pairs created using 'to'`() { lintMethod( """ |val fg = abc to 3 |var fg2 = abc to 4 """.trimMargin(), rulesConfigList = rulesConfigIgnorePairsCreatedUsingTo ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/MultipleModifiersSequenceFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.ruleset.rules.chapter3.MultipleModifiersSequence import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class MultipleModifiersSequenceFixTest : FixTestBase("test/paragraph3/multiple_modifiers", ::MultipleModifiersSequence) { @Test @Tag(WarningNames.WRONG_MULTIPLE_MODIFIERS_ORDER) fun `should fix modifiers order`() { fixAndCompare("ModifierExpected.kt", "ModifierTest.kt") } @Test @Tag(WarningNames.WRONG_MULTIPLE_MODIFIERS_ORDER) fun `should fix annotation order`() { fixAndCompare("AnnotationExpected.kt", "AnnotationTest.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/MultipleModifiersSequenceWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings.WRONG_MULTIPLE_MODIFIERS_ORDER import com.saveourtool.diktat.ruleset.rules.chapter3.MultipleModifiersSequence import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class MultipleModifiersSequenceWarnTest : LintTestBase(::MultipleModifiersSequence) { private val ruleId = "$DIKTAT_RULE_SET_ID:${MultipleModifiersSequence.NAME_ID}" @Test @Tag(WarningNames.WRONG_MULTIPLE_MODIFIERS_ORDER) fun `check wrong order modifier in fun and variable with annotation`() { lintMethod( """ |@Annotation |final public fun foo() { | lateinit open protected var a: List |} """.trimMargin(), DiktatError(2, 1, ruleId, "${WRONG_MULTIPLE_MODIFIERS_ORDER.warnText()} final should be on position 2, but is on position 1", true), DiktatError(2, 7, ruleId, "${WRONG_MULTIPLE_MODIFIERS_ORDER.warnText()} public should be on position 1, but is on position 2", true), DiktatError(3, 4, ruleId, "${WRONG_MULTIPLE_MODIFIERS_ORDER.warnText()} lateinit should be on position 3, but is on position 1", true), DiktatError(3, 18, ruleId, "${WRONG_MULTIPLE_MODIFIERS_ORDER.warnText()} protected should be on position 1, but is on position 3", true) ) } @Test @Tag(WarningNames.WRONG_MULTIPLE_MODIFIERS_ORDER) fun `check correct order modifier in fun and variable and without`() { lintMethod( """ |public final fun foo() { | protected open lateinit var a: List |} | |fun goo() { | |} """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_MULTIPLE_MODIFIERS_ORDER) fun `check wrong order another modifier in fun`() { lintMethod( """ |inline tailrec public fun qwe(vararg text: String) {} | |inline suspend fun f(crossinline body: () -> Unit) {} | |inline fun < reified T> membersOf() = T::class.members """.trimMargin(), DiktatError(1, 1, ruleId, "${WRONG_MULTIPLE_MODIFIERS_ORDER.warnText()} inline should be on position 3, but is on position 1", true), DiktatError(1, 16, ruleId, "${WRONG_MULTIPLE_MODIFIERS_ORDER.warnText()} public should be on position 1, but is on position 3", true), DiktatError(3, 1, ruleId, "${WRONG_MULTIPLE_MODIFIERS_ORDER.warnText()} inline should be on position 2, but is on position 1", true), DiktatError(3, 8, ruleId, "${WRONG_MULTIPLE_MODIFIERS_ORDER.warnText()} suspend should be on position 1, but is on position 2", true) ) } @Test @Tag(WarningNames.WRONG_MULTIPLE_MODIFIERS_ORDER) fun `check wrong order modifier in class`() { lintMethod( """ |enum public class Q {} | |data protected class Counter(val dayIndex: Int) { | operator suspend fun plus(increment: Int): Counter { | return Counter(dayIndex + increment) | } |} """.trimMargin(), DiktatError(1, 1, ruleId, "${WRONG_MULTIPLE_MODIFIERS_ORDER.warnText()} enum should be on position 2, but is on position 1", true), DiktatError(1, 6, ruleId, "${WRONG_MULTIPLE_MODIFIERS_ORDER.warnText()} public should be on position 1, but is on position 2", true), DiktatError(3, 1, ruleId, "${WRONG_MULTIPLE_MODIFIERS_ORDER.warnText()} data should be on position 2, but is on position 1", true), DiktatError(3, 6, ruleId, "${WRONG_MULTIPLE_MODIFIERS_ORDER.warnText()} protected should be on position 1, but is on position 2", true), DiktatError(4, 4, ruleId, "${WRONG_MULTIPLE_MODIFIERS_ORDER.warnText()} operator should be on position 2, but is on position 1", true), DiktatError(4, 13, ruleId, "${WRONG_MULTIPLE_MODIFIERS_ORDER.warnText()} suspend should be on position 1, but is on position 2", true) ) } @Test @Tag(WarningNames.WRONG_MULTIPLE_MODIFIERS_ORDER) fun `check wrong annotation order`() { lintMethod( """ |public @Annotation final fun foo() { |} """.trimMargin(), DiktatError(1, 8, ruleId, "${WRONG_MULTIPLE_MODIFIERS_ORDER.warnText()} @Annotation annotation should be before all modifiers", true) ) } @Test @Tag(WarningNames.WRONG_MULTIPLE_MODIFIERS_ORDER) fun `check correct order modifier for value`() { lintMethod( """ |public value class Foo() { |} """.trimMargin(), ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/NullableTypeRuleFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.ruleset.rules.chapter3.NullableTypeRule import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class NullableTypeRuleFixTest : FixTestBase("test/paragraph3/nullable", ::NullableTypeRule) { @Test @Tag(WarningNames.NULLABLE_PROPERTY_TYPE) fun `should fix primitive types`() { fixAndCompare("NullPrimitiveExpected.kt", "NullPrimitiveTest.kt") } @Test @Tag(WarningNames.NULLABLE_PROPERTY_TYPE) fun `should fix collections`() { fixAndCompare("CollectionExpected.kt", "CollectionTest.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/NullableTypeRuleWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings.NULLABLE_PROPERTY_TYPE import com.saveourtool.diktat.ruleset.rules.chapter3.NullableTypeRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class NullableTypeRuleWarnTest : LintTestBase(::NullableTypeRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${NullableTypeRule.NAME_ID}" @Test @Tag(WarningNames.NULLABLE_PROPERTY_TYPE) fun `check simple property`() { lintMethod( """ |val a: List? = null |val a: Int? = null |val b: Double? = null |val c: String? = null |val a: MutableList? = null """.trimMargin(), DiktatError(1, 21, ruleId, "${NULLABLE_PROPERTY_TYPE.warnText()} initialize explicitly", true), DiktatError(2, 15, ruleId, "${NULLABLE_PROPERTY_TYPE.warnText()} initialize explicitly", true), DiktatError(3, 18, ruleId, "${NULLABLE_PROPERTY_TYPE.warnText()} initialize explicitly", true), DiktatError(4, 18, ruleId, "${NULLABLE_PROPERTY_TYPE.warnText()} initialize explicitly", true), DiktatError(5, 28, ruleId, "${NULLABLE_PROPERTY_TYPE.warnText()} initialize explicitly", true) ) } @Test @Tag(WarningNames.NULLABLE_PROPERTY_TYPE) fun `check property in object`() { lintMethod( """ |class A { | companion object { | val a: Int? = null | val b: Int? = 0 | val c: Boolean? = true | } |} """.trimMargin(), DiktatError(3, 22, ruleId, "${NULLABLE_PROPERTY_TYPE.warnText()} initialize explicitly", true), DiktatError(4, 15, ruleId, "${NULLABLE_PROPERTY_TYPE.warnText()} don't use nullable type", false), DiktatError(5, 15, ruleId, "${NULLABLE_PROPERTY_TYPE.warnText()} don't use nullable type", false) ) } @Test @Tag(WarningNames.NULLABLE_PROPERTY_TYPE) fun `check nullable type with initialize`() { lintMethod( """ |class A { | companion object { | val a: Int? = 0 | val b: Int? = null | val c: Boolean? = false | } |} """.trimMargin(), DiktatError(3, 15, ruleId, "${NULLABLE_PROPERTY_TYPE.warnText()} don't use nullable type", false), DiktatError(4, 22, ruleId, "${NULLABLE_PROPERTY_TYPE.warnText()} initialize explicitly", true), DiktatError(5, 15, ruleId, "${NULLABLE_PROPERTY_TYPE.warnText()} don't use nullable type", false) ) } @Test @Tag(WarningNames.NULLABLE_PROPERTY_TYPE) fun `d nullable type with initialize`() { lintMethod( """ |class A { | val rulesConfigList: List? = RulesConfigReader(javaClass.classLoader).readResource("src/test/resources/test-rules-config.yml") | val q: Int? = foo() | val e: A.Q? = null |} """.trimMargin(), DiktatError(4, 18, ruleId, "${NULLABLE_PROPERTY_TYPE.warnText()} initialize explicitly", false) ) } @Test @Tag(WarningNames.TYPE_ALIAS) fun `should trigger on collection factory`() { lintMethod( """ | val q: List? = emptyList() | val w: List> = emptyList>() | val c: Set? = setOf() | val d: List = emptyList() """.trimMargin(), DiktatError(1, 9, ruleId, "${NULLABLE_PROPERTY_TYPE.warnText()} don't use nullable type", false), DiktatError(3, 9, ruleId, "${NULLABLE_PROPERTY_TYPE.warnText()} don't use nullable type", false) ) } @Test @Tag(WarningNames.TYPE_ALIAS) fun `shouldn't trigger`() { lintMethod( """ | val superClassName: String? = node | .getFirstChildWithType(ElementType.SUPER_TYPE_LIST) | ?.findLeafWithSpecificType(TYPE_REFERENCE) | ?.text | | private val rulesConfigList: List? = rulesConfigList ?: RulesConfigReader(javaClass.classLoader).readResource("diktat-analysis.yml") """.trimMargin()) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/PreviewAnnotationFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.ruleset.rules.chapter3.PreviewAnnotationRule import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class PreviewAnnotationFixTest : FixTestBase("test/paragraph3/preview_annotation", ::PreviewAnnotationRule) { @Test @Tag(WarningNames.PREVIEW_ANNOTATION) fun `should add private modifier`() { fixAndCompare("PreviewAnnotationPrivateModifierExpected.kt", "PreviewAnnotationPrivateModifierTest.kt") } @Test @Tag(WarningNames.PREVIEW_ANNOTATION) fun `should add Preview suffix`() { fixAndCompare("PreviewAnnotationMethodNameExpected.kt", "PreviewAnnotationMethodNameTest.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/PreviewAnnotationWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.api.DiktatError import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter3.PreviewAnnotationRule import com.saveourtool.diktat.util.LintTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class PreviewAnnotationWarnTest : LintTestBase(::PreviewAnnotationRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${PreviewAnnotationRule.NAME_ID}" @Test @Tag(WarningNames.PREVIEW_ANNOTATION) fun `no warn`() { lintMethod( """ |@Preview |@Composable |private fun BannerPreview() {} """.trimMargin() ) } @Test @Tag(WarningNames.PREVIEW_ANNOTATION) fun `method is not private`() { lintMethod( """ |@Preview |@Composable |fun BannerPreview() {} """.trimMargin(), DiktatError(1, 1, ruleId, "${Warnings.PREVIEW_ANNOTATION.warnText()} BannerPreview method should be private", true), ) } @Test @Tag(WarningNames.PREVIEW_ANNOTATION) fun `method has no preview suffix`() { lintMethod( """ |@Preview |@Composable |private fun Banner() {} """.trimMargin(), DiktatError(1, 1, ruleId, "${Warnings.PREVIEW_ANNOTATION.warnText()} Banner method should has `Preview` suffix", true), ) } @Test @Tag(WarningNames.PREVIEW_ANNOTATION) fun `method has no preview suffix and is not private`() { lintMethod( """ |@Preview |@Composable |fun Banner() {} """.trimMargin(), DiktatError(1, 1, ruleId, "${Warnings.PREVIEW_ANNOTATION.warnText()} Banner method should be private", true), DiktatError(1, 1, ruleId, "${Warnings.PREVIEW_ANNOTATION.warnText()} Banner method should has `Preview` suffix", true), ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/RangeConventionalRuleFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.ruleset.rules.chapter3.RangeConventionalRule import com.saveourtool.diktat.util.FixTestBase import org.junit.jupiter.api.Test class RangeConventionalRuleFixTest : FixTestBase("test/paragraph3/range", ::RangeConventionalRule) { @Test fun `should fix with until`() { fixAndCompare("RangeToUntilExpected.kt", "RangeToUntilTest.kt") } @Test fun `should fix with rangeTo`() { fixAndCompare("RangeToExpected.kt", "RangeToTest.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/RangeConventionalRuleWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter3.RangeConventionalRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import org.junit.jupiter.api.Test class RangeConventionalRuleWarnTest : LintTestBase(::RangeConventionalRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${RangeConventionalRule.NAME_ID}" private val rulesConfigRangeRule: List = listOf( RulesConfig( Warnings.CONVENTIONAL_RANGE.name, true, mapOf("isRangeToIgnore" to "true")) ) @Test fun `check simple examples with until`() { lintMethod( """ |fun foo() { | for (i in 1..(4 - 1)) print(i) | for (i in 1..(b - 1)) print(i) | for (i in ((1 .. ((4 - 1))))) print(i) | for (i in 4 downTo 1) print(i) | for (i in 1..4) print(i) | for (i in 1..4 step 2) print(i) | for (i in 4 downTo 1 step 3) print(i) | if (6 in (1..10) && true) {} | for (i in 1..(4 - 1) step 3) print(i) |} """.trimMargin(), DiktatError(2, 15, ruleId, "${Warnings.CONVENTIONAL_RANGE.warnText()} replace `..` with `until`: 1..(4 - 1)", true), DiktatError(3, 15, ruleId, "${Warnings.CONVENTIONAL_RANGE.warnText()} replace `..` with `until`: 1..(b - 1)", true), DiktatError(4, 17, ruleId, "${Warnings.CONVENTIONAL_RANGE.warnText()} replace `..` with `until`: 1 .. ((4 - 1))", true), DiktatError(10, 15, ruleId, "${Warnings.CONVENTIONAL_RANGE.warnText()} replace `..` with `until`: 1..(4 - 1)", true) ) } @Test fun `check simple examples with rangeTo`() { lintMethod( """ |fun foo() { | val num = 1 | val w = num.rangeTo(num, num) | val q = 1..5 | val w = num.rangeTo(num) |} """.trimMargin(), DiktatError(5, 13, ruleId, "${Warnings.CONVENTIONAL_RANGE.warnText()} replace `rangeTo` with `..`: num.rangeTo(num)", true) ) } @Test fun `check simple examples with rangeTo with config`() { lintMethod( """ |fun foo() { | val w = num.rangeTo(num, num) | val w = num.rangeTo(num) |} """.trimMargin(), rulesConfigList = rulesConfigRangeRule ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/SingleLineStatementsRuleFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.ruleset.rules.chapter3.SingleLineStatementsRule import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class SingleLineStatementsRuleFixTest : FixTestBase("test/paragraph3/statement", ::SingleLineStatementsRule) { @Test @Tag(WarningNames.MORE_THAN_ONE_STATEMENT_PER_LINE) fun `should make one statement per line`() { fixAndCompare("StatementExpected.kt", "StatementTest.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/SingleLineStatementsRuleWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings.MORE_THAN_ONE_STATEMENT_PER_LINE import com.saveourtool.diktat.ruleset.rules.chapter3.SingleLineStatementsRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class SingleLineStatementsRuleWarnTest : LintTestBase(::SingleLineStatementsRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${SingleLineStatementsRule.NAME_ID}" @Test @Tag(WarningNames.MORE_THAN_ONE_STATEMENT_PER_LINE) fun `check two statement per line`() { lintMethod( """ |import com.pinterest.ktlint.core.KtLint; import com.pinterest.ktlint.core.LintError | |fun foo() { | if (x < -5) { | goo(); hoo() | } | else { | } | | when(x) { | 1 -> println(1) | else -> println("3;5") | } | val a = 5; val b = 10 | println(1); println(1) |} """.trimMargin(), DiktatError(1, 40, ruleId, "${MORE_THAN_ONE_STATEMENT_PER_LINE.warnText()} import com.pinterest.ktlint.core.KtLint; import com.pinterest.ktlint.core.LintError", true), DiktatError(5, 13, ruleId, "${MORE_THAN_ONE_STATEMENT_PER_LINE.warnText()} goo(); hoo()", true), DiktatError(14, 14, ruleId, "${MORE_THAN_ONE_STATEMENT_PER_LINE.warnText()} val a = 5; val b = 10", true), DiktatError(15, 15, ruleId, "${MORE_THAN_ONE_STATEMENT_PER_LINE.warnText()} println(1); println(1)", true) ) } @Test @Tag(WarningNames.MORE_THAN_ONE_STATEMENT_PER_LINE) fun `check two statement in one line without space`() { lintMethod( """ |fun foo() { | val a = 5;val b = 10 | println(1);println(1) |} """.trimMargin(), DiktatError(2, 14, ruleId, "${MORE_THAN_ONE_STATEMENT_PER_LINE.warnText()} val a = 5;val b = 10", true), DiktatError(3, 15, ruleId, "${MORE_THAN_ONE_STATEMENT_PER_LINE.warnText()} println(1);println(1)", true) ) } @Test @Tag(WarningNames.MORE_THAN_ONE_STATEMENT_PER_LINE) fun `check if expression with semicolon and else block in one line`() { lintMethod( """ |fun foo() { | if (x > 0){ | goo() | }; else { print(123) } |} """.trimMargin(), DiktatError(4, 5, ruleId, "${MORE_THAN_ONE_STATEMENT_PER_LINE.warnText()} }; else { print(123) }", true) ) } @Test @Tag(WarningNames.MORE_THAN_ONE_STATEMENT_PER_LINE) fun `check correct test without more than one statement`() { lintMethod( """ |import com.pinterest.ktlint.core.KtLint | |fun foo() { | if (x < -5) { | goo(); | } | else { | } | | when(x) { | 1 -> println(1) | else -> println("3;5") | } | val a = 5 | println(1) |} """.trimMargin() ) } @Test @Tag(WarningNames.MORE_THAN_ONE_STATEMENT_PER_LINE) fun `check semicolon with enum class expression`() { lintMethod( """ |enum class ProtocolState { | WAITING { | override fun signal() = TALKING | }, | TALKING { | override fun signal() = WAITING | }; abstract fun signal(): ProtocolState |} """.trimMargin(), DiktatError(7, 5, ruleId, "${MORE_THAN_ONE_STATEMENT_PER_LINE.warnText()} }; abstract fun signal(): ProtocolState", true) ) } @Test @Tag(WarningNames.MORE_THAN_ONE_STATEMENT_PER_LINE) fun `check if expression with two wrong semincolon`() { lintMethod( """ |fun foo() { | if(x > 0) { | if ( y> 0){ | ji() | }; gt() | }; gr() |} """.trimMargin(), DiktatError(5, 9, ruleId, "${MORE_THAN_ONE_STATEMENT_PER_LINE.warnText()} }; gt()", true), DiktatError(6, 5, ruleId, "${MORE_THAN_ONE_STATEMENT_PER_LINE.warnText()} }; gr()", true) ) } @Test @Tag(WarningNames.MORE_THAN_ONE_STATEMENT_PER_LINE) fun `check semicolon in the beginning of the line`() { lintMethod( """ |fun foo() { | ; grr() |} """.trimMargin(), DiktatError(2, 4, ruleId, "${MORE_THAN_ONE_STATEMENT_PER_LINE.warnText()} ; grr()", true) ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/SortRuleFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter3.SortRule import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class SortRuleFixTest : FixTestBase("test/paragraph3/sort_error", ::SortRule) { private val rulesConfigSortEnum: List = listOf( RulesConfig(Warnings.WRONG_DECLARATIONS_ORDER.name, true, mapOf("sortEnum" to "true")) ) private val rulesConfigSortProperty: List = listOf( RulesConfig(Warnings.WRONG_DECLARATIONS_ORDER.name, true, mapOf("sortProperty" to "true")) ) @Test @Tag(WarningNames.WRONG_DECLARATIONS_ORDER) fun `should fix enum order`() { fixAndCompare("EnumSortExpected.kt", "EnumSortTest.kt", rulesConfigSortEnum) } @Test @Tag(WarningNames.WRONG_DECLARATIONS_ORDER) fun `should fix constants order`() { fixAndCompare("ConstantsExpected.kt", "ConstantsTest.kt", rulesConfigSortProperty) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/SortRuleWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.WRONG_DECLARATIONS_ORDER import com.saveourtool.diktat.ruleset.rules.chapter3.SortRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class SortRuleWarnTest : LintTestBase(::SortRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${SortRule.NAME_ID}" private val rulesConfigNotSortEnum: List = listOf( RulesConfig(WRONG_DECLARATIONS_ORDER.name, true, mapOf("sortEnum" to "false")) ) private val rulesConfigNotSortProperty: List = listOf( RulesConfig(WRONG_DECLARATIONS_ORDER.name, true, mapOf("sortProperty" to "false")) ) private val rulesConfigNotSortBoth: List = listOf( RulesConfig(WRONG_DECLARATIONS_ORDER.name, true, mapOf("sortProperty" to "false", "sortEnum" to "false")) ) @Test @Tag(WarningNames.WRONG_DECLARATIONS_ORDER) fun `check simple correct enum`() { lintMethod( """ |enum class Alph { | A, | B, | C, | D, | ; |} """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_DECLARATIONS_ORDER) fun `check simple wrong enum`() { lintMethod( """ |enum class Alph { | D, | C, | A, | B, | ; |} """.trimMargin(), DiktatError(1, 17, ruleId, "${WRONG_DECLARATIONS_ORDER.warnText()} enum entries order is incorrect", true) ) } @Test @Tag(WarningNames.WRONG_DECLARATIONS_ORDER) fun `check correct enum`() { lintMethod( """ |enum class ENUM { | BLUE(0x0000FF), | GREEN(0x00FF00), | RED(0xFF0000), | ; |} """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_DECLARATIONS_ORDER) fun `check wrong enum without semicolon`() { lintMethod( """ |enum class ENUM { | GREEN(0x00FF00), | RED(0xFF0000), | BLUE(0x0000FF), |} """.trimMargin(), DiktatError(1, 17, ruleId, "${WRONG_DECLARATIONS_ORDER.warnText()} enum entries order is incorrect", true) ) } @Test @Tag(WarningNames.WRONG_DECLARATIONS_ORDER) fun `check wrong enum without semicolon and last comma`() { lintMethod( """ |enum class ENUM { | GREEN(0x00FF00), | RED(0xFF0000), | BLUE(0x0000FF) |} """.trimMargin(), DiktatError(1, 17, ruleId, "${WRONG_DECLARATIONS_ORDER.warnText()} enum entries order is incorrect", true) ) } @Test @Tag(WarningNames.WRONG_DECLARATIONS_ORDER) fun `check correct enum without semicolon and last comma`() { lintMethod( """ |enum class ENUM { | BLUE(0x0000FF), | GREEN(0x00FF00), | RED(0xFF0000), |} """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_DECLARATIONS_ORDER) fun `check wrong enum with fun`() { lintMethod( """ |enum class Warnings { | WAITING { | override fun signal() = TALKING | }, | TALKING { | override fun signal() = TALKING | }, | ; | abstract fun signal(): ProtocolState |} """.trimMargin(), DiktatError(1, 21, ruleId, "${WRONG_DECLARATIONS_ORDER.warnText()} enum entries order is incorrect", true) ) } @Test @Tag(WarningNames.WRONG_DECLARATIONS_ORDER) fun `check wrong enum with fun but with config`() { lintMethod( """ |enum class Warnings { | WAITING { | override fun signal() = TALKING | }, | TALKING { | override fun signal() = TALKING | }, | ; | abstract fun signal(): ProtocolState |} """.trimMargin(), rulesConfigList = rulesConfigNotSortEnum ) } @Test @Tag(WarningNames.WRONG_DECLARATIONS_ORDER) fun `check wrong properties between non conts`() { lintMethod( """ |class A { | companion object { | private val log = "Log" | private const val B = 4 | private const val A = 5 | private val SIMPLE_VALUE = listOf(IDENTIFIER, WHITE_SPACE, COMMA, SEMICOLON) | private const val A = 5 | } |} """.trimMargin(), DiktatError(4, 8, ruleId, "${WRONG_DECLARATIONS_ORDER.warnText()} constant properties inside companion object order is incorrect", true) ) } @Test @Tag(WarningNames.WRONG_DECLARATIONS_ORDER) fun `check wrong properties between non const more than one group`() { lintMethod( """ |class A { | companion object { | private val log = "Log" | private const val B = 4 | private const val A = 5 | private val SIMPLE_VALUE = listOf(IDENTIFIER, WHITE_SPACE, COMMA, SEMICOLON) | private const val Daa = 5 | private const val Da = 5 | private const val Db = 5 | } |} """.trimMargin(), DiktatError(4, 8, ruleId, "${WRONG_DECLARATIONS_ORDER.warnText()} constant properties inside companion object order is incorrect", true), DiktatError(7, 8, ruleId, "${WRONG_DECLARATIONS_ORDER.warnText()} constant properties inside companion object order is incorrect", true) ) } @Test @Tag(WarningNames.WRONG_DECLARATIONS_ORDER) fun `check wrong properties between non const more than one group only one`() { lintMethod( """ |class A { | companion object { | private val log = "Log" | private const val A = 4 | private const val D = 5 | private val SIMPLE_VALUE = listOf(IDENTIFIER, WHITE_SPACE, COMMA, SEMICOLON) | private const val Daa = 5 | private const val Da = 5 | private const val Db = 5 | } |} """.trimMargin(), DiktatError(7, 8, ruleId, "${WRONG_DECLARATIONS_ORDER.warnText()} constant properties inside companion object order is incorrect", true) ) } @Test @Tag(WarningNames.WRONG_DECLARATIONS_ORDER) fun `check wrong properties but with config`() { lintMethod( """ |class A { | companion object { | private val log = "Log" | private const val A = 4 | private const val D = 5 | private val SIMPLE_VALUE = listOf(IDENTIFIER, WHITE_SPACE, COMMA, SEMICOLON) | private const val Daa = 5 | private const val Da = 5 | private const val Db = 5 | } |} """.trimMargin(), rulesConfigList = rulesConfigNotSortProperty ) } @Test @Tag(WarningNames.WRONG_DECLARATIONS_ORDER) fun `check wrong properties but with both config`() { lintMethod( """ |class A { | companion object { | private val log = "Log" | private const val A = 4 | private const val D = 5 | private val SIMPLE_VALUE = listOf(IDENTIFIER, WHITE_SPACE, COMMA, SEMICOLON) | private const val Daa = 5 | private const val Da = 5 | private const val Db = 5 | } |} |enum class Warnings { | WAITING { | override fun signal() = TALKING | }, | TALKING { | override fun signal() = TALKING | }, | ; | abstract fun signal(): ProtocolState |} """.trimMargin(), rulesConfigList = rulesConfigNotSortBoth ) } @Test @Tag(WarningNames.WRONG_DECLARATIONS_ORDER) fun `check correct simple properties`() { lintMethod( """ |class A { | companion object { | private const val A = 5 | private const val B = 4 | } |} """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_DECLARATIONS_ORDER) fun `check correct simple properties between non const`() { lintMethod( """ |class A { | companion object { | private const val D = 4 | private val SIMPLE_VALUE = listOf(IDENTIFIER, WHITE_SPACE, COMMA, SEMICOLON) | private const val B = 4 | private val SIMPLE_VALUE = listOf(IDENTIFIER, WHITE_SPACE, COMMA, SEMICOLON) | private const val C = 4 | private val SIMPLE_VALUE = listOf(IDENTIFIER, WHITE_SPACE, COMMA, SEMICOLON) | } |} """.trimMargin() ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/StringConcatenationRuleFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter3.StringConcatenationRule import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class StringConcatenationRuleFixTest : FixTestBase( "test/paragraph3/string_concatenation", ::StringConcatenationRule, listOf( RulesConfig(Warnings.STRING_CONCATENATION.name, true, emptyMap()) ) ) { @Test @Tag(WarningNames.STRING_CONCATENATION) fun `fixing string concatenation`() { fixAndCompare("StringConcatenationExpected.kt", "StringConcatenationTest.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/StringConcatenationWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter3.StringConcatenationRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class StringConcatenationWarnTest : LintTestBase(::StringConcatenationRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${StringConcatenationRule.NAME_ID}" private val canBeAutoCorrected = true @Test @Tag(WarningNames.STRING_CONCATENATION) fun `string concatenation - only strings`() { lintMethod( """ | val a = "my string" + "string" + value + "other value" | """.trimMargin(), DiktatError(1, 10, ruleId, Warnings.STRING_CONCATENATION.warnText() + " \"my string\" + \"string\" + value + \"other value\"", canBeAutoCorrected) ) } @Test @Tag(WarningNames.STRING_CONCATENATION) fun `string concatenation - simple string and integers`() { lintMethod( """ | val a = "my string" + 1 + 2 + 3 | """.trimMargin(), DiktatError(1, 10, ruleId, Warnings.STRING_CONCATENATION.warnText() + " \"my string\" + 1 + 2 + 3", canBeAutoCorrected) ) } @Test @Tag(WarningNames.STRING_CONCATENATION) // FixMe: need to check and think if this codeblock should trigger warning or not fun `string concatenation - toString function in string templates`() { lintMethod( """ | val a = (1 + 2).toString() + "my string" + 3 | """.trimMargin(), DiktatError(1, 10, ruleId, Warnings.STRING_CONCATENATION.warnText() + " (1 + 2).toString() + \"my string\" + 3", canBeAutoCorrected) ) } @Test @Tag(WarningNames.STRING_CONCATENATION) fun `string concatenation - toString and variables`() { lintMethod( """ | val myObject = 12 | val a = (1 + 2).toString() + "my string" + 3 + "string" + myObject + myObject | """.trimMargin(), DiktatError(2, 10, ruleId, Warnings.STRING_CONCATENATION.warnText() + " (1 + 2).toString() + \"my string\" + 3 + \"string\" + myObject + myObject", canBeAutoCorrected) ) } @Test @Tag(WarningNames.STRING_CONCATENATION) fun `string concatenation - toString and variables with braces`() { lintMethod( """ | val myObject = 12 | val a = (1 + 2).toString() + "my string" + ("string" + myObject) + myObject | """.trimMargin(), DiktatError(2, 10, ruleId, Warnings.STRING_CONCATENATION.warnText() + " (1 + 2).toString() + \"my string\" + (\"string\" + myObject) + myObject", canBeAutoCorrected) ) } @Test @Tag(WarningNames.STRING_CONCATENATION) fun `string concatenation - function argument`() { lintMethod( """ | fun foo1(){ | foo("my string" + "other string" + (1 + 2 + 3)) | } """.trimMargin(), DiktatError(2, 10, ruleId, Warnings.STRING_CONCATENATION.warnText() + " \"my string\" + \"other string\" + (1 + 2 + 3)", canBeAutoCorrected) ) } @Test @Tag(WarningNames.STRING_CONCATENATION) fun `string concatenation - string and braces`() { lintMethod( """ | val myObject = 12 | val a = "my string" + "other string" + (1 + 2 + 3) | """.trimMargin(), DiktatError(2, 10, ruleId, Warnings.STRING_CONCATENATION.warnText() + " \"my string\" + \"other string\" + (1 + 2 + 3)", canBeAutoCorrected) ) } @Test @Tag(WarningNames.STRING_CONCATENATION) fun `string concatenation - several braces`() { lintMethod( """ | val myObject = 12 | val a = "my string" + (1 + 2 + 3) + ("other string" + 3) + (1 + 2 + 3) | """.trimMargin(), DiktatError(2, 10, ruleId, Warnings.STRING_CONCATENATION.warnText() + " \"my string\" + (1 + 2 + 3) + (\"other string\" + 3) + (1 + 2 + 3)", canBeAutoCorrected) ) } @Test @Tag(WarningNames.STRING_CONCATENATION) fun `string concatenation - multiple braces`() { lintMethod( """ | val a = "my string" + (1 + 2 + 3) + ("other string" + 3) + (1 + (2 + 3)) + ("third string" + ("str" + 5)) | """.trimMargin(), DiktatError(1, 10, ruleId, Warnings.STRING_CONCATENATION.warnText() + " \"my string\" + (1 + 2 + 3) + (\"other string\" + 3) + (1 + (2 + 3)) +" + " (\"third string\" + (\"str\" + 5))", canBeAutoCorrected) ) } @Test @Tag(WarningNames.STRING_CONCATENATION) fun `string concatenation - other binary operators`() { lintMethod( """ | val a = "my string" + ("third string" + ("str" + 5 * 12 / 100)) | """.trimMargin(), DiktatError(1, 10, ruleId, Warnings.STRING_CONCATENATION.warnText() + " \"my string\" + (\"third string\" + (\"str\" + 5 * 12 / 100))", canBeAutoCorrected) ) } @Test @Tag(WarningNames.STRING_CONCATENATION) fun `string concatenation - three lines `() { lintMethod( """ | val a = "my string" + | "string" + value + | other + value | """.trimMargin() ) } @Test @Tag(WarningNames.STRING_CONCATENATION) fun `string concatenation - two lines `() { lintMethod( """ | val a = "my string" + | "string" + value | """.trimMargin() ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/StringTemplateRuleFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.ruleset.rules.chapter3.StringTemplateFormatRule import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class StringTemplateRuleFixTest : FixTestBase("test/paragraph3/string_template", ::StringTemplateFormatRule) { @Test @Tag(WarningNames.STRING_TEMPLATE_CURLY_BRACES) fun `should fix enum order`() { fixAndCompare("StringTemplateExpected.kt", "StringTemplateTest.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/StringTemplateRuleWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter3.StringTemplateFormatRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames.STRING_TEMPLATE_CURLY_BRACES import generated.WarningNames.STRING_TEMPLATE_QUOTES import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class StringTemplateRuleWarnTest : LintTestBase(::StringTemplateFormatRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${StringTemplateFormatRule.NAME_ID}" @Test @Tag(STRING_TEMPLATE_CURLY_BRACES) fun `long string template good example`() { lintMethod( """ |class Some { | val template = "${'$'}{::String} ${'$'}{asd.moo()}" | val some = "${'$'}{foo as Foo}" |} """.trimMargin() ) } @Test @Tag(STRING_TEMPLATE_CURLY_BRACES) fun `long string template bad example`() { lintMethod( """ |class Some { | val template = "${'$'}{a} ${'$'}{asd.moo()}" | val some = "${'$'}{1.0}" | val another = "${'$'}{1}" | val singleLetterCase = "${'$'}{ref}" | val digitsWithLetters = "${'$'}{1.0}asd" |} """.trimMargin(), DiktatError(2, 20, ruleId, "${Warnings.STRING_TEMPLATE_CURLY_BRACES.warnText()} ${'$'}{a}", true), DiktatError(3, 16, ruleId, "${Warnings.STRING_TEMPLATE_CURLY_BRACES.warnText()} ${'$'}{1.0}", true), DiktatError(4, 19, ruleId, "${Warnings.STRING_TEMPLATE_CURLY_BRACES.warnText()} ${'$'}{1}", true), DiktatError(5, 28, ruleId, "${Warnings.STRING_TEMPLATE_CURLY_BRACES.warnText()} ${'$'}{ref}", true), DiktatError(6, 29, ruleId, "${Warnings.STRING_TEMPLATE_CURLY_BRACES.warnText()} ${'$'}{1.0}", true) ) } @Test @Tag(STRING_TEMPLATE_QUOTES) fun `short string template bad example`() { lintMethod( """ |class Some { | val template = "${'$'}a" | val z = a |} """.trimMargin(), DiktatError(2, 20, ruleId, "${Warnings.STRING_TEMPLATE_QUOTES.warnText()} ${'$'}a", true) ) } @Test @Tag(STRING_TEMPLATE_CURLY_BRACES) fun `should trigger on dot after braces`() { lintMethod( """ |class Some { | fun some() { | val s = "abs" | println("${'$'}{s}.length is ${'$'}{s.length}") | } |} """.trimMargin(), DiktatError(4, 17, ruleId, "${Warnings.STRING_TEMPLATE_CURLY_BRACES.warnText()} ${'$'}{s}", true) ) } @Test @Tag(STRING_TEMPLATE_QUOTES) fun `should not trigger`() { lintMethod( """ |class Some { | fun some() { | val price = ""${'"'} | ${'$'}9.99 | ""${'"'} | val some = "${'$'}{index + 1}" | } |} """.trimMargin() ) } @Test @Tag(STRING_TEMPLATE_CURLY_BRACES) fun `underscore after braces - braces should not be removed`() { lintMethod( """ |class Some { | fun some() { | val copyTestFile = File("${'$'}{testFile()} copy ${'$'}{testFile}_copy") | } |} """.trimMargin() ) } @Test @Tag(STRING_TEMPLATE_CURLY_BRACES) fun `should not trigger on array access`() { lintMethod( """ |class Some { | fun some() { | val copyTestFile = "${'$'}{arr[0]}" | } |} """.trimMargin() ) } @Test @Tag(STRING_TEMPLATE_QUOTES) fun `should trigger on long string template`() { lintMethod( """ |class Some { | fun some() { | val x = "asd" | val trippleQuotes = ""${'"'}${'$'}x""${'"'} | } |} """.trimMargin(), DiktatError(4, 31, ruleId, "${Warnings.STRING_TEMPLATE_QUOTES.warnText()} ${'$'}x", true) ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/SuperClassListWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings.WRONG_NEWLINES import com.saveourtool.diktat.ruleset.rules.chapter3.files.NewlinesRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import com.saveourtool.diktat.ruleset.constants.Warnings import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class SuperClassListWarnTest : LintTestBase(::NewlinesRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${NewlinesRule.NAME_ID}" @Test @Tag(WarningNames.WRONG_NEWLINES) fun `superclass list on the same line`() { lintMethod( """ |package com.saveourtool.diktat | |class A : B(), C

, D {} """.trimMargin(), DiktatError(3, 38, ruleId, "${Warnings.WRONG_NEWLINES.warnText()} supertype list entries should be placed on different lines in declaration of ", true), ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `first superclass also on a new line`() { lintMethod( """ |package com.saveourtool.diktat | |class A : B(), |C

, |D {} """.trimMargin(), DiktatError(3, 38, ruleId, "${Warnings.WRONG_NEWLINES.warnText()} supertype list entries should be placed on different lines in declaration of ", true), ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `superclass list of 2 elements on the same line`() { lintMethod( """ |package com.saveourtool.diktat | |class A : B(), C

{} """.trimMargin(), ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `superclass list on separate lines`() { lintMethod( """ |package com.saveourtool.diktat | |class A : |B(), |C

, |D {} """.trimMargin(), ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `superclass list different whitespaces`() { lintMethod( """ |package com.saveourtool.diktat | |class A : |B(), | C

, D {} """.trimMargin(), DiktatError(4, 1, ruleId, "${Warnings.WRONG_NEWLINES.warnText()} supertype list entries should be placed on different lines in declaration of ", true), ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/TrailingCommaFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.common.config.rules.DIKTAT_COMMON import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter3.TrailingCommaRule import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class TrailingCommaFixTest : FixTestBase("test/paragraph3/trailing_comma", ::TrailingCommaRule) { private val config: List = listOf( RulesConfig( Warnings.TRAILING_COMMA.name, true, mapOf("valueArgument" to "true", "valueParameter" to "true", "indices" to "true", "whenConditions" to "true", "collectionLiteral" to "true", "typeArgument" to "true", "typeParameter" to "true", "destructuringDeclaration" to "true")), RulesConfig( DIKTAT_COMMON, true, mapOf("kotlinVersion" to "1.4.21")) ) @Test @Tag(WarningNames.TRAILING_COMMA) fun `should add all trailing comma`() { fixAndCompare("TrailingCommaExpected.kt", "TrailingCommaTest.kt", config) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/TrailingCommaWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.TRAILING_COMMA import com.saveourtool.diktat.ruleset.rules.chapter3.TrailingCommaRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class TrailingCommaWarnTest : LintTestBase(::TrailingCommaRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${TrailingCommaRule.NAME_ID}" private fun getRulesConfig(paramName: String): List = listOf( RulesConfig( TRAILING_COMMA.name, true, mapOf(paramName to "true")) ) @Test @Tag(WarningNames.TRAILING_COMMA) fun `check value arguments`() { lintMethod( """ fun shift(x: Int, y: Int) { shift( 25, 20 // trailing comma ) val colors = listOf( "red", "green", "blue" // trailing comma ) } """.trimMargin(), DiktatError(4, 25, ruleId, "${TRAILING_COMMA.warnText()} after VALUE_ARGUMENT: 20", true), DiktatError(10, 25, ruleId, "${TRAILING_COMMA.warnText()} after VALUE_ARGUMENT: \"blue\"", true), rulesConfigList = getRulesConfig("valueArgument") ) } @Test @Tag(WarningNames.TRAILING_COMMA) fun `check class properties and parameters`() { lintMethod( """ class Customer( val name: String, val lastName: String // trailing comma ) class Customer( val name: String, lastName: String // trailing comma ) """.trimMargin(), DiktatError(3, 21, ruleId, "${TRAILING_COMMA.warnText()} after VALUE_PARAMETER: val lastName: String // trailing comma", true), DiktatError(8, 21, ruleId, "${TRAILING_COMMA.warnText()} after VALUE_PARAMETER: lastName: String // trailing comma", true), rulesConfigList = getRulesConfig("valueParameter") ) } @Test @Tag(WarningNames.TRAILING_COMMA) fun `check function value parameters`() { lintMethod( """ class A { fun foo() {} fun powerOf( number: Int, exponent: Int, // trailing comma ) { /*...*/ } constructor( x: Comparable, y: Iterable ) {} fun print( vararg quantity: Int, description: String ) {} } """.trimMargin(), DiktatError(12, 25, ruleId, "${TRAILING_COMMA.warnText()} after VALUE_PARAMETER: y: Iterable", true), DiktatError(17, 25, ruleId, "${TRAILING_COMMA.warnText()} after VALUE_PARAMETER: description: String", true), rulesConfigList = getRulesConfig("valueParameter") ) } @Test @Tag(WarningNames.TRAILING_COMMA) fun `check parameters with optional type`() { lintMethod( """ fun foo() { val sum: (Int, Int, Int,) -> Int = fun( x, y, z // trailing comma ): Int { return x + y + x } println(sum(8, 8, 8)) } """.trimMargin(), DiktatError(5, 25, ruleId, "${TRAILING_COMMA.warnText()} after VALUE_PARAMETER: z // trailing comma", true), rulesConfigList = getRulesConfig("valueParameter") ) } @Test @Tag(WarningNames.TRAILING_COMMA) fun `check indexing suffix`() { lintMethod( """ class Surface { operator fun get(x: Int, y: Int) = 2 * x + 4 * y - 10 } fun getZValue(mySurface: Surface, xValue: Int, yValue: Int) = mySurface[ xValue, yValue // trailing comma ] """.trimMargin(), DiktatError(7, 25, ruleId, "${TRAILING_COMMA.warnText()} after REFERENCE_EXPRESSION: yValue", true), rulesConfigList = getRulesConfig("referenceExpression") ) } @Test @Tag(WarningNames.TRAILING_COMMA) fun `check lambda parameters`() { lintMethod( """ fun main() { val x = { x: Comparable, y: Iterable -> println("1",) } println(x,) } """.trimMargin(), rulesConfigList = getRulesConfig("valueParameter") ) } @Test @Tag(WarningNames.TRAILING_COMMA) fun `check when entry`() { lintMethod( """ fun isReferenceApplicable(myReference: KClass<*>) = when (myReference) { Comparable::class, Iterable::class, String::class // trailing comma -> true else -> false } fun someFun() { when (x) { is Int, is String -> print((x as Int).length) is Long, -> x as Int } } fun someFun() { when (x) { in 1..2 -> foo() } } fun someFun() { when (x) {} } """.trimMargin(), DiktatError(4, 21, ruleId, "${TRAILING_COMMA.warnText()} after WHEN_CONDITION_WITH_EXPRESSION: String::class", true), DiktatError(12, 24, ruleId, "${TRAILING_COMMA.warnText()} after WHEN_CONDITION_IS_PATTERN: is String", true), DiktatError(20, 24, ruleId, "${TRAILING_COMMA.warnText()} after WHEN_CONDITION_IN_RANGE: in 1..2", true), rulesConfigList = getRulesConfig("whenConditions") ) } @Test @Tag(WarningNames.TRAILING_COMMA) fun `check collection literals`() { lintMethod( """ annotation class ApplicableFor(val services: Array) @ApplicableFor([ "serializer", "balancer", "database", "inMemoryCache" // trailing comma ],) fun foo() {} """.trimMargin(), DiktatError(7, 21, ruleId, "${TRAILING_COMMA.warnText()} after STRING_TEMPLATE: \"inMemoryCache\"", true), rulesConfigList = getRulesConfig("collectionLiteral") ) } @Test @Tag(WarningNames.TRAILING_COMMA) fun `check type arguments`() { lintMethod( """ fun foo() {} fun main() { foo< Comparable, Iterable // trailing comma >() } """.trimMargin(), DiktatError(6, 29, ruleId, "${TRAILING_COMMA.warnText()} after TYPE_PROJECTION: Iterable {} """.trimMargin(), DiktatError(3, 25, ruleId, "${TRAILING_COMMA.warnText()} after TYPE_PARAMETER: MyValue", true), rulesConfigList = getRulesConfig("typeParameter") ) } @Test @Tag(WarningNames.TRAILING_COMMA) fun `check destructuring declarations`() { lintMethod( """ fun foo() { data class Car(val manufacturer: String, val model: String, val year: Int) val myCar = Car("Tesla", "Y", 2019) val ( manufacturer, model, year // trailing comma ) = myCar val cars = listOf() fun printMeanValue() { var meanValue: Int = 0 for (( _, _, year // trailing comma ) in cars) { meanValue += year } println(meanValue/cars.size) } printMeanValue() } """.trimMargin(), DiktatError(8, 25, ruleId, "${TRAILING_COMMA.warnText()} after DESTRUCTURING_DECLARATION_ENTRY: year", true), DiktatError(17, 29, ruleId, "${TRAILING_COMMA.warnText()} after DESTRUCTURING_DECLARATION_ENTRY: year", true), rulesConfigList = getRulesConfig("destructuringDeclaration") ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/WhenMustHaveElseFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.ruleset.rules.chapter3.WhenMustHaveElseRule import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class WhenMustHaveElseFixTest : FixTestBase("test/paragraph3/else_expected", ::WhenMustHaveElseRule) { @Test @Tag(WarningNames.WHEN_WITHOUT_ELSE) fun `should make else branch`() { fixAndCompare("ElseInWhenExpected.kt", "ElseInWhenTest.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/WhenMustHaveElseWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter3.WhenMustHaveElseRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class WhenMustHaveElseWarnTest : LintTestBase(::WhenMustHaveElseRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${WhenMustHaveElseRule.NAME_ID}" @Test @Tag(WarningNames.WHEN_WITHOUT_ELSE) fun `when in func test good`() { lintMethod( """ |fun foo() { | when(a) { | 1 -> print("x is neither 1 nor 2") | else -> {} | } |} """.trimMargin() ) } @Test @Tag(WarningNames.WHEN_WITHOUT_ELSE) fun `when in func test bad`() { lintMethod( """ |fun foo() { | when(a) { | 1 -> print("x is neither 1 nor 2") | } |} | """.trimMargin(), DiktatError(2, 5, ruleId, "${Warnings.WHEN_WITHOUT_ELSE.warnText()} else was not found", true) ) } @Test @Tag(WarningNames.WHEN_WITHOUT_ELSE) fun `when expression in func test good`() { lintMethod( """ |fun foo() { | val obj = when(a) { | 1 -> print("x is neither 1 nor 2") | } |} """.trimMargin() ) } @Test @Tag(WarningNames.WHEN_WITHOUT_ELSE) fun `when expression in func test good 2`() { lintMethod( """ |fun foo() { | val x = listOf().map { | when(it) { | 1 -> it * 2 | } | } |} """.trimMargin() ) } @Test @Tag(WarningNames.WHEN_WITHOUT_ELSE) fun `regression - shouldn't check when in when branches and assignments`() { lintMethod( """ |fun foo() { | var x: Int | x = when(it) { | 1 -> when (x) { | 2 -> foo() | } | } |} """.trimMargin() ) } @Test @Tag(WarningNames.WHEN_WITHOUT_ELSE) fun `when in func only enum entries`() { lintMethod( """ |fun foo() { | val v: Enum | when (v) { | Enum.ONE, Enum.TWO -> foo() | Enum.THREE -> bar() | in Enum.FOUR..Enum.TEN -> boo() | ELEVEN -> anotherFoo() | in TWELVE..Enum.TWENTY -> anotherBar() | } |} """.trimMargin() ) } @Test @Tag(WarningNames.WHEN_WITHOUT_ELSE) fun `when in func not only enum entries`() { lintMethod( """ |fun foo() { | val v: Enum | when (v) { | Enum.ONE -> foo() | f(Enum.TWO) -> bar() | } |} """.trimMargin(), DiktatError(3, 5, ruleId, "${Warnings.WHEN_WITHOUT_ELSE.warnText()} else was not found", true) ) } @Test @Tag(WarningNames.WHEN_WITHOUT_ELSE) fun `when in func not only enum entries but in ranges`() { lintMethod( """ |fun foo() { | val v: Enum | when (v) { | Enum.ONE -> foo() | in 1..5 -> bar() | } |} """.trimMargin(), DiktatError(3, 5, ruleId, "${Warnings.WHEN_WITHOUT_ELSE.warnText()} else was not found", true) ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/files/BlankLinesFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3.files import com.saveourtool.diktat.ruleset.rules.chapter3.files.BlankLinesRule import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class BlankLinesFixTest : FixTestBase("test/paragraph3/blank_lines", ::BlankLinesRule) { @Test @Tag(WarningNames.TOO_MANY_BLANK_LINES) fun `should remove redundant blank lines`() { fixAndCompare("RedundantBlankLinesExpected.kt", "RedundantBlankLinesTest.kt") } @Test @Tag(WarningNames.TOO_MANY_BLANK_LINES) fun `should remove blank lines in the beginning and at the end of code block`() { fixAndCompare("CodeBlockWithBlankLinesExpected.kt", "CodeBlockWithBlankLinesTest.kt") } @Test @Tag(WarningNames.TOO_MANY_BLANK_LINES) fun `should remove empty line before the closing quote`() { fixAndCompare("RedundantBlankLinesAtTheEndOfBlockExpected.kt", "RedundantBlankLinesAtTheEndOfBlockTest.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/files/BlankLinesWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3.files import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings.TOO_MANY_BLANK_LINES import com.saveourtool.diktat.ruleset.rules.chapter3.files.BlankLinesRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class BlankLinesWarnTest : LintTestBase(::BlankLinesRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${BlankLinesRule.NAME_ID}" private val consecutiveLinesWarn = "${TOO_MANY_BLANK_LINES.warnText()} do not use more than two consecutive blank lines" private fun blankLinesInBlockWarn(isBeginning: Boolean) = "${TOO_MANY_BLANK_LINES.warnText()} do not put newlines ${if (isBeginning) "in the beginning" else "at the end"} of code blocks" @Test @Tag(WarningNames.TOO_MANY_BLANK_LINES) fun `blank lines usage - positive example`() { lintMethod( """ |class Example { | fun foo() { | | } |} """.trimMargin() ) } @Test @Tag(WarningNames.TOO_MANY_BLANK_LINES) fun `check lambda with empty block`() { lintMethod( """ |fun foo() { | run { | | } |} """.trimMargin() ) } @Test @Tag(WarningNames.TOO_MANY_BLANK_LINES) fun `should prohibit usage of two or more consecutive blank lines`() { lintMethod( """ |class Example { | | | val foo = 0 | | | fun bar() { } |} """.trimMargin(), DiktatError(1, 16, ruleId, consecutiveLinesWarn, true), DiktatError(4, 16, ruleId, consecutiveLinesWarn, true) ) } @Test @Tag(WarningNames.TOO_MANY_BLANK_LINES) fun `should prohibit blank lines in the beginning and at the end of code block`() { lintMethod( """ |class Example { | | fun foo() { | | bar() | | } |} """.trimMargin(), DiktatError(1, 16, ruleId, blankLinesInBlockWarn(true), true), DiktatError(3, 16, ruleId, blankLinesInBlockWarn(true), true), DiktatError(5, 14, ruleId, blankLinesInBlockWarn(false), true) ) } @Test @Tag(WarningNames.TOO_MANY_BLANK_LINES) fun `should prohibit empty line before the closing quote`() { lintMethod( """ |class Example { | fun foo() { | bar() | | } | |} """.trimMargin(), DiktatError(3, 14, ruleId, blankLinesInBlockWarn(false), true), DiktatError(5, 6, ruleId, blankLinesInBlockWarn(false), true) ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/files/NewlinesRuleFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3.files import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter3.files.NewlinesRule import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class NewlinesRuleFixTest : FixTestBase("test/paragraph3/newlines", ::NewlinesRule) { private val rulesConfigListShort: List = listOf( RulesConfig(Warnings.WRONG_NEWLINES.name, true, mapOf("maxCallsInOneLine" to "1")) ) @Test @Tag(WarningNames.WRONG_NEWLINES) fun `should fix newlines near operators`() { fixAndCompare("OperatorsExpected.kt", "OperatorsTest.kt") } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `should fix newlines to follow functional style`() { fixAndCompare("FunctionalStyleExpected.kt", "FunctionalStyleTest.kt", rulesConfigListShort) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `should fix empty space between identifier and opening parentheses`() { fixAndCompare("LParExpected.kt", "LParTest.kt") } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `should fix wrong newlines around comma`() { fixAndCompare("CommaExpected.kt", "CommaTest.kt") } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `should fix wrong newlines before colon`() { fixAndCompare("ColonExpected.kt", "ColonTest.kt") } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `One line parameters list sheet must contain no more than 2 parameters`() { fixAndCompare("SizeParameterListExpected.kt", "SizeParameterListTest.kt") } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `should fix wrong newlines in lambdas`() { fixAndCompare("LambdaExpected.kt", "LambdaTest.kt") } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `should replace functions with only return with expression body`() { fixAndCompare("ExpressionBodyExpected.kt", "ExpressionBodyTest.kt") } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `should insert newlines in a long parameter or supertype list`() { fixAndCompare("ParameterListExpected.kt", "ParameterListTest.kt") } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `should insert newlines in supertype list`() { fixAndCompare("SuperClassListOnTheSameLineExpected.kt", "SuperClassListOnTheSameLineTest.kt") } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `should fix one line function`() { fixAndCompare("OneLineFunctionExpected.kt", "OneLineFunctionTest.kt") } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `list argument in lambda`() { fixAndCompare("ListArgumentLambdaExpected.kt", "ListArgumentLambdaTest.kt") } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `long dot qualified expression`() { fixAndCompare("LongDotQualifiedExpressionExpected.kt", "LongDotQualifiedExpressionTest.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/files/NewlinesRuleWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3.files import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.COMPLEX_EXPRESSION import com.saveourtool.diktat.ruleset.constants.Warnings.WRONG_NEWLINES import com.saveourtool.diktat.ruleset.rules.chapter3.files.NewlinesRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Tags import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir import java.nio.file.Path @Suppress("LargeClass") class NewlinesRuleWarnTest : LintTestBase(::NewlinesRule) { private val rulesConfigList: List = listOf( RulesConfig(WRONG_NEWLINES.name, true, mapOf("maxCallsInOneLine" to "3")) ) private val rulesConfigListShort: List = listOf( RulesConfig(WRONG_NEWLINES.name, true, mapOf("maxCallsInOneLine" to "1")) ) private val rulesConfigListLong: List = listOf( RulesConfig(WRONG_NEWLINES.name, true, mapOf("maxCallsInOneLine" to "10")) ) private val ruleId = "$DIKTAT_RULE_SET_ID:${NewlinesRule.NAME_ID}" private val dotQuaOrSafeAccessOrPostfixExpression = "${WRONG_NEWLINES.warnText()} wrong split long `dot qualified expression` or `safe access expression`" private val shouldBreakAfter = "${WRONG_NEWLINES.warnText()} should break a line after and not before" private val shouldBreakBefore = "${WRONG_NEWLINES.warnText()} should break a line before and not after" private val functionalStyleWarn = "${WRONG_NEWLINES.warnText()} should follow functional style at" private val lparWarn = "${WRONG_NEWLINES.warnText()} opening parentheses should not be separated from constructor or function name" private val commaWarn = "${WRONG_NEWLINES.warnText()} newline should be placed only after comma" private val prevColonWarn = "${WRONG_NEWLINES.warnText()} newline shouldn't be placed before colon" private val lambdaWithArrowWarn = "${WRONG_NEWLINES.warnText()} in lambda with several lines in body newline should be placed after an arrow" private val lambdaWithoutArrowWarn = "${WRONG_NEWLINES.warnText()} in lambda with several lines in body newline should be placed after an opening brace" private val singleReturnWarn = "${WRONG_NEWLINES.warnText()} functions with single return statement should be simplified to expression body" @Test @Tag(WarningNames.WRONG_NEWLINES) fun `should not false positively trigger on dots in package directive and imports`() { lintMethod( """ |package com.saveourtool.diktat.example | |import com.saveourtool.diktat.Foo |import com.saveourtool.diktat.example.* """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `should not false positively trigger on operators in the middle of the line`() { lintMethod( """ |fun foo() { | val log = LoggerFactory.getLogger(Foo::class.java) | val c = c1 && c2 | obj.foo() | obj?.foo() | obj::foo | bar().let(::baz) | ::baz |} """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `line breaking at operators - positive example`() { lintMethod( """ |fun foo() { | val foo: Foo? = bar ?: Bar(javaClass.classLoader).readResource("baz") | foo?: bar | | val and = condition1 && | condition2 | val plus = x + | y | | obj!! | | obj | .foo() | obj | ?.foo() | obj | ?: OBJ | obj | ::foo |} """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `line breaking at operators`() { lintMethod( """ |fun foo() { | val and = condition1 | && condition2 | // this isn't an expression | val plus = x | + y | | obj. | foo() | obj?. | foo() | obj ?: | OBJ | obj:: | foo |} """.trimMargin(), DiktatError(3, 9, ruleId, "$shouldBreakAfter &&", true), DiktatError(8, 8, ruleId, "$shouldBreakBefore .", true), DiktatError(10, 8, ruleId, "$shouldBreakBefore ?.", true), DiktatError(12, 9, ruleId, "$shouldBreakBefore ?:", true), DiktatError(14, 8, ruleId, "$shouldBreakBefore ::", true) ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `line breaking after infix functions - positive example`() { lintMethod( """ |fun foo() { | true xor | false | | true | .xor(false) | | (true xor | false) | | true xor false or true |} """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `line breaking after infix functions`() { lintMethod( """ |fun foo() { | (true | xor false) | | (true | xor | false) |} """.trimMargin(), DiktatError(3, 9, ruleId, "$shouldBreakAfter xor", true), DiktatError(6, 9, ruleId, "$shouldBreakAfter xor", true) ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `line breaking after infix functions - several functions in a chain`() { lintMethod( """ |fun foo() { | (true xor false | or true | ) | | (true | xor false | or true | ) | | (true | xor | false | or | true | ) |} """.trimMargin(), DiktatError(3, 9, ruleId, "$shouldBreakAfter or", true), DiktatError(7, 9, ruleId, "$shouldBreakAfter xor", true), DiktatError(8, 9, ruleId, "$shouldBreakAfter or", true), DiktatError(12, 9, ruleId, "$shouldBreakAfter xor", true), DiktatError(14, 9, ruleId, "$shouldBreakAfter or", true) ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `chained calls should follow functional style - positive example`() { lintMethod( """ |fun foo(list: List?) { | list!! | .filterNotNull() | .map { it.baz() } | .firstOrNull { | it.condition() | } | ?.qux() |} """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `chained calls should follow functional style - should not trigger on single dot calls`() { lintMethod( """ |fun foo(bar: Bar?) { | bar.baz() | bar?.baz() | bar!!.baz() |} """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `chained calls should follow functional style`() { lintMethod( """ |fun foo(list: List?) { | list!!.filterNotNull() | .map { it.baz() }.firstOrNull { | it.condition() | }?.qux() |} """.trimMargin(), DiktatError(2, 5, ruleId, dotQuaOrSafeAccessOrPostfixExpression, true), DiktatError(2, 11, ruleId, "$functionalStyleWarn .", true), DiktatError(3, 26, ruleId, "$functionalStyleWarn .", true), DiktatError(5, 10, ruleId, "$functionalStyleWarn ?.", true), rulesConfigList = rulesConfigListShort ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `chained calls should follow functional style - exception for ternary if-else`() { lintMethod( """ |fun foo(list: List?) { | if (list.size > n) list.filterNotNull().map { it.baz() } else list.let { it.bar() }.firstOrNull()?.qux() |} """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `newline should be placed only after assignment operator`() { lintMethod( """ |class Example { | val a = | 42 | val b | = 43 |} """.trimMargin(), DiktatError(5, 9, ruleId, "$shouldBreakAfter =", true) ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `newline should be placed only after comma`() { lintMethod( """ |fun foo(a: Int | , | b: Int) { | bar(a | , b) |} """.trimMargin(), DiktatError(2, 9, ruleId, commaWarn, true), DiktatError(5, 9, ruleId, commaWarn, true) ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `newline shouldn't be placed before colon`() { lintMethod( """ |fun foo(a | : Int, | b | : Int) { | bar(a, b) |} """.trimMargin(), DiktatError(2, 9, ruleId, prevColonWarn, true), DiktatError(4, 9, ruleId, prevColonWarn, true) ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `One line parameters list sheet must contain no more than 2 parameters`() { lintMethod( """ |fun foo(a: Int, b: Int, c: Int) { | bar(a, b) |} """.trimMargin(), DiktatError(1, 8, ruleId, "${WRONG_NEWLINES.warnText()} first parameter should be placed on a separate line or all other parameters " + "should be aligned with it in declaration of ", true), DiktatError(1, 8, ruleId, "${WRONG_NEWLINES.warnText()} value parameters should be placed on different lines in declaration of ", true) ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `newline after colon`() { lintMethod( """ |fun foo(a: | Int, | b: | Int) { | bar(a, b) |} """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `list parameter should be placed on different lines`() { lintMethod( """ |fun foo( | a: Int, | b: Int, | c: Int | ) { |} """.trimMargin(), ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `function name should not be separated from ( - positive example`() { lintMethod( """ |val foo = Foo(arg1, arg2) |class Example( | val x: Int |) { | fun foo( | a: Int | ) { } |} """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `function name should not be separated from ( - should not trigger on other parenthesis`() { lintMethod( """ |val x = (2 + 2) * 2 |val y = if (condition) 2 else 1 |fun foo(f: (Int) -> Pair) { | val (a, b) = f(0) |} |fun bar(f: (x: Int) -> Unit) { } """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `function name should not be separated from (`() { lintMethod( """ |val foo = Foo (arg1, arg2) |class Example | ( | val x: Int | ) { | fun foo | ( | a: Int | ) { } |} """.trimMargin(), DiktatError(1, 16, ruleId, lparWarn, true), DiktatError(3, 5, ruleId, lparWarn, true), DiktatError(7, 5, ruleId, lparWarn, true) ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `newline should be placed only after comma - positive example`() { lintMethod( """ |fun foo(a: Int, | b: Int) { | bar(a, b) |} """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `in lambdas newline should be placed after an arrow - positive example`() { lintMethod( """ |class Example { | val a = list.map { elem -> | foo(elem) | } | val b = list.map { elem: Type -> | foo(elem) | } | val c = list.map { | bar(elem) | } | val d = list.map { elem -> bar(elem) } | val e = list.map { elem: Type -> bar(elem) } | val f = list.map { bar(elem) } |} """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `in lambdas newline should be placed after an arrow`() { lintMethod( """ |class Example { | val a = list.map { | elem -> | foo(elem) | } | val b = list.map { elem: Type | -> | foo(elem) | } | val c = list.map { elem | -> bar(elem) | } | val d = list.map { elem: Type -> bar(elem) | foo(elem) | } | val e = list.map { bar(elem) | foo(elem) | } |} """.trimMargin(), DiktatError(3, 14, ruleId, lambdaWithArrowWarn, true), DiktatError(7, 9, ruleId, lambdaWithArrowWarn, true), DiktatError(11, 9, ruleId, lambdaWithArrowWarn, true), DiktatError(13, 35, ruleId, lambdaWithArrowWarn, true), DiktatError(16, 22, ruleId, lambdaWithoutArrowWarn, true) ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `should warn if function consists of a single return statement - positive example`() { lintMethod( """ |fun foo() = "lorem ipsum" | |fun bar(): String { | baz() | return "lorem ipsum" |} | |fun qux(list: List): Int { | list.filter { | return@filter condition(it) | }.forEach { | return 0 | } | return list.first() |} | |fun quux() { return } | |fun quux2(): Unit { return } """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `should warn for array access expression`() { lintMethod( """ |fun bar(): String { | val a = list.responseBody!![0]. | name |} """.trimMargin(), DiktatError(2, 35, ruleId, "$shouldBreakBefore .", true) ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `long argument list should be split into several lines - positive example`() { lintMethod( """ |class SmallExample(val a: Int) | |class Example(val a: Int, | val b: Int) { | fun foo(a: Int) { } | | fun bar( | a: Int, | b: Int | ) { } |} """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_NEWLINES) @Disabled("Will be implemented later") fun `long argument list should be split into several lines`() { lintMethod( """ |class SmallExample(val a: Int) | |class Example(val a: Int, val b: Int) { | fun foo(a: Int) { } | | fun bar( | a: Int, b: Int | ) { } |} """.trimMargin(), DiktatError(3, 14, ruleId, "${WRONG_NEWLINES.warnText()} argument list should be split into several lines", true), DiktatError(7, 12, ruleId, "${WRONG_NEWLINES.warnText()} argument list should be split into several lines", true) ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `should warn if function consists of a single return statement`() { lintMethod( """ |fun foo(): String { | return "lorem ipsum" |} """.trimMargin(), DiktatError(2, 5, ruleId, singleReturnWarn, true) ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `shouldn't warn if function consists of a single return statement with a nested return`() { lintMethod( """ |fun foo(): String { | return Demo(string ?: return null) |} """.trimMargin(), ) } @Test @Tag(WarningNames.WRONG_NEWLINES) @Suppress("TOO_LONG_FUNCTION") fun `should not trigger`() { lintMethod( """ |fun foo(): String { | | val a = java.lang.Boolean.getBoolean(properties.getProperty("parallel.mode")) | | allProperties?.filter { | predicate(it) | val x = listOf(1,2,3).filter { it < 3 } | x == 0 | } | .foo() | .bar() | | allProperties?.filter { | predicate(it) | } | .foo() | .bar() | .let { | it.some() | } | | allProperties | ?.filter { | predicate(it) | } | .foo() | .bar() | .let { | mutableListOf().also { | | } | } |} """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `should trigger for several lambdas on same line`() { lintMethod( """ |fun foo(): String { | allProperties.filter { predicate(it) } | .foo() | .bar() | | allProperties?.filter { predicate(it) } | .foo() | .bar() | | list.foo() | .bar() | .filter { | baz() | } | | list.filter { | | } | .map(::foo).filter { | bar() | } |} """.trimMargin(), DiktatError(16, 9, ruleId, dotQuaOrSafeAccessOrPostfixExpression, true), DiktatError(19, 20, ruleId, "$functionalStyleWarn .", true), rulesConfigList = rulesConfigListShort ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `should suggest newlines in a long argument list of a constructor`() { lintMethod( """ |class Foo(val arg1: Int, arg2: Int) { } | |class Foo(val arg1: Int, arg2: Int, arg3: Int) { | constructor(arg1: Int, arg2: String, arg3: String) : this(arg1, 0, 0) { } |} | |class Foo(val arg1: Int, | var arg2: Int, | arg3: Int |) { } """.trimMargin(), DiktatError(3, 10, ruleId, "${WRONG_NEWLINES.warnText()} first parameter should be placed on a separate line or all other parameters " + "should be aligned with it in declaration of ", true), DiktatError(3, 10, ruleId, "${WRONG_NEWLINES.warnText()} value parameters should be placed on different lines in declaration of ", true), DiktatError(4, 16, ruleId, "${WRONG_NEWLINES.warnText()} first parameter should be placed on a separate line or all other parameters " + "should be aligned with it in declaration of ", true), DiktatError(4, 16, ruleId, "${WRONG_NEWLINES.warnText()} value parameters should be placed on different lines in declaration of ", true), DiktatError(4, 62, ruleId, "${WRONG_NEWLINES.warnText()} first value argument (arg1) should be placed on the new line or " + "all other parameters should be aligned with it", true), DiktatError(4, 62, ruleId, "${WRONG_NEWLINES.warnText()} value arguments should be placed on different lines", true) ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `should suggest newlines in a long argument list`() { lintMethod( """ |fun foo(arg1: Int, arg2: Int) { } | |fun bar(arg1: Int, arg2: Int, arg3: Int) { } | |// should not trigger on functional types |fun bar(arg1: (_arg1: Int, _arg2: Int, _arg3: Int) -> Int) { } | |// should not trigger on functional type receivers |fun bar(arg1: Foo.(_arg1: Int, _arg2: Int, _arg3: Int) -> Int) { } | |fun baz(arg1: Int, | arg2: Int, | arg3: Int |) { } """.trimMargin(), DiktatError(3, 8, ruleId, "${WRONG_NEWLINES.warnText()} first parameter should be placed on a separate line or all other parameters " + "should be aligned with it in declaration of ", true), DiktatError(3, 8, ruleId, "${WRONG_NEWLINES.warnText()} value parameters should be placed on different lines in declaration of ", true) ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `should not raise warning on value arguments`() { lintMethod( """ |class SomeRule(configRules: List) : Rule("id", |configRules, |listOf("foo", "baz") |) { | |} """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `should not raise warning on list params`() { lintMethod( """ |class SomeRule(configRules: List) : Rule("id", |configRules, |listOf("foo", "baz", "triple", "bar") |) { | |} """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `should raise warning on value arguments`() { lintMethod( """ |class SomeRule(configRules: List) : Rule("id", configRules, listOf("foo", "baz")) { | |} """.trimMargin(), DiktatError(1, 46, ruleId, "${WRONG_NEWLINES.warnText()} first value argument (\"id\") should be placed on the new line or " + "all other parameters should be aligned with it", true), DiktatError(1, 46, ruleId, "${WRONG_NEWLINES.warnText()} value arguments should be placed on different lines", true), ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `should suggest newlines in a long supertype list`() { lintMethod( """ |class Foo : | FooBase(), | BazInterface, | BazSuperclass { } | |class Foo : FooBase(), BazInterface, | BazSuperclass { } | |class Foo : FooBase(), BazInterface, BazSuperclass { } | |class Foo : FooBase() { } """.trimMargin(), DiktatError(6, 13, ruleId, "${WRONG_NEWLINES.warnText()} supertype list entries should be placed on different lines in declaration of ", true), DiktatError(9, 13, ruleId, "${WRONG_NEWLINES.warnText()} supertype list entries should be placed on different lines in declaration of ", true) ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `should warn dot qualified with first on same line`() { lintMethod( """ |fun foo() { | x.map() | .gre().few().dfh().qwe() |} | |fun foo() { | x.map() | .gre() | .few() |} | |fun foo() { | x.map().gre().few().qwe() |} """.trimMargin(), DiktatError(2, 4, ruleId, dotQuaOrSafeAccessOrPostfixExpression, true), DiktatError(3, 22, ruleId, "$functionalStyleWarn .", true), DiktatError(13, 4, ruleId, dotQuaOrSafeAccessOrPostfixExpression, true), DiktatError(13, 23, ruleId, "$functionalStyleWarn .", true) ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `should warn dot qualified with first on diff line`() { lintMethod( """ |fun foo() { | x | .map() | .gre().few().qwe().qwe() |} | |fun foo() { | x | .map().gre().few().vfd() |} | |fun foo() { | x | .map() | .gre() | .few() |} """.trimMargin(), DiktatError(2, 4, ruleId, dotQuaOrSafeAccessOrPostfixExpression, true), DiktatError(4, 22, ruleId, "$functionalStyleWarn .", true), DiktatError(8, 4, ruleId, dotQuaOrSafeAccessOrPostfixExpression, true), DiktatError(9, 22, ruleId, "$functionalStyleWarn .", true) ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `should warn dot qualified with safe access`() { lintMethod( """ |fun foo() { | x | ?.map() | .gre().few() |} | |fun foo() { | x | ?.map().gre().few() |} |fun foo() { | x | ?.map() | .gre() | .few() |} | |fun foo() { | x?.map() | .gre() | .few() |} """.trimMargin(), DiktatError(2, 4, ruleId, dotQuaOrSafeAccessOrPostfixExpression, true), DiktatError(4, 10, ruleId, "$functionalStyleWarn .", true), DiktatError(8, 4, ruleId, dotQuaOrSafeAccessOrPostfixExpression, true), DiktatError(9, 11, ruleId, "$functionalStyleWarn .", true), DiktatError(9, 17, ruleId, "$functionalStyleWarn .", true), rulesConfigList = rulesConfigListShort ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `should warn dot qualified with exclexcl`() { lintMethod( """ |fun foo() { | x!!.map() | .gre() | .few() |} | |fun foo() { | x!! | .map() | .gre() | .few() |} | |fun foo() { | x!! | .map().gre() | .few() |} | |fun foo() { | x!!.map() | .gre().few() |} """.trimMargin(), DiktatError(2, 7, ruleId, "$functionalStyleWarn .", true), DiktatError(15, 4, ruleId, dotQuaOrSafeAccessOrPostfixExpression, true), DiktatError(16, 10, ruleId, "$functionalStyleWarn .", true), DiktatError(21, 4, ruleId, dotQuaOrSafeAccessOrPostfixExpression, true), DiktatError(21, 7, ruleId, "$functionalStyleWarn .", true), DiktatError(22, 10, ruleId, "$functionalStyleWarn .", true), rulesConfigList = rulesConfigListShort ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `should warn elvis`() { lintMethod( """ |fun foo() { | | z.goo() | ?: | goo() | | x.goo() | ?:goo() | | y.ds()?:gh() |} """.trimMargin(), DiktatError(4, 8, ruleId, "$shouldBreakBefore ?:", true) ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `should warn elvis with several dot qualifided`() { lintMethod( """ |fun foo() { | z.goo() | ?: | goo().gor().goo() |} """.trimMargin(), DiktatError(3, 8, ruleId, "$shouldBreakBefore ?:", true) ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `test configuration for calls in one line`() { lintMethod( """ |fun foo() { | z.goo().foo().qwe() | z!!.htr().foo() | x.goo().foo().goo() | x.gf().gfh() ?: true | x.gf().fge().qwe().fd() |} """.trimMargin(), DiktatError(6, 4, ruleId, dotQuaOrSafeAccessOrPostfixExpression, true), DiktatError(6, 22, ruleId, "$functionalStyleWarn .", true), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `more test for prefix`() { lintMethod( """ |fun foo() { | foo | .bar() | .goo() | .qwe()!! | | goo()!!.gre() | | bfr()!!.qwe().foo().qwe().dg() |} | |fun foo() { | foo | .bar() | .goo()!! | .qwe() |} """.trimMargin(), DiktatError(9, 4, ruleId, dotQuaOrSafeAccessOrPostfixExpression, true), DiktatError(9, 29, ruleId, "$functionalStyleWarn .", true) ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `more test for unsafe calls`() { lintMethod( """ |fun foo() { | foo | .bar() | .goo()!! | .qwe() | | val x = foo | .bar!! | .baz |} """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `test for null safety one line`() { lintMethod( """ |fun foo() { | foo.qwe() ?: bar.baz() | foo ?: bar().qwe() | foo ?: bar().qwe().qwe() |} """.trimMargin(), DiktatError(2, 14, ruleId, "$functionalStyleWarn ?:", true), DiktatError(4, 8, ruleId, "$functionalStyleWarn ?:", true), DiktatError(4, 11, ruleId, dotQuaOrSafeAccessOrPostfixExpression, true), DiktatError(4, 22, ruleId, "$functionalStyleWarn .", true), rulesConfigList = rulesConfigListShort ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `test for not first prefix`() { lintMethod( """ |fun foo() { | a().b!!.c() | a().b.c()!! |} """.trimMargin(), DiktatError(2, 4, ruleId, dotQuaOrSafeAccessOrPostfixExpression, true), DiktatError(2, 11, ruleId, "$functionalStyleWarn .", true), DiktatError(3, 4, ruleId, dotQuaOrSafeAccessOrPostfixExpression, true), DiktatError(3, 9, ruleId, "$functionalStyleWarn .", true), rulesConfigList = rulesConfigListShort ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `test for null safety several lines`() { lintMethod( """ |fun foo() { | foo.qwe() | ?: bar.baz() | foo | ?: bar().qwe() | foo | ?: bar().qwe().qwe() | foo | .qwe() ?: qwe().qwe() | foo().qwe() ?: qwe(). | qwe() | foo().qwe() ?: qwe() | .qwe() |} """.trimMargin(), DiktatError(7, 11, ruleId, dotQuaOrSafeAccessOrPostfixExpression, true), DiktatError(7, 22, ruleId, "$functionalStyleWarn .", true), DiktatError(9, 15, ruleId, "$functionalStyleWarn ?:", true), DiktatError(10, 16, ruleId, "$functionalStyleWarn ?:", true), DiktatError(10, 24, ruleId, "$shouldBreakBefore .", true), DiktatError(12, 16, ruleId, "$functionalStyleWarn ?:", true), rulesConfigList = rulesConfigListShort ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `test for null safety correct examples`() { lintMethod( """ |fun foo() { | foo | ?: bar | .bar() | .qux() | | | foo.bar() | .baz() | .qux() | ?: boo |} """.trimMargin(), rulesConfigList = rulesConfigListShort ) } @Test @Tags(Tag(WarningNames.WRONG_NEWLINES), Tag(WarningNames.COMPLEX_EXPRESSION)) fun `complex expression in condition`() { lintMethod( """ |fun foo() { | if(a.b.c) {} | while(a?.b?.c) {} | when(a.b!!.c) {} | goo(a?.b.c) |} """.trimMargin(), DiktatError(2, 10, ruleId, "${COMPLEX_EXPRESSION.warnText()} .", false), DiktatError(3, 14, ruleId, "${COMPLEX_EXPRESSION.warnText()} ?.", false), DiktatError(4, 14, ruleId, "${COMPLEX_EXPRESSION.warnText()} .", false), DiktatError(5, 12, ruleId, "${COMPLEX_EXPRESSION.warnText()} .", false), rulesConfigList = rulesConfigListShort ) } @Test @Tags(Tag(WarningNames.WRONG_NEWLINES), Tag(WarningNames.COMPLEX_EXPRESSION)) fun `complex expression in condition with double warnings`() { lintMethod( """ |fun foo() { | if(a().b().c()) {} |} """.trimMargin(), DiktatError(2, 7, ruleId, dotQuaOrSafeAccessOrPostfixExpression, true), DiktatError(2, 14, ruleId, "${COMPLEX_EXPRESSION.warnText()} .", false), DiktatError(2, 14, ruleId, "$functionalStyleWarn .", true), rulesConfigList = rulesConfigListShort ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `shouldn't fall with NPE`() { lintMethod( """ |val TEST_FUNC = { _: Int, _: Set, _: Int, -> |} """.trimMargin(), DiktatError(1, 19, ruleId, "${WRONG_NEWLINES.warnText()} " + "first parameter should be placed on a separate line or all other parameters should be aligned with it in declaration of ", true), DiktatError(1, 19, ruleId, "${WRONG_NEWLINES.warnText()} value parameters should be placed on different lines", true), ) } @Test @Tag(WarningNames.WRONG_NEWLINES) fun `not complaining on fun without return type`() { lintMethod( """ |fun foo(x: Int, y: Int) = x + y """.trimMargin(), ) } @Test @Tag(WarningNames.COMPLEX_EXPRESSION) fun `COMPLEX_EXPRESSION shouldn't trigger for declarations in kts`(@TempDir tempDir: Path) { lintMethodWithFile( """ |dependencies { | implementation(libs.spring.cloud.starter.gateway) |} """.trimMargin(), tempDir = tempDir, fileName = "build.gradle.kts" ) } @Test @Tags(Tag(WarningNames.WRONG_NEWLINES), Tag(WarningNames.COMPLEX_EXPRESSION)) fun `shouldn't trigger on allowed many calls in one line`() { lintMethod( """ |fun foo() { | if(a().b().c().d().e()) {} |} """.trimMargin(), rulesConfigList = rulesConfigListLong ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/files/SemicolonsRuleFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3.files import com.saveourtool.diktat.ruleset.rules.chapter3.files.SemicolonsRule import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class SemicolonsRuleFixTest : FixTestBase("test/paragraph3/semicolons", ::SemicolonsRule) { @Test @Tag(WarningNames.REDUNDANT_SEMICOLON) fun `should remove redundant semicolons`() { fixAndCompare("SemicolonsExpected.kt", "SemicolonsTest.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/files/SemicolonsRuleWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3.files import com.saveourtool.diktat.api.DiktatError import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings.REDUNDANT_SEMICOLON import com.saveourtool.diktat.ruleset.rules.chapter3.files.SemicolonsRule import com.saveourtool.diktat.util.LintTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class SemicolonsRuleWarnTest : LintTestBase(::SemicolonsRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${SemicolonsRule.NAME_ID}" @Test @Tag(WarningNames.REDUNDANT_SEMICOLON) fun `should forbid EOL semicolons`() { lintMethod( """ |enum class Example { | A, | B | ; | | fun foo() {}; | val a = 0; | val b = if (condition) { bar(); baz()} else qux |}; """.trimMargin(), DiktatError(6, 17, ruleId, "${REDUNDANT_SEMICOLON.warnText()} fun foo() {};", true), DiktatError(7, 14, ruleId, "${REDUNDANT_SEMICOLON.warnText()} val a = 0;", true), DiktatError(9, 2, ruleId, "${REDUNDANT_SEMICOLON.warnText()} };", true) ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/files/TopLevelOrderRuleFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3.files import com.saveourtool.diktat.ruleset.rules.chapter3.files.TopLevelOrderRule import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class TopLevelOrderRuleFixTest : FixTestBase("test/paragraph3/top_level", ::TopLevelOrderRule) { @Test @Tag(WarningNames.TOP_LEVEL_ORDER) fun `should fix top level order`() { fixAndCompare("TopLevelSortExpected.kt", "TopLevelSortTest.kt") } @Test @Tag(WarningNames.TOP_LEVEL_ORDER) fun `should fix top level order with comment`() { fixAndCompare("TopLevelWithCommentExpected.kt", "TopLevelWithCommentTest.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/files/TopLevelOrderRuleWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3.files import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings.TOP_LEVEL_ORDER import com.saveourtool.diktat.ruleset.rules.chapter3.files.TopLevelOrderRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class TopLevelOrderRuleWarnTest : LintTestBase(::TopLevelOrderRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${TopLevelOrderRule.NAME_ID}" @Test @Tag(WarningNames.TOP_LEVEL_ORDER) fun `correct order`() { lintMethod( """ |const val CONSTANT = 42 |val topLevelProperty = "String constant" |lateinit var q: String |fun String.foo() {} |fun foo() {} |private fun gio() {} """.trimMargin() ) } @Test @Tag(WarningNames.TOP_LEVEL_ORDER) fun `wrong order`() { lintMethod( """ |class A {} |lateinit var q: String |interface B {} |fun foo() {} |fun String.foo() {} |private val et = 0 |public const val g = 9.8 |object B {} """.trimMargin(), DiktatError(1, 1, ruleId, "${TOP_LEVEL_ORDER.warnText()} class A {}", true), DiktatError(2, 1, ruleId, "${TOP_LEVEL_ORDER.warnText()} lateinit var q: String", true), DiktatError(3, 1, ruleId, "${TOP_LEVEL_ORDER.warnText()} interface B {}", true), DiktatError(4, 1, ruleId, "${TOP_LEVEL_ORDER.warnText()} fun foo() {}", true), DiktatError(5, 1, ruleId, "${TOP_LEVEL_ORDER.warnText()} fun String.foo() {}", true), DiktatError(6, 1, ruleId, "${TOP_LEVEL_ORDER.warnText()} private val et = 0", true), DiktatError(7, 1, ruleId, "${TOP_LEVEL_ORDER.warnText()} public const val g = 9.8", true), DiktatError(8, 1, ruleId, "${TOP_LEVEL_ORDER.warnText()} object B {}", true) ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/spaces/ExpectedIndentationError.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3.spaces import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings.WRONG_INDENTATION import com.saveourtool.diktat.ruleset.junit.ExpectedLintError import com.saveourtool.diktat.ruleset.rules.chapter3.files.IndentationRule.Companion.NAME_ID import com.saveourtool.diktat.api.DiktatError /** * The expected indentation error (extracted from annotated code fragments). * * @property line the line number (1-based). * @property column the column number (1-based). */ class ExpectedIndentationError(override val line: Int, override val column: Int = 1, private val message: String ) : ExpectedLintError { /** * @param line the line number (1-based). * @param column the column number (1-based). * @param expectedIndent the expected indentation level (in space characters). * @param actualIndent the actual indentation level (in space characters). */ constructor(line: Int, column: Int = 1, expectedIndent: Int, actualIndent: Int ) : this( line, column, warnText(expectedIndent)(actualIndent) ) override fun asLintError(): DiktatError = DiktatError( line, column, "$DIKTAT_RULE_SET_ID:$NAME_ID", message, true) private companion object { private val warnText: (Int) -> (Int) -> String = { expectedIndent -> { actualIndent -> "${WRONG_INDENTATION.warnText()} expected $expectedIndent but was $actualIndent" } } } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/spaces/IndentationConfigAwareTest.kt ================================================ @file:Suppress("FILE_UNORDERED_IMPORTS")// False positives, see #1494. package com.saveourtool.diktat.ruleset.chapter3.spaces import com.saveourtool.diktat.ruleset.junit.NaturalDisplayName import com.saveourtool.diktat.ruleset.rules.chapter3.files.IndentationAmount.EXTENDED import com.saveourtool.diktat.ruleset.rules.chapter3.files.IndentationAmount.NONE import com.saveourtool.diktat.ruleset.rules.chapter3.files.IndentationAmount.SINGLE import com.saveourtool.diktat.ruleset.rules.chapter3.files.IndentationConfigAware.Factory.withIndentationConfig import com.saveourtool.diktat.ruleset.utils.indentation.IndentationConfig.Companion.INDENTATION_SIZE import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.TestMethodOrder import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource import com.saveourtool.diktat.ruleset.chapter3.spaces.IndentationConfigFactory as IndentationConfig @TestMethodOrder(NaturalDisplayName::class) class IndentationConfigAwareTest { @ParameterizedTest(name = "$INDENTATION_SIZE = {0}") @ValueSource(ints = [2, 4, 8]) fun `Int + IndentationAmount`(indentationSize: Int) { val config = IndentationConfig(INDENTATION_SIZE to indentationSize) withIndentationConfig(config) { assertThat(42 + NONE).isEqualTo(42) assertThat(42 + SINGLE).isEqualTo(42 + indentationSize) assertThat(42 + EXTENDED).isEqualTo(42 + 2 * indentationSize) } } @ParameterizedTest(name = "$INDENTATION_SIZE = {0}") @ValueSource(ints = [2, 4, 8]) fun `Int - IndentationAmount`(indentationSize: Int) { val config = IndentationConfig(INDENTATION_SIZE to indentationSize) withIndentationConfig(config) { assertThat(42 - NONE).isEqualTo(42) assertThat(42 - SINGLE).isEqualTo(42 - indentationSize) assertThat(42 - EXTENDED).isEqualTo(42 - 2 * indentationSize) } } @ParameterizedTest(name = "$INDENTATION_SIZE = {0}") @ValueSource(ints = [2, 4, 8]) fun `IndentationAmount + Int`(indentationSize: Int) { val config = IndentationConfig(INDENTATION_SIZE to indentationSize) withIndentationConfig(config) { assertThat(NONE + 42).isEqualTo(42 + NONE) assertThat(SINGLE + 42).isEqualTo(42 + SINGLE) assertThat(EXTENDED + 42).isEqualTo(42 + EXTENDED) assertThat(42 + (SINGLE + 2)).isEqualTo((42 + SINGLE) + 2) } } @ParameterizedTest(name = "$INDENTATION_SIZE = {0}") @ValueSource(ints = [2, 4, 8]) fun `IndentationAmount - Int`(indentationSize: Int) { val config = IndentationConfig(INDENTATION_SIZE to indentationSize) withIndentationConfig(config) { assertThat(NONE - 42).isEqualTo(-(42 - NONE)) assertThat(SINGLE - 42).isEqualTo(-(42 - SINGLE)) assertThat(EXTENDED - 42).isEqualTo(-(42 - EXTENDED)) assertThat(42 - (SINGLE - 2)).isEqualTo(42 - SINGLE + 2) } } @ParameterizedTest(name = "$INDENTATION_SIZE = {0}") @ValueSource(ints = [2, 4, 8]) fun `IndentationAmount + IndentationAmount`(indentationSize: Int) { val config = IndentationConfig(INDENTATION_SIZE to indentationSize) withIndentationConfig(config) { assertThat(NONE + SINGLE).isEqualTo(0 + SINGLE) assertThat(SINGLE + SINGLE).isEqualTo(0 + EXTENDED) assertThat(42 + SINGLE + SINGLE).isEqualTo(42 + EXTENDED) assertThat(42 + (SINGLE + SINGLE)).isEqualTo(42 + EXTENDED) } } @ParameterizedTest(name = "$INDENTATION_SIZE = {0}") @ValueSource(ints = [2, 4, 8]) fun `IndentationAmount - IndentationAmount`(indentationSize: Int) { val config = IndentationConfig(INDENTATION_SIZE to indentationSize) withIndentationConfig(config) { assertThat(NONE - SINGLE).isEqualTo(0 - SINGLE) assertThat(SINGLE - SINGLE).isEqualTo(0) assertThat(EXTENDED - SINGLE).isEqualTo(0 + SINGLE) assertThat(NONE - EXTENDED).isEqualTo(0 - EXTENDED) assertThat(42 + (SINGLE - SINGLE)).isEqualTo(42 + SINGLE - SINGLE) } } @ParameterizedTest(name = "$INDENTATION_SIZE = {0}") @ValueSource(ints = [2, 4, 8]) fun unaryPlus(indentationSize: Int) { val config = IndentationConfig(INDENTATION_SIZE to indentationSize) withIndentationConfig(config) { assertThat(+NONE).isEqualTo(0) assertThat(+SINGLE).isEqualTo(indentationSize) assertThat(+EXTENDED).isEqualTo(2 * indentationSize) assertThat(EXTENDED - SINGLE).isEqualTo(+SINGLE) } } @ParameterizedTest(name = "$INDENTATION_SIZE = {0}") @ValueSource(ints = [2, 4, 8]) fun unaryMinus(indentationSize: Int) { val config = IndentationConfig(INDENTATION_SIZE to indentationSize) withIndentationConfig(config) { assertThat(-NONE).isEqualTo(0) assertThat(-SINGLE).isEqualTo(-indentationSize) assertThat(-EXTENDED).isEqualTo(-2 * indentationSize) assertThat(NONE - SINGLE).isEqualTo(-SINGLE) assertThat(NONE - EXTENDED).isEqualTo(-EXTENDED) } } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/spaces/IndentationConfigFactory.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3.spaces import com.saveourtool.diktat.ruleset.utils.indentation.IndentationConfig internal object IndentationConfigFactory { /** * Creates an `IndentationConfig` from zero or more * [config entries][configEntries]. Invoke without arguments to create a * default `IndentationConfig`. * * @param configEntries the configuration entries to create this instance from. * @return the configuration created. * @see [IndentationConfig] */ operator fun invoke(vararg configEntries: Pair): IndentationConfig = IndentationConfig(mapOf(*configEntries).mapValues { (_, value) -> value.toString() }) } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/spaces/IndentationRuleFixTest.kt ================================================ @file:Suppress("FILE_UNORDERED_IMPORTS")// False positives, see #1494. package com.saveourtool.diktat.ruleset.chapter3.spaces import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.WRONG_INDENTATION import com.saveourtool.diktat.ruleset.junit.NaturalDisplayName import com.saveourtool.diktat.ruleset.rules.chapter3.files.IndentationRule import com.saveourtool.diktat.ruleset.utils.indentation.IndentationConfig.Companion.ALIGNED_PARAMETERS import com.saveourtool.diktat.ruleset.utils.indentation.IndentationConfig.Companion.EXTENDED_INDENT_AFTER_OPERATORS import com.saveourtool.diktat.ruleset.utils.indentation.IndentationConfig.Companion.EXTENDED_INDENT_BEFORE_DOT import com.saveourtool.diktat.ruleset.utils.indentation.IndentationConfig.Companion.EXTENDED_INDENT_FOR_EXPRESSION_BODIES import com.saveourtool.diktat.ruleset.utils.indentation.IndentationConfig.Companion.EXTENDED_INDENT_OF_PARAMETERS import com.saveourtool.diktat.ruleset.utils.indentation.IndentationConfig.Companion.NEWLINE_AT_END import com.saveourtool.diktat.test.framework.processing.TestFileContent import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.intellij.lang.annotations.Language import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestMethodOrder import org.junit.jupiter.api.io.TempDir import java.nio.file.Path import com.saveourtool.diktat.ruleset.chapter3.spaces.IndentationConfigFactory as IndentationConfig /** * Legacy indentation tests. * * Consider adding new tests to [IndentationRuleTest] instead. * * @see IndentationRuleTest */ @TestMethodOrder(NaturalDisplayName::class) class IndentationRuleFixTest : FixTestBase("test/paragraph3/indentation", ::IndentationRule, listOf( RulesConfig(WRONG_INDENTATION.name, true, mapOf( NEWLINE_AT_END to "true", // expected file should have two newlines at end in order to be read by BufferedReader correctly EXTENDED_INDENT_OF_PARAMETERS to "true", ALIGNED_PARAMETERS to "true", EXTENDED_INDENT_FOR_EXPRESSION_BODIES to "true", EXTENDED_INDENT_AFTER_OPERATORS to "true", EXTENDED_INDENT_BEFORE_DOT to "true", ) ) ) ) { @Test @Tag(WarningNames.WRONG_INDENTATION) fun `parameters should be properly aligned`() { fixAndCompare("IndentationParametersExpected.kt", "IndentationParametersTest.kt") } @Test @Tag(WarningNames.WRONG_INDENTATION) fun `indentation rule - example 1`() { fixAndCompare("IndentationFull1Expected.kt", "IndentationFull1Test.kt") } @Test @Tag(WarningNames.WRONG_INDENTATION) fun `indentation rule - verbose example from ktlint`() { fixAndCompare("IndentFullExpected.kt", "IndentFullTest.kt") } @Test @Tag(WarningNames.WRONG_INDENTATION) fun `regression - incorrect fixing in constructor parameter list`() { fixAndCompare("ConstructorExpected.kt", "ConstructorTest.kt") } @Nested @TestMethodOrder(NaturalDisplayName::class) inner class `Multi-line string literals` { /** * Correctly-indented opening quotation mark, incorrectly-indented * closing quotation mark. */ @Test @Tag(WarningNames.WRONG_INDENTATION) @Suppress("LOCAL_VARIABLE_EARLY_DECLARATION") // False positives fun `case 1 - mis-aligned opening and closing quotes`(@TempDir tempDir: Path) { val actualCode = """ |fun f() { | g( | ""${'"'} | |val q = 1 | | | ""${'"'}.trimMargin(), | arg1 = "arg1" | ) |} """.trimMargin() val expectedCode = """ |fun f() { | g( | ""${'"'} | |val q = 1 | | | ""${'"'}.trimMargin(), | arg1 = "arg1" | ) |} """.trimMargin() val lintResult = fixAndCompareContent(actualCode, expectedCode, tempDir) lintResult.assertSuccessful() } /** * Both the opening and the closing quotation marks are incorrectly * indented (indentation level is less than needed). */ @Test @Tag(WarningNames.WRONG_INDENTATION) @Suppress("LOCAL_VARIABLE_EARLY_DECLARATION") // False positives fun `case 2`(@TempDir tempDir: Path) { val actualCode = """ |fun f() { | g( | ""${'"'} | |val q = 1 | | | ""${'"'}.trimMargin(), | arg1 = "arg1" | ) |} """.trimMargin() val expectedCode = """ |fun f() { | g( | ""${'"'} | |val q = 1 | | | ""${'"'}.trimMargin(), | arg1 = "arg1" | ) |} """.trimMargin() val lintResult = fixAndCompareContent(actualCode, expectedCode, tempDir) lintResult.assertSuccessful() } /** * Both the opening and the closing quotation marks are incorrectly * indented (indentation level is greater than needed). */ @Test @Tag(WarningNames.WRONG_INDENTATION) @Suppress("LOCAL_VARIABLE_EARLY_DECLARATION") // False positives fun `case 3`(@TempDir tempDir: Path) { val actualCode = """ |fun f() { | g( | ""${'"'} | |val q = 1 | | | ""${'"'}.trimMargin(), | arg1 = "arg1" | ) |} """.trimMargin() val expectedCode = """ |fun f() { | g( | ""${'"'} | |val q = 1 | | | ""${'"'}.trimMargin(), | arg1 = "arg1" | ) |} """.trimMargin() val lintResult = fixAndCompareContent(actualCode, expectedCode, tempDir) lintResult.assertSuccessful() } /** * Both the opening and the closing quotation marks are incorrectly * indented and misaligned. */ @Test @Tag(WarningNames.WRONG_INDENTATION) @Suppress("LOCAL_VARIABLE_EARLY_DECLARATION") // False positives fun `case 4 - mis-aligned opening and closing quotes`(@TempDir tempDir: Path) { val actualCode = """ |fun f() { | g( | ""${'"'} | |val q = 1 | | | ""${'"'}.trimMargin(), | arg1 = "arg1" | ) |} """.trimMargin() val expectedCode = """ |fun f() { | g( | ""${'"'} | |val q = 1 | | | ""${'"'}.trimMargin(), | arg1 = "arg1" | ) |} """.trimMargin() val lintResult = fixAndCompareContent(actualCode, expectedCode, tempDir) lintResult.assertSuccessful() } private fun fixAndCompareContent(@Language("kotlin") actualCode: String, @Language("kotlin") expectedCode: String, tempDir: Path ): TestFileContent { val config = IndentationConfig(NEWLINE_AT_END to false).withCustomParameters().asRulesConfigList() return fixAndCompareContent(actualCode, expectedCode, tempDir, overrideRulesConfigList = config) } } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/spaces/IndentationRuleTest.kt ================================================ @file:Suppress("FILE_IS_TOO_LONG") package com.saveourtool.diktat.ruleset.chapter3.spaces import com.saveourtool.diktat.ruleset.chapter3.spaces.junit.IndentationTest import com.saveourtool.diktat.ruleset.chapter3.spaces.junit.IndentedSourceCode import com.saveourtool.diktat.ruleset.junit.BooleanOrDefault.FALSE import com.saveourtool.diktat.ruleset.junit.BooleanOrDefault.TRUE import com.saveourtool.diktat.ruleset.junit.NaturalDisplayName import org.junit.jupiter.api.Nested import org.junit.jupiter.api.TestMethodOrder /** * For legacy indentation tests, see [IndentationRuleWarnTest] and * [IndentationRuleFixTest]. * * @see IndentationRuleWarnTest * @see IndentationRuleFixTest */ @Suppress( "LargeClass", "MaxLineLength", "LONG_LINE", ) @TestMethodOrder(NaturalDisplayName::class) class IndentationRuleTest { /** * See [#1330](https://github.com/saveourtool/diktat/issues/1330). */ @Nested @TestMethodOrder(NaturalDisplayName::class) inner class `Expression body functions` { @IndentationTest( first = IndentedSourceCode( """ @Test fun `checking that suppression with ignore everything works`() { val code = ""${'"'} // diktat:WRONG_INDENTATION[expectedIndent = 12] @Suppress("diktat") fun foo() { val a = 1 } ""${'"'}.trimIndent() lintMethod(code) } """, extendedIndentForExpressionBodies = FALSE), second = IndentedSourceCode( """ @Test fun `checking that suppression with ignore everything works`() { val code = ""${'"'} // diktat:WRONG_INDENTATION[expectedIndent = 8] @Suppress("diktat") fun foo() { val a = 1 } ""${'"'}.trimIndent() lintMethod(code) } """, extendedIndentForExpressionBodies = TRUE)) fun `case 1`() = Unit @IndentationTest( first = IndentedSourceCode( """ val currentTime: Time get() = with(currentDateTime) { // diktat:WRONG_INDENTATION[expectedIndent = 12] Time(hour = hour, minute = minute, second = second) // diktat:WRONG_INDENTATION[expectedIndent = 16] } // diktat:WRONG_INDENTATION[expectedIndent = 12] """, extendedIndentForExpressionBodies = FALSE), second = IndentedSourceCode( """ val currentTime: Time get() = with(currentDateTime) { // diktat:WRONG_INDENTATION[expectedIndent = 8] Time(hour = hour, minute = minute, second = second) // diktat:WRONG_INDENTATION[expectedIndent = 12] } // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentForExpressionBodies = TRUE)) fun `case 2`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun formatDateByPattern(date: String, pattern: String = "ddMMyy"): String = DateTimeFormatter.ofPattern(pattern).format(LocalDate.parse(date)) // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentForExpressionBodies = FALSE), second = IndentedSourceCode( """ fun formatDateByPattern(date: String, pattern: String = "ddMMyy"): String = DateTimeFormatter.ofPattern(pattern).format(LocalDate.parse(date)) // diktat:WRONG_INDENTATION[expectedIndent = 4] """, extendedIndentForExpressionBodies = TRUE)) fun `case 3`() = Unit @IndentationTest( first = IndentedSourceCode( """ private fun createLayoutParams(): WindowManager.LayoutParams = WindowManager.LayoutParams().apply { /* ... */ } // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentForExpressionBodies = FALSE), second = IndentedSourceCode( """ private fun createLayoutParams(): WindowManager.LayoutParams = WindowManager.LayoutParams().apply { /* ... */ } // diktat:WRONG_INDENTATION[expectedIndent = 4] """, extendedIndentForExpressionBodies = TRUE)) fun `case 4`() = Unit @IndentationTest( first = IndentedSourceCode( """ private fun createLayoutParams(): WindowManager.LayoutParams = WindowManager.LayoutParams().apply { // diktat:WRONG_INDENTATION[expectedIndent = 8] type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL // diktat:WRONG_INDENTATION[expectedIndent = 12] token = composeView.applicationWindowToken // diktat:WRONG_INDENTATION[expectedIndent = 12] width = WindowManager.LayoutParams.MATCH_PARENT // diktat:WRONG_INDENTATION[expectedIndent = 12] height = WindowManager.LayoutParams.MATCH_PARENT // diktat:WRONG_INDENTATION[expectedIndent = 12] format = PixelFormat.TRANSLUCENT // diktat:WRONG_INDENTATION[expectedIndent = 12] // TODO make composable configurable // diktat:WRONG_INDENTATION[expectedIndent = 12] // see https://stackoverflow.com/questions/43511326/android-making-activity-full-screen-with-status-bar-on-top-of-it // diktat:WRONG_INDENTATION[expectedIndent = 12] if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { // diktat:WRONG_INDENTATION[expectedIndent = 12] windowInsetsController?.hide(WindowInsets.Type.statusBars()) // diktat:WRONG_INDENTATION[expectedIndent = 16] } else { // diktat:WRONG_INDENTATION[expectedIndent = 12] @Suppress("DEPRECATION") // diktat:WRONG_INDENTATION[expectedIndent = 16] systemUiVisibility = (View.SYSTEM_UI_FLAG_LOW_PROFILE or // diktat:WRONG_INDENTATION[expectedIndent = 16] View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) } // diktat:WRONG_INDENTATION[expectedIndent = 12] } // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentForExpressionBodies = FALSE), second = IndentedSourceCode( """ private fun createLayoutParams(): WindowManager.LayoutParams = WindowManager.LayoutParams().apply { // diktat:WRONG_INDENTATION[expectedIndent = 4] type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL // diktat:WRONG_INDENTATION[expectedIndent = 8] token = composeView.applicationWindowToken // diktat:WRONG_INDENTATION[expectedIndent = 8] width = WindowManager.LayoutParams.MATCH_PARENT // diktat:WRONG_INDENTATION[expectedIndent = 8] height = WindowManager.LayoutParams.MATCH_PARENT // diktat:WRONG_INDENTATION[expectedIndent = 8] format = PixelFormat.TRANSLUCENT // diktat:WRONG_INDENTATION[expectedIndent = 8] // TODO make composable configurable // diktat:WRONG_INDENTATION[expectedIndent = 8] // see https://stackoverflow.com/questions/43511326/android-making-activity-full-screen-with-status-bar-on-top-of-it // diktat:WRONG_INDENTATION[expectedIndent = 8] if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { // diktat:WRONG_INDENTATION[expectedIndent = 8] windowInsetsController?.hide(WindowInsets.Type.statusBars()) // diktat:WRONG_INDENTATION[expectedIndent = 12] } else { // diktat:WRONG_INDENTATION[expectedIndent = 8] @Suppress("DEPRECATION") // diktat:WRONG_INDENTATION[expectedIndent = 12] systemUiVisibility = (View.SYSTEM_UI_FLAG_LOW_PROFILE or // diktat:WRONG_INDENTATION[expectedIndent = 12] View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) } // diktat:WRONG_INDENTATION[expectedIndent = 8] } // diktat:WRONG_INDENTATION[expectedIndent = 4] """, extendedIndentForExpressionBodies = TRUE)) fun `case 5`() = Unit @IndentationTest( first = IndentedSourceCode( """ val offsetDelta = if (shimmerAnimationType != ShimmerAnimationType.FADE) translateAnim.dp // diktat:WRONG_INDENTATION[expectedIndent = 8] else 2000.dp // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentForExpressionBodies = FALSE), second = IndentedSourceCode( """ val offsetDelta = if (shimmerAnimationType != ShimmerAnimationType.FADE) translateAnim.dp // diktat:WRONG_INDENTATION[expectedIndent = 4] else 2000.dp // diktat:WRONG_INDENTATION[expectedIndent = 4] """, extendedIndentForExpressionBodies = TRUE)) fun `case 6`() = Unit @IndentationTest( first = IndentedSourceCode( """ private fun lerp(start: Float, stop: Float, fraction: Float): Float = (1 - fraction) * start + fraction * stop // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentForExpressionBodies = FALSE), second = IndentedSourceCode( """ private fun lerp(start: Float, stop: Float, fraction: Float): Float = (1 - fraction) * start + fraction * stop // diktat:WRONG_INDENTATION[expectedIndent = 4] """, extendedIndentForExpressionBodies = TRUE)) fun `case 7`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun foo() = println() // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentForExpressionBodies = FALSE), second = IndentedSourceCode( """ fun foo() = println() // diktat:WRONG_INDENTATION[expectedIndent = 4] """, extendedIndentForExpressionBodies = TRUE)) fun `case 8`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun f() = x + (y + // diktat:WRONG_INDENTATION[expectedIndent = 8] g(x) ) // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentForExpressionBodies = FALSE), second = IndentedSourceCode( """ fun f() = x + (y + // diktat:WRONG_INDENTATION[expectedIndent = 4] g(x) ) // diktat:WRONG_INDENTATION[expectedIndent = 4] """, extendedIndentForExpressionBodies = TRUE)) fun `case 9`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun f() = (1 + // diktat:WRONG_INDENTATION[expectedIndent = 8] 2) """, extendedIndentForExpressionBodies = FALSE), second = IndentedSourceCode( """ fun f() = (1 + // diktat:WRONG_INDENTATION[expectedIndent = 4] 2) """, extendedIndentForExpressionBodies = TRUE)) fun `case 10`() = Unit } @Nested @TestMethodOrder(NaturalDisplayName::class) inner class `String templates` { /** * No message like * * > only spaces are allowed for indentation and each indentation should * > equal to 4 spaces (tabs are not allowed): the same number of * > indents to the opening and closing quotes was expected * * should be reported. * * See [#1490](https://github.com/saveourtool/diktat/issues/1490). */ @IndentationTest(IndentedSourceCode( """ val value = f( "text ${'$'}variable text".isEmpty() ) """), singleConfiguration = true) fun `mis-aligned opening and closing quotes of a string template, false positive, case 1 (#1490)`() = Unit /** * No message like * * > only spaces are allowed for indentation and each indentation should * > equal to 4 spaces (tabs are not allowed): the same number of * > indents to the opening and closing quotes was expected * * should be reported. * * See [#1490](https://github.com/saveourtool/diktat/issues/1490). */ @IndentationTest(IndentedSourceCode( """ val value = f( "text ${'$'}variable text".trimIndent() ) """), singleConfiguration = true) fun `mis-aligned opening and closing quotes of a string template, false positive, case 2 (#1490)`() = Unit /** * No message like * * > only spaces are allowed for indentation and each indentation should * > equal to 4 spaces (tabs are not allowed): the same number of * > indents to the opening and closing quotes was expected * * should be reported. * * See [#1490](https://github.com/saveourtool/diktat/issues/1490). */ @IndentationTest(IndentedSourceCode( """ val value = f( "text ${'$'}variable text".trimMargin() ) """), singleConfiguration = true) fun `mis-aligned opening and closing quotes of a string template, false positive, case 3 (#1490)`() = Unit } /** * See [#1347](https://github.com/saveourtool/diktat/issues/1347). */ @Nested @TestMethodOrder(NaturalDisplayName::class) inner class `Multi-line string literals` { @IndentationTest( first = IndentedSourceCode( """ @Test fun `test method name`() { @Language("kotlin") val code = ""${'"'} @Suppress("diktat") fun foo() { val a = 1 } ""${'"'}.trimIndent() lintMethod(code) } """, extendedIndentOfParameters = FALSE, extendedIndentForExpressionBodies = FALSE, extendedIndentAfterOperators = FALSE, extendedIndentBeforeDot = FALSE), second = IndentedSourceCode( """ @Test fun `test method name`() { @Language("kotlin") val code = ""${'"'} @Suppress("diktat") fun foo() { val a = 1 } ""${'"'}.trimIndent() lintMethod(code) } """, extendedIndentOfParameters = TRUE, extendedIndentForExpressionBodies = TRUE, extendedIndentAfterOperators = TRUE, extendedIndentBeforeDot = TRUE), includeWarnTests = false ) fun `no whitespace should be injected, case 1`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun f0() { @Language("kotlin") val code = ""${'"'} |@Suppress("diktat") |fun foo() { | val a = 1 |} ""${'"'}.trimMargin() lintMethod(code) } """, extendedIndentOfParameters = FALSE, extendedIndentForExpressionBodies = FALSE, extendedIndentAfterOperators = FALSE, extendedIndentBeforeDot = FALSE), second = IndentedSourceCode( """ fun f0() { @Language("kotlin") val code = ""${'"'} |@Suppress("diktat") |fun foo() { | val a = 1 |} ""${'"'}.trimMargin() lintMethod(code) } """, extendedIndentOfParameters = TRUE, extendedIndentForExpressionBodies = TRUE, extendedIndentAfterOperators = TRUE, extendedIndentBeforeDot = TRUE), includeWarnTests = false ) fun `no whitespace should be injected, case 2`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun f1() { @Language("kotlin") val code = ""${'"'} |@Suppress("diktat") |fun foo() { | val a = 1 |} ""${'"'}.trimMargin("|") lintMethod(code) } """, extendedIndentOfParameters = FALSE, extendedIndentForExpressionBodies = FALSE, extendedIndentAfterOperators = FALSE, extendedIndentBeforeDot = FALSE), second = IndentedSourceCode( """ fun f1() { @Language("kotlin") val code = ""${'"'} |@Suppress("diktat") |fun foo() { | val a = 1 |} ""${'"'}.trimMargin("|") lintMethod(code) } """, extendedIndentOfParameters = TRUE, extendedIndentForExpressionBodies = TRUE, extendedIndentAfterOperators = TRUE, extendedIndentBeforeDot = TRUE), includeWarnTests = false ) fun `no whitespace should be injected, case 3`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun f2() { @Language("kotlin") val code = ""${'"'} >@Suppress("diktat") >fun foo() { > val a = 1 >} ""${'"'} . trimMargin ( marginPrefix = ">" ) lintMethod(code) } """, extendedIndentOfParameters = FALSE, extendedIndentForExpressionBodies = FALSE, extendedIndentAfterOperators = FALSE, extendedIndentBeforeDot = FALSE), second = IndentedSourceCode( """ fun f2() { @Language("kotlin") val code = ""${'"'} >@Suppress("diktat") >fun foo() { > val a = 1 >} ""${'"'} . trimMargin ( marginPrefix = ">" ) lintMethod(code) } """, extendedIndentOfParameters = TRUE, extendedIndentForExpressionBodies = TRUE, extendedIndentAfterOperators = TRUE, extendedIndentBeforeDot = TRUE), includeWarnTests = false ) fun `no whitespace should be injected, case 4`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun checkScript() { lintMethod( ""${'"'} |val A = "aa" ""${'"'}.trimMargin(), ) } """, extendedIndentOfParameters = FALSE, extendedIndentForExpressionBodies = FALSE, extendedIndentAfterOperators = FALSE, extendedIndentBeforeDot = FALSE), second = IndentedSourceCode( """ fun checkScript() { lintMethod( ""${'"'} |val A = "aa" ""${'"'}.trimMargin(), ) } """, extendedIndentOfParameters = TRUE, extendedIndentForExpressionBodies = TRUE, extendedIndentAfterOperators = TRUE, extendedIndentBeforeDot = TRUE), includeWarnTests = false ) fun `no whitespace should be injected, case 5`() = Unit } /** * Expressions wrapped on an operator or an infix function. * * See [#1340](https://github.com/saveourtool/diktat/issues/1340). */ @Nested @TestMethodOrder(NaturalDisplayName::class) inner class `Expressions wrapped after operator` { @IndentationTest( first = IndentedSourceCode( """ fun f() { systemUiVisibility = View.SYSTEM_UI_FLAG_LOW_PROFILE or View.SYSTEM_UI_FLAG_FULLSCREEN or // diktat:WRONG_INDENTATION[expectedIndent = 12] View.SYSTEM_UI_FLAG_LAYOUT_STABLE or // diktat:WRONG_INDENTATION[expectedIndent = 12] View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or // diktat:WRONG_INDENTATION[expectedIndent = 12] View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or // diktat:WRONG_INDENTATION[expectedIndent = 12] View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // diktat:WRONG_INDENTATION[expectedIndent = 12] } """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ fun f() { systemUiVisibility = View.SYSTEM_UI_FLAG_LOW_PROFILE or View.SYSTEM_UI_FLAG_FULLSCREEN or // diktat:WRONG_INDENTATION[expectedIndent = 8] View.SYSTEM_UI_FLAG_LAYOUT_STABLE or // diktat:WRONG_INDENTATION[expectedIndent = 8] View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or // diktat:WRONG_INDENTATION[expectedIndent = 8] View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or // diktat:WRONG_INDENTATION[expectedIndent = 8] View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // diktat:WRONG_INDENTATION[expectedIndent = 8] } """, extendedIndentAfterOperators = TRUE)) fun `case 1`() = Unit @IndentationTest( first = IndentedSourceCode( """ val systemUiVisibility = View.SYSTEM_UI_FLAG_LOW_PROFILE or View.SYSTEM_UI_FLAG_FULLSCREEN or // diktat:WRONG_INDENTATION[expectedIndent = 8] View.SYSTEM_UI_FLAG_LAYOUT_STABLE or // diktat:WRONG_INDENTATION[expectedIndent = 8] View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or // diktat:WRONG_INDENTATION[expectedIndent = 8] View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or // diktat:WRONG_INDENTATION[expectedIndent = 8] View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ val systemUiVisibility = View.SYSTEM_UI_FLAG_LOW_PROFILE or View.SYSTEM_UI_FLAG_FULLSCREEN or // diktat:WRONG_INDENTATION[expectedIndent = 4] View.SYSTEM_UI_FLAG_LAYOUT_STABLE or // diktat:WRONG_INDENTATION[expectedIndent = 4] View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or // diktat:WRONG_INDENTATION[expectedIndent = 4] View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or // diktat:WRONG_INDENTATION[expectedIndent = 4] View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // diktat:WRONG_INDENTATION[expectedIndent = 4] """, extendedIndentAfterOperators = TRUE)) fun `case 2`() = Unit @IndentationTest( first = IndentedSourceCode( """ const val FOO = 1 const val BAR = 2 const val BAZ = 4 fun acceptInteger(arg: Int) = Unit fun main() { acceptInteger(FOO or BAR or BAZ or FOO or BAR or BAZ or FOO or BAR or BAZ or FOO or BAR or BAZ or FOO or BAR or BAZ or // diktat:WRONG_INDENTATION[expectedIndent = 12] FOO or BAR or BAZ) // diktat:WRONG_INDENTATION[expectedIndent = 12] } """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ const val FOO = 1 const val BAR = 2 const val BAZ = 4 fun acceptInteger(arg: Int) = Unit fun main() { acceptInteger(FOO or BAR or BAZ or FOO or BAR or BAZ or FOO or BAR or BAZ or FOO or BAR or BAZ or FOO or BAR or BAZ or // diktat:WRONG_INDENTATION[expectedIndent = 8] FOO or BAR or BAZ) // diktat:WRONG_INDENTATION[expectedIndent = 8] } """, extendedIndentAfterOperators = TRUE)) fun `case 3`() = Unit @IndentationTest( first = IndentedSourceCode( """ const val TRUE = true const val FALSE = false fun acceptBoolean(arg: Boolean) = Unit fun f() { acceptBoolean(TRUE || FALSE || // diktat:WRONG_INDENTATION[expectedIndent = 12] TRUE) // diktat:WRONG_INDENTATION[expectedIndent = 12] } """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ const val TRUE = true const val FALSE = false fun acceptBoolean(arg: Boolean) = Unit fun f() { acceptBoolean(TRUE || FALSE || // diktat:WRONG_INDENTATION[expectedIndent = 8] TRUE) // diktat:WRONG_INDENTATION[expectedIndent = 8] } """, extendedIndentAfterOperators = TRUE)) fun `case 4`() = Unit @IndentationTest( first = IndentedSourceCode( """ val c = 3 + 2 // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ val c = 3 + 2 // diktat:WRONG_INDENTATION[expectedIndent = 4] """, extendedIndentAfterOperators = TRUE)) fun `case 5`() = Unit @IndentationTest( first = IndentedSourceCode( """ infix fun Int.combineWith(that: Int) = this + that fun f() { val x = 1 combineWith 2 combineWith // diktat:WRONG_INDENTATION[expectedIndent = 12] 3 combineWith // diktat:WRONG_INDENTATION[expectedIndent = 12] 4 combineWith // diktat:WRONG_INDENTATION[expectedIndent = 12] 5 combineWith // diktat:WRONG_INDENTATION[expectedIndent = 12] 6 // diktat:WRONG_INDENTATION[expectedIndent = 12] } """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ infix fun Int.combineWith(that: Int) = this + that fun f() { val x = 1 combineWith 2 combineWith // diktat:WRONG_INDENTATION[expectedIndent = 8] 3 combineWith // diktat:WRONG_INDENTATION[expectedIndent = 8] 4 combineWith // diktat:WRONG_INDENTATION[expectedIndent = 8] 5 combineWith // diktat:WRONG_INDENTATION[expectedIndent = 8] 6 // diktat:WRONG_INDENTATION[expectedIndent = 8] } """, extendedIndentAfterOperators = TRUE)) fun `case 6`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun f(i1: Int, i2: Int, i3: Int): Int { if (i2 > 0 && i3 < 0) { // diktat:WRONG_INDENTATION[expectedIndent = 12] return 2 } return 0 } """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ fun f(i1: Int, i2: Int, i3: Int): Int { if (i2 > 0 && i3 < 0) { // diktat:WRONG_INDENTATION[expectedIndent = 8] return 2 } return 0 } """, extendedIndentAfterOperators = TRUE)) fun `case 7`() = Unit @IndentationTest( first = IndentedSourceCode( """ val value1 = 1 to 2 to // diktat:WRONG_INDENTATION[expectedIndent = 8] 3 // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ val value1 = 1 to 2 to // diktat:WRONG_INDENTATION[expectedIndent = 4] 3 // diktat:WRONG_INDENTATION[expectedIndent = 4] """, extendedIndentAfterOperators = TRUE)) fun `case 8`() = Unit @IndentationTest( first = IndentedSourceCode( """ val value1a = (1 to 2 to // diktat:WRONG_INDENTATION[expectedIndent = 8] 3) // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ val value1a = (1 to 2 to // diktat:WRONG_INDENTATION[expectedIndent = 4] 3) // diktat:WRONG_INDENTATION[expectedIndent = 4] """, extendedIndentAfterOperators = TRUE)) fun `case 9`() = Unit @IndentationTest( first = IndentedSourceCode( """ val value2 = 1 to 2 to // diktat:WRONG_INDENTATION[expectedIndent = 12] 3 // diktat:WRONG_INDENTATION[expectedIndent = 12] """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ val value2 = 1 to 2 to // diktat:WRONG_INDENTATION[expectedIndent = 8] 3 // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentAfterOperators = TRUE)) fun `case 10`() = Unit @IndentationTest( first = IndentedSourceCode( """ val value3 = (1 to 2 to // diktat:WRONG_INDENTATION[expectedIndent = 12] 3) // diktat:WRONG_INDENTATION[expectedIndent = 12] """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ val value3 = (1 to 2 to // diktat:WRONG_INDENTATION[expectedIndent = 8] 3) // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentAfterOperators = TRUE)) fun `case 11`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun identity(t: T): T = t val value4 = identity(1 to 2 to // diktat:WRONG_INDENTATION[expectedIndent = 8] 3) // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ fun identity(t: T): T = t val value4 = identity(1 to 2 to // diktat:WRONG_INDENTATION[expectedIndent = 4] 3) // diktat:WRONG_INDENTATION[expectedIndent = 4] """, extendedIndentAfterOperators = TRUE)) fun `case 12`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun identity(t: T): T = t val value5 = identity( 1 to 2 to // diktat:WRONG_INDENTATION[expectedIndent = 12] 3) // diktat:WRONG_INDENTATION[expectedIndent = 12] """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ fun identity(t: T): T = t val value5 = identity( 1 to 2 to // diktat:WRONG_INDENTATION[expectedIndent = 8] 3) // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentAfterOperators = TRUE)) fun `case 13`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun identity(t: T): T = t val value6 = identity(1 to 2 to // diktat:WRONG_INDENTATION[expectedIndent = 12] 3) // diktat:WRONG_INDENTATION[expectedIndent = 12] """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ fun identity(t: T): T = t val value6 = identity(1 to 2 to // diktat:WRONG_INDENTATION[expectedIndent = 8] 3) // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentAfterOperators = TRUE)) fun `case 14`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun identity(t: T): T = t /** * Line breaks: * * 1. before the expression body (`=`), * 2. before the effective function arguments, and * 3. on each infix function call ([to]). */ val value7 = identity( 1 to 2 to // diktat:WRONG_INDENTATION[expectedIndent = 16] 3) // diktat:WRONG_INDENTATION[expectedIndent = 16] """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ fun identity(t: T): T = t /** * Line breaks: * * 1. before the expression body (`=`), * 2. before the effective function arguments, and * 3. on each infix function call ([to]). */ val value7 = identity( 1 to 2 to // diktat:WRONG_INDENTATION[expectedIndent = 12] 3) // diktat:WRONG_INDENTATION[expectedIndent = 12] """, extendedIndentAfterOperators = TRUE)) fun `case 15`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun identity(t: T): T = t val value8 = identity(identity(1 to 2 to // diktat:WRONG_INDENTATION[expectedIndent = 8] 3)) // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ fun identity(t: T): T = t val value8 = identity(identity(1 to 2 to // diktat:WRONG_INDENTATION[expectedIndent = 4] 3)) // diktat:WRONG_INDENTATION[expectedIndent = 4] """, extendedIndentAfterOperators = TRUE)) fun `case 16`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun identity(t: T): T = t val value9 = identity(identity( 1 to 2 to // diktat:WRONG_INDENTATION[expectedIndent = 12] 3)) // diktat:WRONG_INDENTATION[expectedIndent = 12] """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ fun identity(t: T): T = t val value9 = identity(identity( 1 to 2 to // diktat:WRONG_INDENTATION[expectedIndent = 8] 3)) // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentAfterOperators = TRUE)) fun `case 17`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun identity(t: T): T = t val value10 = identity(identity(1 to 2 to // diktat:WRONG_INDENTATION[expectedIndent = 12] 3)) // diktat:WRONG_INDENTATION[expectedIndent = 12] """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ fun identity(t: T): T = t val value10 = identity(identity(1 to 2 to // diktat:WRONG_INDENTATION[expectedIndent = 8] 3)) // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentAfterOperators = TRUE)) fun `case 18`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun identity(t: T): T = t val value11 = identity(identity( 1 to 2 to // diktat:WRONG_INDENTATION[expectedIndent = 16] 3)) // diktat:WRONG_INDENTATION[expectedIndent = 16] """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ fun identity(t: T): T = t val value11 = identity(identity( 1 to 2 to // diktat:WRONG_INDENTATION[expectedIndent = 12] 3)) // diktat:WRONG_INDENTATION[expectedIndent = 12] """, extendedIndentAfterOperators = TRUE)) fun `case 19`() = Unit @IndentationTest( first = IndentedSourceCode( """ // Same as above, but using a custom getter instead of an explicit initializer. val value12 get() = 1 to 2 to // diktat:WRONG_INDENTATION[expectedIndent = 16] 3 // diktat:WRONG_INDENTATION[expectedIndent = 16] """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ // Same as above, but using a custom getter instead of an explicit initializer. val value12 get() = 1 to 2 to // diktat:WRONG_INDENTATION[expectedIndent = 12] 3 // diktat:WRONG_INDENTATION[expectedIndent = 12] """, extendedIndentAfterOperators = TRUE)) fun `case 20`() = Unit @IndentationTest( first = IndentedSourceCode( """ // Same as above, but using a custom getter instead of an explicit initializer. val value13 get() = (1 to 2 to // diktat:WRONG_INDENTATION[expectedIndent = 16] 3) // diktat:WRONG_INDENTATION[expectedIndent = 16] """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ // Same as above, but using a custom getter instead of an explicit initializer. val value13 get() = (1 to 2 to // diktat:WRONG_INDENTATION[expectedIndent = 12] 3) // diktat:WRONG_INDENTATION[expectedIndent = 12] """, extendedIndentAfterOperators = TRUE)) fun `case 21`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun identity(t: T): T = t // Same as above, but using a custom getter instead of an explicit initializer. val value14 get() = identity(1 to 2 to // diktat:WRONG_INDENTATION[expectedIndent = 16] 3) // diktat:WRONG_INDENTATION[expectedIndent = 16] """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ fun identity(t: T): T = t // Same as above, but using a custom getter instead of an explicit initializer. val value14 get() = identity(1 to 2 to // diktat:WRONG_INDENTATION[expectedIndent = 12] 3) // diktat:WRONG_INDENTATION[expectedIndent = 12] """, extendedIndentAfterOperators = TRUE)) fun `case 22`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun identity(t: T): T = t // Same as above, but using a custom getter instead of an explicit initializer. val value15 get() = identity(identity(1 to 2 to // diktat:WRONG_INDENTATION[expectedIndent = 16] 3)) // diktat:WRONG_INDENTATION[expectedIndent = 16] """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ fun identity(t: T): T = t // Same as above, but using a custom getter instead of an explicit initializer. val value15 get() = identity(identity(1 to 2 to // diktat:WRONG_INDENTATION[expectedIndent = 12] 3)) // diktat:WRONG_INDENTATION[expectedIndent = 12] """, extendedIndentAfterOperators = TRUE)) fun `case 23`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun f() { g(42 as Integer) // diktat:WRONG_INDENTATION[expectedIndent = 12] } """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ fun f() { g(42 as Integer) // diktat:WRONG_INDENTATION[expectedIndent = 8] } """, extendedIndentAfterOperators = TRUE)) fun `case 24`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun f() { g("" as? String?) // diktat:WRONG_INDENTATION[expectedIndent = 12] } """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ fun f() { g("" as? String?) // diktat:WRONG_INDENTATION[expectedIndent = 8] } """, extendedIndentAfterOperators = TRUE)) fun `case 25`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun f() { // The dot-qualified expression is always single-indented. "" .length as Int // diktat:WRONG_INDENTATION[expectedIndent = 12] } """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ fun f() { // The dot-qualified expression is always single-indented. "" .length as Int // diktat:WRONG_INDENTATION[expectedIndent = 8] } """, extendedIndentAfterOperators = TRUE)) fun `case 26`() = Unit } /** * Expressions wrapped before an operator or an infix function. * * See [#1340](https://github.com/saveourtool/diktat/issues/1340). */ @Nested @TestMethodOrder(NaturalDisplayName::class) inner class `Expressions wrapped before operator` { @IndentationTest( first = IndentedSourceCode( """ fun f() { systemUiVisibility = (View.SYSTEM_UI_FLAG_LOW_PROFILE or View.SYSTEM_UI_FLAG_FULLSCREEN // diktat:WRONG_INDENTATION[expectedIndent = 12] or View.SYSTEM_UI_FLAG_LAYOUT_STABLE // diktat:WRONG_INDENTATION[expectedIndent = 12] or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY // diktat:WRONG_INDENTATION[expectedIndent = 12] or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION // diktat:WRONG_INDENTATION[expectedIndent = 12] or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) // diktat:WRONG_INDENTATION[expectedIndent = 12] } """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ fun f() { systemUiVisibility = (View.SYSTEM_UI_FLAG_LOW_PROFILE or View.SYSTEM_UI_FLAG_FULLSCREEN // diktat:WRONG_INDENTATION[expectedIndent = 8] or View.SYSTEM_UI_FLAG_LAYOUT_STABLE // diktat:WRONG_INDENTATION[expectedIndent = 8] or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY // diktat:WRONG_INDENTATION[expectedIndent = 8] or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION // diktat:WRONG_INDENTATION[expectedIndent = 8] or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) // diktat:WRONG_INDENTATION[expectedIndent = 8] } """, extendedIndentAfterOperators = TRUE)) fun `case 1`() = Unit @IndentationTest( first = IndentedSourceCode( """ val systemUiVisibility = (View.SYSTEM_UI_FLAG_LOW_PROFILE or View.SYSTEM_UI_FLAG_FULLSCREEN // diktat:WRONG_INDENTATION[expectedIndent = 8] or View.SYSTEM_UI_FLAG_LAYOUT_STABLE // diktat:WRONG_INDENTATION[expectedIndent = 8] or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY // diktat:WRONG_INDENTATION[expectedIndent = 8] or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION // diktat:WRONG_INDENTATION[expectedIndent = 8] or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ val systemUiVisibility = (View.SYSTEM_UI_FLAG_LOW_PROFILE or View.SYSTEM_UI_FLAG_FULLSCREEN // diktat:WRONG_INDENTATION[expectedIndent = 4] or View.SYSTEM_UI_FLAG_LAYOUT_STABLE // diktat:WRONG_INDENTATION[expectedIndent = 4] or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY // diktat:WRONG_INDENTATION[expectedIndent = 4] or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION // diktat:WRONG_INDENTATION[expectedIndent = 4] or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) // diktat:WRONG_INDENTATION[expectedIndent = 4] """, extendedIndentAfterOperators = TRUE)) fun `case 2`() = Unit @IndentationTest( first = IndentedSourceCode( """ const val FOO = 1 const val BAR = 2 const val BAZ = 4 fun acceptInteger(arg: Int) = Unit fun main() { acceptInteger(FOO or BAR or BAZ or FOO or BAR or BAZ or FOO or BAR or BAZ or FOO or BAR or BAZ or FOO or BAR or BAZ // diktat:WRONG_INDENTATION[expectedIndent = 12] or FOO or BAR or BAZ) // diktat:WRONG_INDENTATION[expectedIndent = 12] } """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ const val FOO = 1 const val BAR = 2 const val BAZ = 4 fun acceptInteger(arg: Int) = Unit fun main() { acceptInteger(FOO or BAR or BAZ or FOO or BAR or BAZ or FOO or BAR or BAZ or FOO or BAR or BAZ or FOO or BAR or BAZ // diktat:WRONG_INDENTATION[expectedIndent = 8] or FOO or BAR or BAZ) // diktat:WRONG_INDENTATION[expectedIndent = 8] } """, extendedIndentAfterOperators = TRUE)) fun `case 3`() = Unit @IndentationTest( first = IndentedSourceCode( """ const val TRUE = true const val FALSE = false fun acceptBoolean(arg: Boolean) = Unit fun f() { acceptBoolean(TRUE || FALSE // diktat:WRONG_INDENTATION[expectedIndent = 12] || TRUE) // diktat:WRONG_INDENTATION[expectedIndent = 12] } """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ const val TRUE = true const val FALSE = false fun acceptBoolean(arg: Boolean) = Unit fun f() { acceptBoolean(TRUE || FALSE // diktat:WRONG_INDENTATION[expectedIndent = 8] || TRUE) // diktat:WRONG_INDENTATION[expectedIndent = 8] } """, extendedIndentAfterOperators = TRUE)) fun `case 4`() = Unit @IndentationTest( first = IndentedSourceCode( """ val c = (3 + 2) // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ val c = (3 + 2) // diktat:WRONG_INDENTATION[expectedIndent = 4] """, extendedIndentAfterOperators = TRUE)) fun `case 5`() = Unit @IndentationTest( first = IndentedSourceCode( """ infix fun Int.combineWith(that: Int) = this + that fun f() { val x = (1 combineWith 2 // diktat:WRONG_INDENTATION[expectedIndent = 12] combineWith 3 // diktat:WRONG_INDENTATION[expectedIndent = 12] combineWith 4 // diktat:WRONG_INDENTATION[expectedIndent = 12] combineWith 5 // diktat:WRONG_INDENTATION[expectedIndent = 12] combineWith 6) // diktat:WRONG_INDENTATION[expectedIndent = 12] } """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ infix fun Int.combineWith(that: Int) = this + that fun f() { val x = (1 combineWith 2 // diktat:WRONG_INDENTATION[expectedIndent = 8] combineWith 3 // diktat:WRONG_INDENTATION[expectedIndent = 8] combineWith 4 // diktat:WRONG_INDENTATION[expectedIndent = 8] combineWith 5 // diktat:WRONG_INDENTATION[expectedIndent = 8] combineWith 6) // diktat:WRONG_INDENTATION[expectedIndent = 8] } """, extendedIndentAfterOperators = TRUE)) fun `case 6`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun f(i1: Int, i2: Int, i3: Int): Int { if (i2 > 0 && i3 < 0) { // diktat:WRONG_INDENTATION[expectedIndent = 12] return 2 } return 0 } """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ fun f(i1: Int, i2: Int, i3: Int): Int { if (i2 > 0 && i3 < 0) { // diktat:WRONG_INDENTATION[expectedIndent = 8] return 2 } return 0 } """, extendedIndentAfterOperators = TRUE)) fun `case 7`() = Unit @IndentationTest( first = IndentedSourceCode( """ val value1 = (1 to 2 // diktat:WRONG_INDENTATION[expectedIndent = 8] to 3) // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ val value1 = (1 to 2 // diktat:WRONG_INDENTATION[expectedIndent = 4] to 3) // diktat:WRONG_INDENTATION[expectedIndent = 4] """, extendedIndentAfterOperators = TRUE)) fun `case 8`() = Unit @IndentationTest( first = IndentedSourceCode( """ val value2 = (1 to 2 // diktat:WRONG_INDENTATION[expectedIndent = 12] to 3) // diktat:WRONG_INDENTATION[expectedIndent = 12] """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ val value2 = (1 to 2 // diktat:WRONG_INDENTATION[expectedIndent = 8] to 3) // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentAfterOperators = TRUE)) fun `case 9`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun identity(t: T): T = t val value3 = identity(1 to 2 // diktat:WRONG_INDENTATION[expectedIndent = 8] to 3) // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ fun identity(t: T): T = t val value3 = identity(1 to 2 // diktat:WRONG_INDENTATION[expectedIndent = 4] to 3) // diktat:WRONG_INDENTATION[expectedIndent = 4] """, extendedIndentAfterOperators = TRUE)) fun `case 10`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun identity(t: T): T = t val value4 = identity( 1 to 2 // diktat:WRONG_INDENTATION[expectedIndent = 12] to 3) // diktat:WRONG_INDENTATION[expectedIndent = 12] """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ fun identity(t: T): T = t val value4 = identity( 1 to 2 // diktat:WRONG_INDENTATION[expectedIndent = 8] to 3) // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentAfterOperators = TRUE)) fun `case 11`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun identity(t: T): T = t val value5 = identity(1 to 2 // diktat:WRONG_INDENTATION[expectedIndent = 12] to 3) // diktat:WRONG_INDENTATION[expectedIndent = 12] """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ fun identity(t: T): T = t val value5 = identity(1 to 2 // diktat:WRONG_INDENTATION[expectedIndent = 8] to 3) // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentAfterOperators = TRUE)) fun `case 12`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun identity(t: T): T = t /** * Line breaks: * * 1. before the expression body (`=`), * 2. before the effective function arguments, and * 3. on each infix function call ([to]). */ val value6 = identity( 1 to 2 // diktat:WRONG_INDENTATION[expectedIndent = 16] to 3) // diktat:WRONG_INDENTATION[expectedIndent = 16] """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ fun identity(t: T): T = t /** * Line breaks: * * 1. before the expression body (`=`), * 2. before the effective function arguments, and * 3. on each infix function call ([to]). */ val value6 = identity( 1 to 2 // diktat:WRONG_INDENTATION[expectedIndent = 12] to 3) // diktat:WRONG_INDENTATION[expectedIndent = 12] """, extendedIndentAfterOperators = TRUE)) fun `case 13`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun identity(t: T): T = t val value7 = identity(identity(1 to 2 // diktat:WRONG_INDENTATION[expectedIndent = 8] to 3)) // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ fun identity(t: T): T = t val value7 = identity(identity(1 to 2 // diktat:WRONG_INDENTATION[expectedIndent = 4] to 3)) // diktat:WRONG_INDENTATION[expectedIndent = 4] """, extendedIndentAfterOperators = TRUE)) fun `case 14`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun identity(t: T): T = t val value8 = identity(identity( 1 to 2 // diktat:WRONG_INDENTATION[expectedIndent = 12] to 3)) // diktat:WRONG_INDENTATION[expectedIndent = 12] """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ fun identity(t: T): T = t val value8 = identity(identity( 1 to 2 // diktat:WRONG_INDENTATION[expectedIndent = 8] to 3)) // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentAfterOperators = TRUE)) fun `case 15`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun identity(t: T): T = t val value9 = identity(identity(1 to 2 // diktat:WRONG_INDENTATION[expectedIndent = 12] to 3)) // diktat:WRONG_INDENTATION[expectedIndent = 12] """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ fun identity(t: T): T = t val value9 = identity(identity(1 to 2 // diktat:WRONG_INDENTATION[expectedIndent = 8] to 3)) // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentAfterOperators = TRUE)) fun `case 16`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun identity(t: T): T = t val value10 = identity(identity( 1 to 2 // diktat:WRONG_INDENTATION[expectedIndent = 16] to 3)) // diktat:WRONG_INDENTATION[expectedIndent = 16] """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ fun identity(t: T): T = t val value10 = identity(identity( 1 to 2 // diktat:WRONG_INDENTATION[expectedIndent = 12] to 3)) // diktat:WRONG_INDENTATION[expectedIndent = 12] """, extendedIndentAfterOperators = TRUE)) fun `case 17`() = Unit @IndentationTest( first = IndentedSourceCode( """ // Same as above, but using a custom getter instead of an explicit initializer. val value11 get() = (1 to 2 // diktat:WRONG_INDENTATION[expectedIndent = 16] to 3) // diktat:WRONG_INDENTATION[expectedIndent = 16] """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ // Same as above, but using a custom getter instead of an explicit initializer. val value11 get() = (1 to 2 // diktat:WRONG_INDENTATION[expectedIndent = 12] to 3) // diktat:WRONG_INDENTATION[expectedIndent = 12] """, extendedIndentAfterOperators = TRUE)) fun `case 18`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun identity(t: T): T = t // Same as above, but using a custom getter instead of an explicit initializer. val value12 get() = identity(1 to 2 // diktat:WRONG_INDENTATION[expectedIndent = 16] to 3) // diktat:WRONG_INDENTATION[expectedIndent = 16] """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ fun identity(t: T): T = t // Same as above, but using a custom getter instead of an explicit initializer. val value12 get() = identity(1 to 2 // diktat:WRONG_INDENTATION[expectedIndent = 12] to 3) // diktat:WRONG_INDENTATION[expectedIndent = 12] """, extendedIndentAfterOperators = TRUE)) fun `case 19`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun identity(t: T): T = t // Same as above, but using a custom getter instead of an explicit initializer. val value13 get() = identity(identity(1 to 2 // diktat:WRONG_INDENTATION[expectedIndent = 16] to 3)) // diktat:WRONG_INDENTATION[expectedIndent = 16] """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ fun identity(t: T): T = t // Same as above, but using a custom getter instead of an explicit initializer. val value13 get() = identity(identity(1 to 2 // diktat:WRONG_INDENTATION[expectedIndent = 12] to 3)) // diktat:WRONG_INDENTATION[expectedIndent = 12] """, extendedIndentAfterOperators = TRUE)) fun `case 20`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun f() { g(42 as Integer) // diktat:WRONG_INDENTATION[expectedIndent = 12] } """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ fun f() { g(42 as Integer) // diktat:WRONG_INDENTATION[expectedIndent = 8] } """, extendedIndentAfterOperators = TRUE)) fun `case 21`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun f() { g("" as? String?) // diktat:WRONG_INDENTATION[expectedIndent = 12] } """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ fun f() { g("" as? String?) // diktat:WRONG_INDENTATION[expectedIndent = 8] } """, extendedIndentAfterOperators = TRUE)) fun `case 22`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun f() { // The dot-qualified expression is always single-indented. "" .length as Int // diktat:WRONG_INDENTATION[expectedIndent = 12] } """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ fun f() { // The dot-qualified expression is always single-indented. "" .length as Int // diktat:WRONG_INDENTATION[expectedIndent = 8] } """, extendedIndentAfterOperators = TRUE)) fun `case 23`() = Unit } /** * Parenthesized expressions. * * See [#1409](https://github.com/saveourtool/diktat/issues/1409). */ @Nested @TestMethodOrder(NaturalDisplayName::class) inner class `Parentheses-surrounded infix expressions` { @IndentationTest( first = IndentedSourceCode( """ fun f1() = ( 1 + 2 // diktat:WRONG_INDENTATION[expectedIndent = 4] ) """, extendedIndentForExpressionBodies = FALSE, extendedIndentAfterOperators = TRUE), second = IndentedSourceCode( """ fun f1() = ( 1 + 2 // diktat:WRONG_INDENTATION[expectedIndent = 8] ) """, extendedIndentForExpressionBodies = TRUE, extendedIndentAfterOperators = FALSE)) fun `case 1`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun f2() = ( 1 + 2) // diktat:WRONG_INDENTATION[expectedIndent = 4] """, extendedIndentForExpressionBodies = FALSE, extendedIndentAfterOperators = TRUE), second = IndentedSourceCode( """ fun f2() = ( 1 + 2) // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentForExpressionBodies = TRUE, extendedIndentAfterOperators = FALSE)) fun `case 2`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun f3() = ( // diktat:WRONG_INDENTATION[expectedIndent = 8] 1 + 2 ) // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentForExpressionBodies = FALSE, extendedIndentAfterOperators = TRUE), second = IndentedSourceCode( """ fun f3() = ( // diktat:WRONG_INDENTATION[expectedIndent = 4] 1 + 2 ) // diktat:WRONG_INDENTATION[expectedIndent = 4] """, extendedIndentForExpressionBodies = TRUE, extendedIndentAfterOperators = FALSE)) fun `case 3`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun f4() = ( // diktat:WRONG_INDENTATION[expectedIndent = 8] 1 + 2) """, extendedIndentForExpressionBodies = FALSE, extendedIndentAfterOperators = TRUE), second = IndentedSourceCode( """ fun f4() = ( // diktat:WRONG_INDENTATION[expectedIndent = 4] 1 + 2) """, extendedIndentForExpressionBodies = TRUE, extendedIndentAfterOperators = FALSE)) fun `case 4`() = Unit @IndentationTest( first = IndentedSourceCode( """ const val v1 = ( 1 + 2 // diktat:WRONG_INDENTATION[expectedIndent = 4] ) """, extendedIndentForExpressionBodies = FALSE, extendedIndentAfterOperators = TRUE), second = IndentedSourceCode( """ const val v1 = ( 1 + 2 // diktat:WRONG_INDENTATION[expectedIndent = 8] ) """, extendedIndentForExpressionBodies = TRUE, extendedIndentAfterOperators = FALSE)) fun `case 5`() = Unit @IndentationTest( first = IndentedSourceCode( """ const val v2 = ( 1 + 2) // diktat:WRONG_INDENTATION[expectedIndent = 4] """, extendedIndentForExpressionBodies = FALSE, extendedIndentAfterOperators = TRUE), second = IndentedSourceCode( """ const val v2 = ( 1 + 2) // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentForExpressionBodies = TRUE, extendedIndentAfterOperators = FALSE)) fun `case 6`() = Unit @IndentationTest( first = IndentedSourceCode( """ const val v3 = ( // diktat:WRONG_INDENTATION[expectedIndent = 8] 1 + 2 ) // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentForExpressionBodies = FALSE, extendedIndentAfterOperators = TRUE), second = IndentedSourceCode( """ const val v3 = ( // diktat:WRONG_INDENTATION[expectedIndent = 4] 1 + 2 ) // diktat:WRONG_INDENTATION[expectedIndent = 4] """, extendedIndentForExpressionBodies = TRUE, extendedIndentAfterOperators = FALSE)) fun `case 7`() = Unit @IndentationTest( first = IndentedSourceCode( """ const val v4 = ( // diktat:WRONG_INDENTATION[expectedIndent = 8] 1 + 2) """, extendedIndentForExpressionBodies = FALSE, extendedIndentAfterOperators = TRUE), second = IndentedSourceCode( """ const val v4 = ( // diktat:WRONG_INDENTATION[expectedIndent = 4] 1 + 2) """, extendedIndentForExpressionBodies = TRUE, extendedIndentAfterOperators = FALSE)) fun `case 8`() = Unit } /** * Dot-qualified and safe-access expressions. * * See [#1336](https://github.com/saveourtool/diktat/issues/1336). */ @Nested @TestMethodOrder(NaturalDisplayName::class) inner class `Dot- and safe-qualified expressions` { @IndentationTest( first = IndentedSourceCode( """ fun LocalDateTime.updateTime( hour: Int? = null, minute: Int? = null, second: Int? = null, ): LocalDateTime = withHour(hour ?: getHour()) .withMinute(minute ?: getMinute()) // diktat:WRONG_INDENTATION[expectedIndent = 8] .withSecond(second ?: getSecond()) // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentBeforeDot = FALSE), second = IndentedSourceCode( """ fun LocalDateTime.updateTime( hour: Int? = null, minute: Int? = null, second: Int? = null, ): LocalDateTime = withHour(hour ?: getHour()) .withMinute(minute ?: getMinute()) // diktat:WRONG_INDENTATION[expectedIndent = 4] .withSecond(second ?: getSecond()) // diktat:WRONG_INDENTATION[expectedIndent = 4] """, extendedIndentBeforeDot = TRUE)) fun `case 1`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun f() { first() .second() // diktat:WRONG_INDENTATION[expectedIndent = 12] .third() // diktat:WRONG_INDENTATION[expectedIndent = 12] } """, extendedIndentBeforeDot = FALSE), second = IndentedSourceCode( """ fun f() { first() .second() // diktat:WRONG_INDENTATION[expectedIndent = 8] .third() // diktat:WRONG_INDENTATION[expectedIndent = 8] } """, extendedIndentBeforeDot = TRUE)) fun `case 2`() = Unit @IndentationTest( first = IndentedSourceCode( """ val a = first() .second() // diktat:WRONG_INDENTATION[expectedIndent = 8] .third() // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentBeforeDot = FALSE), second = IndentedSourceCode( """ val a = first() .second() // diktat:WRONG_INDENTATION[expectedIndent = 4] .third() // diktat:WRONG_INDENTATION[expectedIndent = 4] """, extendedIndentBeforeDot = TRUE)) fun `case 3`() = Unit @IndentationTest( first = IndentedSourceCode( """ val b = first() ?.second() // diktat:WRONG_INDENTATION[expectedIndent = 8] ?.third() // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentBeforeDot = FALSE), second = IndentedSourceCode( """ val b = first() ?.second() // diktat:WRONG_INDENTATION[expectedIndent = 4] ?.third() // diktat:WRONG_INDENTATION[expectedIndent = 4] """, extendedIndentBeforeDot = TRUE)) fun `case 4`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun f1() = first() .second() // diktat:WRONG_INDENTATION[expectedIndent = 8] .third() // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentBeforeDot = FALSE), second = IndentedSourceCode( """ fun f1() = first() .second() // diktat:WRONG_INDENTATION[expectedIndent = 4] .third() // diktat:WRONG_INDENTATION[expectedIndent = 4] """, extendedIndentBeforeDot = TRUE)) fun `case 5`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun f2() = first() .second() // diktat:WRONG_INDENTATION[expectedIndent = 12] .third() // diktat:WRONG_INDENTATION[expectedIndent = 12] """, extendedIndentBeforeDot = FALSE), second = IndentedSourceCode( """ fun f2() = first() .second() // diktat:WRONG_INDENTATION[expectedIndent = 8] .third() // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentBeforeDot = TRUE)) fun `case 6`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun f3() = g(first() .second() // diktat:WRONG_INDENTATION[expectedIndent = 8] .third() // diktat:WRONG_INDENTATION[expectedIndent = 8] .fourth()) // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentBeforeDot = FALSE), second = IndentedSourceCode( """ fun f3() = g(first() .second() // diktat:WRONG_INDENTATION[expectedIndent = 4] .third() // diktat:WRONG_INDENTATION[expectedIndent = 4] .fourth()) // diktat:WRONG_INDENTATION[expectedIndent = 4] """, extendedIndentBeforeDot = TRUE)) fun `case 7`() = Unit @IndentationTest( first = IndentedSourceCode( """ fun f4() = g( first() .second() // diktat:WRONG_INDENTATION[expectedIndent = 12] .third() // diktat:WRONG_INDENTATION[expectedIndent = 12] .fourth()) // diktat:WRONG_INDENTATION[expectedIndent = 12] """, extendedIndentBeforeDot = FALSE), second = IndentedSourceCode( """ fun f4() = g( first() .second() // diktat:WRONG_INDENTATION[expectedIndent = 8] .third() // diktat:WRONG_INDENTATION[expectedIndent = 8] .fourth()) // diktat:WRONG_INDENTATION[expectedIndent = 8] """, extendedIndentBeforeDot = TRUE)) fun `case 8`() = Unit } @Nested @TestMethodOrder(NaturalDisplayName::class) inner class `If expressions` { /** * #1351, case 1. * * Boolean operator priority (`&&` has higher priority than `||`). * * Currently, this is an incorrectly formatted code kept to detect the * contract breakage. It will be re-formatted once the issue is fixed. */ @IndentationTest( first = IndentedSourceCode( """ fun f1() { if (valueParameterNode.parents().none { it.elementType == PRIMARY_CONSTRUCTOR } || !valueParameterNode.hasChildOfType(VAL_KEYWORD) && // diktat:WRONG_INDENTATION[expectedIndent = 12] !valueParameterNode.hasChildOfType(VAR_KEYWORD) // diktat:WRONG_INDENTATION[expectedIndent = 16] ) { return } } """, extendedIndentAfterOperators = FALSE), second = IndentedSourceCode( """ fun f1() { if (valueParameterNode.parents().none { it.elementType == PRIMARY_CONSTRUCTOR } || !valueParameterNode.hasChildOfType(VAL_KEYWORD) && // diktat:WRONG_INDENTATION[expectedIndent = 8] !valueParameterNode.hasChildOfType(VAR_KEYWORD) // diktat:WRONG_INDENTATION[expectedIndent = 16] ) { return } } """, extendedIndentAfterOperators = TRUE)) fun `case 1`() = Unit /** * #1351, case 2. * * IDEA combines the values of `CONTINUATION_INDENT_IN_IF_CONDITIONS` * and `CONTINUATION_INDENT_FOR_CHAINED_CALLS`, so the resulting indent * can be anything between 8 (2x) and 16 (4x). * * Currently, this is an incorrectly formatted code kept to detect the * contract breakage. It will be re-formatted once the issue is fixed. */ @IndentationTest( first = IndentedSourceCode( """ fun f2() { val prevComment = if (valueParameterNode.siblings(forward = false) .takeWhile { it.elementType != EOL_COMMENT && it.elementType != BLOCK_COMMENT } // diktat:WRONG_INDENTATION[expectedIndent = 12] .all { it.elementType == WHITE_SPACE } // diktat:WRONG_INDENTATION[expectedIndent = 12] ) { 0 } else { 1 } } """, extendedIndentBeforeDot = FALSE), second = IndentedSourceCode( """ fun f2() { val prevComment = if (valueParameterNode.siblings(forward = false) .takeWhile { it.elementType != EOL_COMMENT && it.elementType != BLOCK_COMMENT } // diktat:WRONG_INDENTATION[expectedIndent = 8] .all { it.elementType == WHITE_SPACE } // diktat:WRONG_INDENTATION[expectedIndent = 8] ) { 0 } else { 1 } } """, extendedIndentBeforeDot = TRUE)) fun `case 2`() = Unit } @Nested @TestMethodOrder(NaturalDisplayName::class) inner class `Comments inside binary expressions` { @IndentationTest( IndentedSourceCode( """ val x: Int = functionCall() // This is a comment ?: 42 """), singleConfiguration = true) fun `case 1`() = Unit @IndentationTest( IndentedSourceCode( """ val x: Int = functionCall() as Int? // This is a comment ?: 42 """), singleConfiguration = true) fun `case 2`() = Unit @IndentationTest( IndentedSourceCode( """ val x: Int = null as Int? // This is a comment ?: 42 """), singleConfiguration = true) fun `case 3`() = Unit @IndentationTest( IndentedSourceCode( """ val x: Int = 42 as Int? // This is a comment ?: 42 """), singleConfiguration = true) fun `case 4`() = Unit @IndentationTest( IndentedSourceCode( """ val x: Boolean = functionCall() /* * This is a block comment */ ?: true """), singleConfiguration = true) fun `case 5`() = Unit @IndentationTest( IndentedSourceCode( """ val x: Boolean = functionCall() as Boolean? /* * This is a block comment */ ?: true """), singleConfiguration = true) fun `case 6`() = Unit @IndentationTest( IndentedSourceCode( """ val x: Boolean = null as Boolean? /* * This is a block comment */ ?: true """), singleConfiguration = true) fun `case 7`() = Unit @IndentationTest( IndentedSourceCode( """ val x: Boolean = true as Boolean? /* * This is a block comment */ ?: true """), singleConfiguration = true) fun `case 8`() = Unit @IndentationTest( IndentedSourceCode( """ fun f(): String { return functionCall() // This is a comment // This is the 2nd line of the comment ?: "default value" } """), singleConfiguration = true) fun `case 9`() = Unit @IndentationTest( IndentedSourceCode( """ fun f(): String { return functionCall() as String? // This is a comment // This is the 2nd line of the comment ?: "default value" } """), singleConfiguration = true) fun `case 10`() = Unit @IndentationTest( IndentedSourceCode( """ fun f(): String { return null as String? // This is a comment // This is the 2nd line of the comment ?: "default value" } """), singleConfiguration = true) fun `case 11`() = Unit @IndentationTest( IndentedSourceCode( """ fun f(): String { return "return value" as String? // This is a comment // This is the 2nd line of the comment ?: "default value" // This is a comment // This is the 2nd line of the comment ?: "unreachable code" } """), singleConfiguration = true) fun `case 12`() = Unit } @Nested @TestMethodOrder(NaturalDisplayName::class) inner class `Multi-line Elvis expressions` { @IndentationTest( IndentedSourceCode( """ val elvisExpressionInsideBinaryExpressionA = true && "" ?.trim() ?.trim() ?.trim() ?.isEmpty() ?: true """), singleConfiguration = true) fun `case 1`() = Unit @IndentationTest( IndentedSourceCode( """ val elvisExpressionInsideBinaryExpressionB = false || "" .trim() .trim() .trim() .isEmpty() ?: true """), singleConfiguration = true) fun `case 2`() = Unit @IndentationTest( IndentedSourceCode( """ val elvisExpressionInsideBinaryExpressionC = true && null as Boolean? ?: true """), singleConfiguration = true) fun `case 3`() = Unit @IndentationTest( IndentedSourceCode( """ val elvisExpressionInsideBinaryExpressionD = false || (null as Boolean?) ?: true """), singleConfiguration = true) fun `case 4`() = Unit @IndentationTest( IndentedSourceCode( """ val elvisExpressionInsideBinaryExpressionE = true && (42 as? Boolean) ?: true """), singleConfiguration = true) fun `case 5`() = Unit /** * _Elvis_ after a _safe-access_ expression should have the same * indentation level as the previous function calls. */ @IndentationTest( IndentedSourceCode( """ val elvisAfterSafeAccess = "" ?.trim() ?.trim() ?.trim() ?.isEmpty() ?: "" """), singleConfiguration = true) fun `case 6`() = Unit /** * _Elvis_ after a _dot-qualified_ expression should have the same * indentation level as the previous function calls. */ @IndentationTest( IndentedSourceCode( """ val elvisAfterDotQualified = "" .trim() .trim() .trim() .isEmpty() ?: "" """), singleConfiguration = true) fun `case 7`() = Unit @IndentationTest( IndentedSourceCode( """ fun f(): Boolean { return list.getChildren(null) .none { it.elementType in badModifiers } && classBody?.getAllChildrenWithType(FUN) ?.isEmpty() ?: false && getFirstChildWithType(SUPER_TYPE_LIST) == null } """), singleConfiguration = true) fun `case 8`() = Unit @IndentationTest( IndentedSourceCode( """ fun f(): Boolean { return classBody?.getFirstChildWithType(FUN) == null && getFirstChildWithType(SUPER_TYPE_LIST) == null && // if there is any prop with logic in accessor then don't recommend to convert class to data class classBody?.let(::areGoodProps) ?: true } """), singleConfiguration = true) fun `case 9`() = Unit @IndentationTest( IndentedSourceCode( """ fun f(): Boolean { return block.getChildrenOfType() .any { it.nameAsName == property.nameAsName && expression.node.isGoingAfter(it.node) } || block.parent .let { it as? KtFunctionLiteral } ?.valueParameters ?.any { it.nameAsName == property.nameAsName } ?: false } """), singleConfiguration = true) fun `case 10`() = Unit @IndentationTest( IndentedSourceCode( """ fun f(): Boolean { return blockExpression .statements .takeWhile { !it.isAncestor(this, true) } .mapNotNull { it as? KtProperty } .find { it.isLocal && it.hasInitializer() && it.name?.equals(getReferencedName()) ?: false } } """), singleConfiguration = true) fun `case 11`() = Unit @IndentationTest( IndentedSourceCode( """ fun f(): Any { return siblings(forward = true, withItself = false) .filterNot { it.node.isPartOfComment() || it is PsiWhiteSpace } .takeWhile { // statements like `name.field = value` where name == propertyName it is KtBinaryExpression && it.node.findChildByType(OPERATION_REFERENCE)?.findChildByType(EQ) != null && (it.left as? KtDotQualifiedExpression)?.run { (receiverExpression as? KtNameReferenceExpression)?.getReferencedName() == propertyName } ?: false } } """), singleConfiguration = true) fun `case 12`() = Unit @IndentationTest( IndentedSourceCode( """ fun f(): Any { return blockExpression .statements .takeWhile { !it.isAncestor(this, true) } .mapNotNull { it as? KtProperty } .find { it.isLocal && it.hasInitializer() && it.name?.equals(getReferencedName()) ?: false } } """), singleConfiguration = true) fun `case 13`() = Unit } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/spaces/IndentationRuleTestSuite.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3.spaces import org.junit.platform.suite.api.SelectClasses import org.junit.platform.suite.api.Suite /** * Runs all indentation rule tests. */ @Suite @SelectClasses( IndentationRuleWarnTest::class, IndentationRuleFixTest::class, IndentationRuleTest::class, ) class IndentationRuleTestSuite ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/spaces/IndentationRuleTestUtils.kt ================================================ @file:JvmName("IndentationRuleTestUtils") @file:Suppress("HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE") package com.saveourtool.diktat.ruleset.chapter3.spaces import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.WRONG_INDENTATION import com.saveourtool.diktat.ruleset.utils.NEWLINE import com.saveourtool.diktat.ruleset.utils.indentation.IndentationConfig import com.saveourtool.diktat.ruleset.utils.indentation.IndentationConfig.Companion.ALIGNED_PARAMETERS import com.saveourtool.diktat.ruleset.utils.indentation.IndentationConfig.Companion.EXTENDED_INDENT_AFTER_OPERATORS import com.saveourtool.diktat.ruleset.utils.indentation.IndentationConfig.Companion.EXTENDED_INDENT_BEFORE_DOT import com.saveourtool.diktat.ruleset.utils.indentation.IndentationConfig.Companion.EXTENDED_INDENT_FOR_EXPRESSION_BODIES import com.saveourtool.diktat.ruleset.utils.indentation.IndentationConfig.Companion.EXTENDED_INDENT_OF_PARAMETERS import com.saveourtool.diktat.ruleset.utils.indentation.IndentationConfig.Companion.INDENTATION_SIZE import com.saveourtool.diktat.ruleset.utils.indentation.IndentationConfig.Companion.NEWLINE_AT_END /** * @param configEntries the optional values which override the state of this * [IndentationConfig]. * @return the content of this [IndentationConfig] as a map, with some * configuration entries overridden via [configEntries]. */ internal fun IndentationConfig.withCustomParameters(vararg configEntries: Pair): Map = mutableMapOf( ALIGNED_PARAMETERS to alignedParameters.toString(), INDENTATION_SIZE to indentationSize.toString(), NEWLINE_AT_END to newlineAtEnd.toString(), EXTENDED_INDENT_OF_PARAMETERS to extendedIndentOfParameters.toString(), EXTENDED_INDENT_FOR_EXPRESSION_BODIES to extendedIndentForExpressionBodies.toString(), EXTENDED_INDENT_AFTER_OPERATORS to extendedIndentAfterOperators.toString(), EXTENDED_INDENT_BEFORE_DOT to extendedIndentBeforeDot.toString(), ).apply { configEntries.forEach { (key, value) -> this[key] = value.toString() } } /** * @param configEntries the optional values which override the state of this * [IndentationConfig]. * @return the content of this [IndentationConfig] as a map, with some * configuration entries overridden via [configEntries]. */ internal fun IndentationConfig.withCustomParameters(configEntries: Map): Map = withCustomParameters(*configEntries .asSequence() .map { (key, value) -> key to value }.toList() .toTypedArray()) /** * Converts this map to a list containing a single [RulesConfig]. * * @return the list containing a single [RulesConfig] entry. */ internal fun Map.asRulesConfigList(): List = listOf( RulesConfig( name = WRONG_INDENTATION.name, enabled = true, configuration = this ) ) ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/spaces/IndentationRuleWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3.spaces import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.WRONG_INDENTATION import com.saveourtool.diktat.ruleset.junit.NaturalDisplayName import com.saveourtool.diktat.ruleset.rules.chapter3.files.IndentationRule import com.saveourtool.diktat.ruleset.utils.indentation.IndentationConfig.Companion.ALIGNED_PARAMETERS import com.saveourtool.diktat.ruleset.utils.indentation.IndentationConfig.Companion.EXTENDED_INDENT_AFTER_OPERATORS import com.saveourtool.diktat.ruleset.utils.indentation.IndentationConfig.Companion.EXTENDED_INDENT_BEFORE_DOT import com.saveourtool.diktat.ruleset.utils.indentation.IndentationConfig.Companion.EXTENDED_INDENT_FOR_EXPRESSION_BODIES import com.saveourtool.diktat.ruleset.utils.indentation.IndentationConfig.Companion.EXTENDED_INDENT_OF_PARAMETERS import com.saveourtool.diktat.ruleset.utils.indentation.IndentationConfig.Companion.INDENTATION_SIZE import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.util.TEST_FILE_NAME import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestMethodOrder import org.junit.jupiter.api.io.TempDir import java.nio.file.Path /** * Legacy indentation tests. * * Consider adding new tests to [IndentationRuleTest] instead. * * @see IndentationRuleTest */ @Suppress("LargeClass") @TestMethodOrder(NaturalDisplayName::class) class IndentationRuleWarnTest : LintTestBase(::IndentationRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${IndentationRule.NAME_ID}" private val rulesConfigList = listOf( RulesConfig(WRONG_INDENTATION.name, true, mapOf( EXTENDED_INDENT_OF_PARAMETERS to "true", ALIGNED_PARAMETERS to "true", EXTENDED_INDENT_FOR_EXPRESSION_BODIES to "true", EXTENDED_INDENT_AFTER_OPERATORS to "true", EXTENDED_INDENT_BEFORE_DOT to "false", INDENTATION_SIZE to "4" ) ) ) private val disabledOptionsRulesConfigList = listOf( RulesConfig(WRONG_INDENTATION.name, true, mapOf( EXTENDED_INDENT_OF_PARAMETERS to "false", ALIGNED_PARAMETERS to "false", EXTENDED_INDENT_FOR_EXPRESSION_BODIES to "false", EXTENDED_INDENT_AFTER_OPERATORS to "false", EXTENDED_INDENT_BEFORE_DOT to "false", INDENTATION_SIZE to "4" ) ) ) @Test @Tag(WarningNames.WRONG_INDENTATION) fun `should warn if tabs are used in indentation`() { lintMethod( """ |class Example { |${"\t"}val zero = 0 |} | """.trimMargin(), DiktatError(2, 1, ruleId, "${WRONG_INDENTATION.warnText()} tabs are not allowed for indentation", true) ) } @Test @Tag(WarningNames.WRONG_INDENTATION) fun `should warn if indent size is not 4 spaces`() { lintMethod( """ |class Example { | val zero = 0 |} | """.trimMargin(), DiktatError(2, 1, ruleId, warnText(4, 3), true) ) } @Test @Tag(WarningNames.WRONG_INDENTATION) fun `should warn if no new line at the end of file`(@TempDir tempDir: Path) { lintMethodWithFile( """ |class Example { | val zero = 0 |} """.trimMargin(), tempDir = tempDir, fileName = TEST_FILE_NAME, DiktatError(3, 1, ruleId, "${WRONG_INDENTATION.warnText()} no newline at the end of file $TEST_FILE_NAME", true) ) } @Test @Tag(WarningNames.WRONG_INDENTATION) fun `should warn if no new line at the end of file, last child whitespace`(@TempDir tempDir: Path) { lintMethodWithFile( """ |class Example { | val zero = 0 |} """.trimMargin(), tempDir = tempDir, fileName = TEST_FILE_NAME, DiktatError(3, 1, ruleId, "${WRONG_INDENTATION.warnText()} no newline at the end of file $TEST_FILE_NAME", true) ) } @Test @Tag(WarningNames.WRONG_INDENTATION) fun `should warn if too many blank lines at the end of file`(@TempDir tempDir: Path) { lintMethodWithFile( """ |class Example { | val zero = 0 |} | | """.trimMargin(), tempDir = tempDir, fileName = TEST_FILE_NAME, DiktatError(5, 1, ruleId, "${WRONG_INDENTATION.warnText()} too many blank lines at the end of file $TEST_FILE_NAME", true) ) } @Test @Tag(WarningNames.WRONG_INDENTATION) fun `valid indentation - example 1`() { lintMethod( """ |class Example { | private val foo = 0 | private val fuu = | 0 | | fun bar() { | if (foo > 0) { | baz() | } else { | bazz() | } | return foo | } |} | """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_INDENTATION) fun `parameters can be indented by 8 spaces - positive example`() { lintMethod( """ |class Example( | val field1: Type1, | val field2: Type2, | val field3: Type3 |) { | val e1 = Example( | t1, | t2, | t3 | ) | | val e2 = Example(t1, t2, | t3 | ) |} | """.trimMargin(), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.WRONG_INDENTATION) fun `parameters can be aligned - positive example`() { lintMethod( """ |class Example(val field1: Type1, | val field2: Type2, | val field3: Type3) { |} | """.trimMargin(), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.WRONG_INDENTATION) fun `parameters can be aligned`() { lintMethod( """ |class Example( | val field1: Type1, | val field2: Type2, | val field3: Type3) { |} | """.trimMargin(), DiktatError(2, 1, ruleId, warnText(8, 14), true), DiktatError(3, 1, ruleId, warnText(8, 14), true), DiktatError(4, 1, ruleId, warnText(8, 14), true), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.WRONG_INDENTATION) fun `lines split by operator can be indented by 8 spaces`() { lintMethod( """ |fun foo(a: Int, b: Int) { | return 2 * a + | b |} | """.trimMargin(), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.WRONG_INDENTATION) fun `should check indentation in KDocs - positive example`() { lintMethod( """ |/** | * Lorem ipsum | */ |class Example { |} | """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_INDENTATION) fun `assignment increases indentation if followed by newline`() { lintMethod( """ |fun foo(list: List) { | val a = list.filter { | predicate(it) | } | | val b = | list.filter { | predicate(it) | } |} | """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_INDENTATION) fun `when lambda is assigned, indentation is increased by one step`() { lintMethod( """ |fun foo() { | val a = { x: Int -> | x * 2 | } | | val b = | { x: Int -> | x * 2 | } |} | """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_INDENTATION) fun `should check indentation in KDocs`() { lintMethod( """ |/** |* Lorem ipsum |*/ |class Example { |} | """.trimMargin(), DiktatError(2, 1, ruleId, warnText(1, 0), true), DiktatError(3, 1, ruleId, warnText(1, 0), true) ) } @Test @Tag(WarningNames.WRONG_INDENTATION) fun `dot call increases indentation`() { lintMethod( """ |fun foo() { | Integer | .valueOf(2).also { | println(it) | } | ?.also { | println("Also with safe access") | } | ?: Integer.valueOf(0) | | bar | .baz() | as Baz | as? Baz |} | """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_INDENTATION) fun `loops and conditionals without braces should be indented - positive example`() { lintMethod( """ |fun foo() { | for (i in 1..100) | println(i) | | do | println() | while (condition) | | if (condition) | bar() | else | baz() |} | """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_INDENTATION) fun `loops and conditionals without braces should be indented`() { lintMethod( """ |fun foo() { | for (i in 1..100) | println(i) | | do | println() | while (condition) | | if (condition) | bar() | else | baz() |} | """.trimMargin(), DiktatError(3, 1, ruleId, warnText(8, 4), true), DiktatError(6, 1, ruleId, warnText(8, 4), true), DiktatError(10, 1, ruleId, warnText(8, 4), true), DiktatError(12, 1, ruleId, warnText(8, 4), true) ) } @Test @Tag(WarningNames.WRONG_INDENTATION) fun `loops and conditionals without braces should be indented - if-else with mixed braces`() { lintMethod( """ |fun foo() { | if (condition) { | bar() | } else | baz() | | if (condition) | bar() | else { | baz() | } | | if (condition) | bar() | else if (condition2) { | baz() | } else | qux() | | if (condition) | bar() | else if (condition2) | baz() | else { | quux() | } |} | """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_INDENTATION) fun `opening braces should not increase indent when placed on the same line`() { lintMethod( """ |fun foo() { | consume(Example( | t1, t2, t3 | )) | | bar(baz( | 1, | 2 | ) | ) | | bar(baz( | 1, | 2), | 3 | ) |} | """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_INDENTATION) fun `opening braces should not increase indent when placed on the same line - with disabled options`() { lintMethod( """ |fun foo() { | bar(baz( | 1, | 2), | 3 | ) |} | """.trimMargin(), rulesConfigList = disabledOptionsRulesConfigList ) } @Test @Tag(WarningNames.WRONG_INDENTATION) fun `custom getters and setters should increase indentation - positive example`() { lintMethod( """ |class Example { | private val foo | get() = 0 | | private var backing = 0 | | var bar | get() = backing | set(value) { backing = value } |} | """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_INDENTATION) fun `custom getters and setters should increase indentation`() { lintMethod( """ |class Example { | private val foo | get() = 0 | | private var backing = 0 | | var bar | get() = backing | set(value) { backing = value } |} | """.trimMargin(), DiktatError(3, 1, ruleId, warnText(8, 12), true), DiktatError(8, 1, ruleId, warnText(8, 4), true), DiktatError(9, 1, ruleId, warnText(8, 4), true) ) } @Test @Tag(WarningNames.WRONG_INDENTATION) fun `regression - indentation should be increased inside parameter list for multiline parameters`() { lintMethod( """ |fun foo() { | bar( | param1 = baz( | 1, | 2 | ), | param2 = { elem -> | elem.qux() | }, | param3 = x | .y() | ) |} | """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_INDENTATION) fun `regression - nested blocks inside loops and conditionals without braces should be properly indented`() { lintMethod( """ |fun foo() { | if (condition) | list.filter { | bar() | } | .call( | param1, | param2 | ) | else | list | .filter { | baz() | } |} | """.trimMargin(), rulesConfigList = listOf( RulesConfig(WRONG_INDENTATION.name, true, mapOf( EXTENDED_INDENT_OF_PARAMETERS to "false", EXTENDED_INDENT_BEFORE_DOT to "false" ) ) ) ) } @Test @Tag(WarningNames.WRONG_INDENTATION) fun `arrows in when expression should increase indentation - positive example`() { lintMethod( """ |fun foo() { | when (x) { | X_1 -> | foo(x) | X_2 -> bar(x) | X_3 -> { | baz(x) | } | else -> | qux(x) | } |} | """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_INDENTATION) fun `arrows in when expression should increase indentation`() { lintMethod( """ |fun foo() { | when (x) { | X_1 -> | foo(x) | X_2 -> bar(x) | X_3 -> { | baz(x) | } | else -> | qux(x) | } |} | """.trimMargin(), DiktatError(4, 1, ruleId, warnText(12, 8), true), DiktatError(7, 1, ruleId, warnText(12, 8), true), DiktatError(10, 1, ruleId, warnText(12, 8), true) ) } @Test @Tag(WarningNames.WRONG_INDENTATION) fun `comments should not turn off exceptional indentation`() { lintMethod( """ |fun foo() { | list | .map(::foo) | // comment about the next call | .filter { it.bar() } | // another comment about the next call | ?.filter { it.bar() } | ?.count() | | list.any { predicate(it) } && | list.any { | predicate(it) | } | | list.any { predicate(it) } && | // comment | list.any { | predicate(it) | } | | list.filter { | predicate(it) && | // comment | predicate(it) | } |} | """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_INDENTATION) fun `regression - npe with comments`() { lintMethod( """ |fun foo() { | bar.let { | baz(it) | // lorem ipsum | } |} | """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_INDENTATION) fun `closing parenthesis bug`() { lintMethod( """ |fun foo() { | return x + | (y + | foo(x) | ) |} | """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_INDENTATION) fun `should trigger on string templates starting with new line`() { lintMethod( """ |fun foo(some: String) { | fun bar() { | val a = "${'$'}{ | expression | .foo() | .bar() | }" | } | | val b = "${'$'}{ foo().bar() }" |} | """.trimMargin(), DiktatError(4, 1, ruleId, warnText(12, 8), true), DiktatError(5, 1, ruleId, warnText(16, 12), true), DiktatError(6, 1, ruleId, warnText(16, 12), true), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.WRONG_INDENTATION) fun `check script`(@TempDir tempDir: Path) { lintMethodWithFile( """ |val q = 1 | """.trimMargin(), tempDir = tempDir, fileName = "src/main/kotlin/com/saveourtool/diktat/Example.kts" ) } @Test @Tag(WarningNames.WRONG_INDENTATION) fun `check gradle script`(@TempDir tempDir: Path) { lintMethodWithFile( """ |projectName = "diKTat" | """.trimMargin(), tempDir = tempDir, fileName = "src/main/kotlin/com/saveourtool/diktat/build.gradle.kts" ) } @Test @Tag(WarningNames.WRONG_INDENTATION) fun `should warn message with configured indentation size`() { val rulesConfigListWithIndentation2 = rulesConfigList.map { ruleConfig -> ruleConfig.copy(configuration = ruleConfig.configuration.mapValues { (key, value) -> if (key == INDENTATION_SIZE) { "2" } else { value } }) } val warnMessage = WRONG_INDENTATION.warnText().replace("4", "2") lintMethod( """ |fun foo(some: String) { | print("test") |if (test){ |print("2") |} |} | """.trimMargin(), DiktatError(2, 1, ruleId, "$warnMessage expected 2 but was 4", true), DiktatError(3, 1, ruleId, "$warnMessage expected 2 but was 0", true), DiktatError(4, 1, ruleId, "$warnMessage expected 4 but was 0", true), DiktatError(5, 1, ruleId, "$warnMessage expected 2 but was 0", true), rulesConfigList = rulesConfigListWithIndentation2 ) } private fun warnText(expected: Int, actual: Int) = "${WRONG_INDENTATION.warnText()} expected $expected but was $actual" } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/spaces/WhiteSpaceRuleFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3.spaces import com.saveourtool.diktat.ruleset.rules.chapter3.files.WhiteSpaceRule import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class WhiteSpaceRuleFixTest : FixTestBase("test/paragraph3/spaces", ::WhiteSpaceRule) { @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `should keep single whitespace between keyword and opening parentheses`() { fixAndCompare("WhiteSpaceBeforeLParExpected.kt", "WhiteSpaceBeforeLParTest.kt") } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `should keep single whitespace between keyword and opening brace`() { fixAndCompare("LBraceAfterKeywordExpected.kt", "LBraceAfterKeywordTest.kt") } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `should remove spaces between ( and { when lambda is used as an argument`() { fixAndCompare("LambdaAsArgumentExpected.kt", "LambdaAsArgumentTest.kt") } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `should keep single whitespace before any other opening brace`() { fixAndCompare("LbraceExpected.kt", "LbraceTest.kt") } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `should surround binary operators with spaces`() { fixAndCompare("BinaryOpExpected.kt", "BinaryOpTest.kt") } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `should trim spaces in the end of line`() { fixAndCompare("EolSpacesExpected.kt", "EolSpacesTest.kt") } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `should fix space in annotation`() { fixAndCompare("AnnotationExpected.kt", "AnnotationTest.kt") } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `should add spaces on both sides of equals`() { fixAndCompare("EqualsExpected.kt", "EqualsTest.kt") } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `should add spaces on both sides of braces in lambda`() { fixAndCompare("BracesLambdaSpacesExpected.kt", "BracesLambdaSpacesTest.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/spaces/WhiteSpaceRuleWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3.spaces import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings.WRONG_WHITESPACE import com.saveourtool.diktat.ruleset.rules.chapter3.files.WhiteSpaceRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test @Suppress("LargeClass") class WhiteSpaceRuleWarnTest : LintTestBase(::WhiteSpaceRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${WhiteSpaceRule.NAME_ID}" private val eolSpaceWarn = "${WRONG_WHITESPACE.warnText()} there should be no spaces in the end of line" private val lbraceWarn = "${WRONG_WHITESPACE.warnText()} there should be a whitespace before '{'" private fun keywordWarn(keyword: String, sep: String) = "${WRONG_WHITESPACE.warnText()} keyword '$keyword' should be separated from '$sep' with a whitespace" private fun tokenWarn(token: String, before: Int?, after: Int?, reqBefore: Int?, reqAfter: Int? ) = "${WRONG_WHITESPACE.warnText()} $token should have" + (reqBefore?.let { " $it space(s) before" } ?: "") + (if (reqBefore != null && reqAfter != null) " and" else "") + (reqAfter?.let { " $it space(s) after" } ?: "") + ", but has" + (before?.let { " $it space(s) before" } ?: "") + (if (before != null && after != null) " and" else "") + (after?.let { " $it space(s) after" } ?: "") @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `keywords should have space before opening parenthesis and braces - positive example`() { lintMethod( """ |class Example { | constructor(val a: Int) | | fun foo() { | if (condition) { } | else { } | for (i in 1..100) { } | when (expression) { } | } |} """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `keywords should have space before opening parenthesis`() { lintMethod( """ |class Example { | fun foo() { | if(condition) { } | for (i in 1..100) { } | when(expression) { } | } |} """.trimMargin(), DiktatError(3, 11, ruleId, keywordWarn("if", "("), true), DiktatError(4, 14, ruleId, keywordWarn("for", "("), true), DiktatError(5, 13, ruleId, keywordWarn("when", "("), true) ) } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `constructor should not have space before opening parenthesis`() { lintMethod( """ |class Example { | constructor (val a: Int) |} """.trimMargin(), DiktatError(2, 5, ruleId, "${WRONG_WHITESPACE.warnText()} keyword 'constructor' should not be separated from '(' with a whitespace", true) ) } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `keywords should have space before opening braces`() { lintMethod( """ |class Example { | fun foo() { | if (condition) { } | else{} | try{ } | finally{ } | } |} """.trimMargin(), DiktatError(4, 14, ruleId, keywordWarn("else", "{"), true), DiktatError(5, 13, ruleId, keywordWarn("try", "{"), true), DiktatError(6, 17, ruleId, keywordWarn("finally", "{"), true) ) } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `keywords should have space before opening braces - else without braces`() { lintMethod( """ |fun foo() { | if (condition) | bar() | else | baz() | | if (condition) bar() else baz() |} """.trimMargin(), DiktatError(7, 33, ruleId, keywordWarn("else", "baz"), true) ) } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `all opening braces should have leading space`() { lintMethod( """ |class Example{ | fun foo(){ | list.run{ | map{ bar(it) } | } | } |} """.trimMargin(), DiktatError(1, 14, ruleId, lbraceWarn, true), DiktatError(2, 14, ruleId, lbraceWarn, true), DiktatError(3, 17, ruleId, lbraceWarn, true), DiktatError(4, 16, ruleId, lbraceWarn, true) ) } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `all opening braces should have leading space - exception for lambdas as arguments`() { lintMethod( """ |fun foo(a: (Int) -> Int, b: Int) { | foo({ x: Int -> x }, 5) |} | |fun bar(a: (Int) -> Int, b: Int) { | bar( { x: Int -> x }, 5) |} | |val lambda = { x: Int -> 2 * x } """.trimMargin(), DiktatError(6, 10, ruleId, "${WRONG_WHITESPACE.warnText()} there should be no whitespace before '{' of lambda inside argument list", true) ) } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `binary operators should be surrounded by spaces - positive example`() { lintMethod( """ |class Example where T : UpperType { | fun foo(t: T) = t + 1 | | fun bar() { | listOf().map(this::foo).filter { elem -> predicate(elem) } | } |} """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `should not false positively trigger when operators are surrounded with newlines`() { lintMethod( """ |class Example where T | : | UpperType { | fun foo(t: T) = | t + 1 | | fun bar() { | listOf() | .map(this | ::foo) | .filter { elem -> | predicate(elem) | } | } |} """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `should not false positively trigger when operators are surrounded with newlines and EOL comments`() { lintMethod( """ |class Example where T | : // comment about UpperType | UpperType { | fun foo(t: T) = // another comment | t + 1 | | fun bar() { | listOf() | .map(this // lorem ipsum | ::foo) | .filter { elem -> // dolor sit amet | predicate(elem) | } | } |} """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_WHITESPACE) @Suppress("TOO_LONG_FUNCTION") fun `binary operators should be surrounded by spaces`() { lintMethod( """ |class Example where T:UpperType, R: UpperType, Q :UpperType { | fun foo(t: T) = t+ 1 | fun foo2(t: T) = t+1 | fun foo3(t: T) = t +1 | | fun bar() { | listOf() .map(this ::foo) ?.filter { elem ->predicate(elem) } !!.first() | listOf() . map(this :: foo) ?. filter { elem->predicate(elem) } !! .first() | listOf(). map(this:: foo)?. filter { elem-> predicate(elem) }!!. first() | } |} """.trimMargin(), DiktatError(1, 31, ruleId, tokenWarn(":", 0, 0, 1, 1), true), DiktatError(1, 44, ruleId, tokenWarn(":", 0, null, 1, 1), true), DiktatError(1, 59, ruleId, tokenWarn(":", null, 0, 1, 1), true), DiktatError(2, 22, ruleId, tokenWarn("+", 0, null, 1, 1), true), DiktatError(3, 23, ruleId, tokenWarn("+", 0, 0, 1, 1), true), DiktatError(4, 24, ruleId, tokenWarn("+", null, 0, 1, 1), true), DiktatError(7, 21, ruleId, tokenWarn(".", 1, null, 0, 0), true), DiktatError(7, 31, ruleId, tokenWarn("::", 1, null, 0, 0), true), DiktatError(7, 38, ruleId, tokenWarn("?.", 1, null, 0, 0), true), DiktatError(7, 54, ruleId, tokenWarn("->", null, 0, 1, 1), true), DiktatError(7, 74, ruleId, tokenWarn("!!", 1, null, 0, null), true), DiktatError(8, 21, ruleId, tokenWarn(".", 1, 1, 0, 0), true), DiktatError(8, 32, ruleId, tokenWarn("::", 1, 1, 0, 0), true), DiktatError(8, 40, ruleId, tokenWarn("?.", 1, 1, 0, 0), true), DiktatError(8, 56, ruleId, tokenWarn("->", 0, 0, 1, 1), true), DiktatError(8, 76, ruleId, tokenWarn("!!", 1, null, 0, null), true), DiktatError(8, 79, ruleId, tokenWarn(".", 1, null, 0, 0), true), DiktatError(9, 20, ruleId, tokenWarn(".", null, 1, 0, 0), true), DiktatError(9, 30, ruleId, tokenWarn("::", null, 1, 0, 0), true), DiktatError(9, 37, ruleId, tokenWarn("?.", null, 1, 0, 0), true), DiktatError(9, 53, ruleId, tokenWarn("->", 0, null, 1, 1), true), DiktatError(9, 75, ruleId, tokenWarn(".", null, 1, 0, 0), true) ) } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `operators with single space after - positive example`() { lintMethod( """ |class Example { | fun foo(t1: T, t2: T) { | println(); println() | } | | fun bar(t: T, | d: T) { | println(); | } | | val x: Int |} """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `operators with single space after`() { lintMethod( """ |class Example {${" "} | fun foo(t1 :T ,t2:T) {${" "} | println();println() | println() ; println() | } | | val x : Int |} """.trimMargin(), DiktatError(1, 19, ruleId, eolSpaceWarn, true), DiktatError(2, 16, ruleId, tokenWarn(":", 1, 0, 0, 1), true), DiktatError(2, 19, ruleId, tokenWarn(",", 1, 0, 0, 1), true), DiktatError(2, 22, ruleId, tokenWarn(":", null, 0, 0, 1), true), DiktatError(2, 27, ruleId, eolSpaceWarn, true), DiktatError(3, 18, ruleId, tokenWarn(";", null, 0, 0, 1), true), DiktatError(4, 19, ruleId, tokenWarn(";", 1, null, 0, 1), true), DiktatError(7, 11, ruleId, tokenWarn(":", 1, null, 0, 1), true) ) } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `operators with single space after - exceptional cases - positive example`() { lintMethod( """ |abstract class Foo : IFoo { } | |class FooImpl : Foo() { | constructor(x: String) : this(x) { /*...*/ } | | val x = object : IFoo { /*...*/ } |} """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `operators with single space after - exceptional cases`() { lintMethod( """ |abstract class Foo: IFoo { } | |class FooImpl: Foo() { | constructor(x: String): this(x) { /*...*/ } | | val x = object: IFoo { /*...*/ } |} """.trimMargin(), DiktatError(1, 25, ruleId, tokenWarn(":", 0, null, 1, 1), true), DiktatError(1, 31, ruleId, tokenWarn(":", 0, null, 1, 1), true), DiktatError(3, 14, ruleId, tokenWarn(":", 0, null, 1, 1), true), DiktatError(4, 27, ruleId, tokenWarn(":", 0, null, 1, 1), true), DiktatError(6, 19, ruleId, tokenWarn(":", 0, null, 1, 1), true) ) } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `there should be no space before question mark in nullable types`() { lintMethod( """ |class Example { | lateinit var x: Int? | lateinit var x: Int ? |} """.trimMargin(), DiktatError(3, 25, ruleId, tokenWarn("?", 1, null, 0, null), true) ) } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `there should be no space before and after square bracket`() { lintMethod( """ |val x = list[0] |val y = list [0] """.trimMargin(), DiktatError(2, 14, ruleId, tokenWarn("[", 1, null, 0, 0), true) ) } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `there should be no space between constructor or function name and opening parentheses - positive example`() { lintMethod( """ |class Example(val x: Int) { | constructor() : this(0) | | fun foo(y: Int): AnotherExample { | bar(x) | return AnotherExample(y) | } |} """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `there should be no space between constructor or function name and opening parentheses`() { lintMethod( """ |class Example (val x: Int) { | constructor() : this (0) | | fun foo (y: Int): AnotherExample { | bar (x) | return AnotherExample (y) | } |} """.trimMargin(), DiktatError(1, 15, ruleId, tokenWarn("(", 1, null, 0, 0), true), DiktatError(2, 26, ruleId, tokenWarn("(", 1, null, 0, 0), true), DiktatError(4, 13, ruleId, tokenWarn("(", 1, null, 0, 0), true), DiktatError(5, 13, ruleId, tokenWarn("(", 1, null, 0, 0), true), DiktatError(6, 31, ruleId, tokenWarn("(", 1, null, 0, 0), true) ) } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `there should be no space before and single space after colon in function return type`() { lintMethod( """ |fun foo(): String = "lorem" |fun bar() : String = "ipsum" |fun baz() :String = "dolor" """.trimMargin(), DiktatError(2, 11, ruleId, tokenWarn(":", 1, null, 0, 1), true), DiktatError(3, 11, ruleId, tokenWarn(":", 1, 0, 0, 1), true) ) } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `there should be no space before and after colon when use-site annotation is used`() { lintMethod( """ |class Example(@field:Anno val foo: Type, | @get:Anno val bar: Type, | @param:Anno val baz: Type) | |class Example2(@field: Anno val foo: Type, | @get :Anno val bar: Type, | @param : Anno val baz: Type) """.trimMargin(), DiktatError(5, 22, ruleId, tokenWarn(":", null, 1, 0, 0), true), DiktatError(6, 21, ruleId, tokenWarn(":", 1, null, 0, 0), true), DiktatError(7, 23, ruleId, tokenWarn(":", 1, 1, 0, 0), true) ) } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `regression - comma after !!`() { lintMethod( """ |fun foo() { | val codeFix = CodeFix(codeForm.initialCode!! ,codeFormHtml.ruleSet[0]) |} """.trimMargin(), DiktatError(2, 50, ruleId, tokenWarn(",", 1, 0, 0, 1), true) ) } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `space after annotation`() { lintMethod( """ |@Annotation ("Text") |fun foo() { | |} """.trimMargin(), DiktatError(1, 13, ruleId, tokenWarn("(\"Text\")", 1, null, 0, null), true) ) } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `check space on both sides of equals`() { lintMethod( """ |fun foo() { | val q=10 | var w = 10 | w=q |} """.trimMargin(), DiktatError(2, 9, ruleId, tokenWarn("=", 0, 0, 1, 1), true), DiktatError(4, 5, ruleId, tokenWarn("=", 0, 0, 1, 1), true) ) } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `check eq in other cases`() { lintMethod( """ |fun foo()=10 | |val q =goo(text=ty) """.trimMargin(), DiktatError(1, 10, ruleId, tokenWarn("=", 0, 0, 1, 1), true), DiktatError(3, 7, ruleId, tokenWarn("=", null, 0, 1, 1), true), DiktatError(3, 16, ruleId, tokenWarn("=", 0, 0, 1, 1), true) ) } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `singe space after open brace`() { lintMethod( """ |fun foo() { | "${"$"}{foo()}" |} """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `array initializers in annotations`() { lintMethod( """ |@RequestMapping(value =["/"], method = [RequestMethod.GET]) |fun foo() { | a[0] |} """.trimMargin(), DiktatError(1, 23, ruleId, tokenWarn("=", null, 0, 1, 1), true), DiktatError(1, 24, ruleId, tokenWarn("[", 0, null, 1, 0), true) ) } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `lambda as rigth value in arguments`() { lintMethod( """ |fun foo() { | Example(cb = { _, _ -> Unit }) |} """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `lambdas as argument for function`() { lintMethod( """ |val q = foo(bar, { it.baz() }) |val q = foo({ it.baz() }) |val q = foo( { it.baz() }) """.trimMargin(), DiktatError(3, 14, ruleId, "${WRONG_WHITESPACE.warnText()} there should be no whitespace before '{' of lambda inside argument list", true) ) } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `regression - prefix coloncolon should be checked separately - positive example`() { lintMethod( """ |fun foo() { | Example(::ClassName) | bar(param1, ::ClassName) | bar(param1, param2 = ::ClassName) | list.map(::operationReference) |} """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `regression - prefix coloncolon should be checked separately`() { lintMethod( """ |fun foo() { | Example( :: ClassName) | bar(param1, :: ClassName) | bar(param1, param2 = :: ClassName) | list.map(:: operationReference) |} """.trimMargin(), DiktatError(2, 12, ruleId, tokenWarn("(", null, 1, 0, 0), true), DiktatError(2, 14, ruleId, tokenWarn("::", null, 1, null, 0), true), DiktatError(3, 15, ruleId, tokenWarn(",", null, 2, 0, 1), true), DiktatError(3, 18, ruleId, tokenWarn("::", null, 1, null, 0), true), DiktatError(4, 26, ruleId, tokenWarn("::", null, 1, null, 0), true), DiktatError(5, 14, ruleId, tokenWarn("::", null, 1, null, 0), true) ) } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `regression - should correctly handle prefix and postfix operators`() { lintMethod( """ |fun foo() { | var index = 1 | --index | return index++ |} | |fun bar() { | var index = 1 | -- index | return index ++ |} """.trimMargin(), DiktatError(9, 5, ruleId, tokenWarn("--", null, 1, null, 0), true), DiktatError(10, 18, ruleId, tokenWarn("++", 1, null, 0, null), true) ) } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `check whitespaces around braces in lambda example - good`() { lintMethod( """ |fun foo() { | list.map { it.text } |} """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `check whitespaces around braces in lambda example - bad`() { lintMethod( """ |fun foo() { | list.map {it.text} |} """.trimMargin(), DiktatError(2, 14, ruleId, "${WRONG_WHITESPACE.warnText()} there should be a whitespace after {", true), DiktatError(2, 22, ruleId, "${WRONG_WHITESPACE.warnText()} there should be a whitespace before }", true) ) } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `should not trigger on braces with empty body #1`() { lintMethod( """ |val project = KotlinCoreEnvironment.createForProduction( | Disposable {}, | compilerConfiguration, | EnvironmentConfigFiles.JVM_CONFIG_FILES |).project """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `should not trigger on braces with empty body #2`() { lintMethod( """ |val project = KotlinCoreEnvironment.createForProduction( | Disposable { }, | compilerConfiguration, | EnvironmentConfigFiles.JVM_CONFIG_FILES |).project """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_WHITESPACE) fun `should not trigger in braces on the beginning of the line`() { lintMethod( """ |val onClick: () -> Unit = remember { | { | /* do stuff */ | } |} """.trimMargin() ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/spaces/junit/IndentationTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3.spaces.junit import generated.WarningNames.WRONG_INDENTATION import org.junit.jupiter.api.Tag import org.junit.jupiter.api.TestTemplate import org.junit.jupiter.api.extension.ExtendWith import kotlin.annotation.AnnotationRetention.RUNTIME import kotlin.annotation.AnnotationTarget.FUNCTION /** * @property includeWarnTests whether unit tests for the "warn" mode should also * be generated. If `false`, the code is allowed to have no expected-error * annotations, and only fix mode tests get generated. The default is `true`. * @property singleConfiguration whether only a single code fragment is to be * analysed. If `true`, the value of [second] is ignored, resulting in fewer * unit tests being generated. The default is `false`. */ @Target(FUNCTION) @Retention(RUNTIME) @MustBeDocumented @TestTemplate @ExtendWith(IndentationTestInvocationContextProvider::class) @Tag(WRONG_INDENTATION) annotation class IndentationTest( val first: IndentedSourceCode, val second: IndentedSourceCode = IndentedSourceCode(""), val includeWarnTests: Boolean = true, val singleConfiguration: Boolean = false, ) ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/spaces/junit/IndentationTestExtension.kt ================================================ @file:Suppress("FILE_UNORDERED_IMPORTS")// False positives, see #1494. package com.saveourtool.diktat.ruleset.chapter3.spaces.junit import com.saveourtool.diktat.ruleset.utils.indentation.IndentationConfig.Companion.NEWLINE_AT_END import org.intellij.lang.annotations.Language import org.junit.jupiter.api.extension.BeforeTestExecutionCallback import com.saveourtool.diktat.ruleset.chapter3.spaces.IndentationConfigFactory as IndentationConfig /** * The common super-interface for indentation-specific `Extension` * implementations. */ internal interface IndentationTestExtension : BeforeTestExecutionCallback { /** * The default configuration for the indentation rule. */ @Suppress("CUSTOM_GETTERS_SETTERS") val defaultConfig get() = IndentationConfig(NEWLINE_AT_END to false) /** * Non-default configuration for the indentation rule. */ val customConfig: Map /** * The original file content (may well get modified as fixes are applied). */ @get:Language("kotlin") val actualCode: String } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/spaces/junit/IndentationTestFixExtension.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3.spaces.junit import com.saveourtool.diktat.ruleset.chapter3.spaces.asRulesConfigList import com.saveourtool.diktat.ruleset.chapter3.spaces.withCustomParameters import com.saveourtool.diktat.ruleset.junit.CloseablePath import com.saveourtool.diktat.ruleset.rules.chapter3.files.IndentationRule import com.saveourtool.diktat.util.FixTestBase import org.intellij.lang.annotations.Language import org.junit.jupiter.api.extension.BeforeEachCallback import org.junit.jupiter.api.extension.ExtensionContext import org.junit.jupiter.api.extension.ExtensionContext.Namespace import java.nio.file.Path import kotlin.io.path.createTempDirectory /** * The `Extension` implementation for indentation test templates (fix mode). * * @property customConfig non-default configuration for the indentation rule. * @property actualCode the original file content (may well get modified as * fixes are applied). */ @Suppress( "TOO_MANY_BLANK_LINES", // Readability "WRONG_INDENTATION") // False positives, see #1404. class IndentationTestFixExtension( override val customConfig: Map, @Language("kotlin") override val actualCode: String, @Language("kotlin") private val expectedCode: String ) : FixTestBase("nonexistent", ::IndentationRule), IndentationTestExtension, BeforeEachCallback { private lateinit var tempDir: Path override fun beforeEach(context: ExtensionContext) { tempDir = context.getStore(namespace).getOrComputeIfAbsent(KEY, { CloseablePath(createTempDirectory(prefix = TEMP_DIR_PREFIX)) }, CloseablePath::class.java).directory } override fun beforeTestExecution(context: ExtensionContext) { val lintResult = fixAndCompareContent( actualCode, expectedCode, tempDir, overrideRulesConfigList = defaultConfig.withCustomParameters(customConfig).asRulesConfigList(), ) lintResult.assertSuccessful() } private companion object { private const val KEY = "temp.dir" private const val TEMP_DIR_PREFIX = "junit" private val namespace = Namespace.create(IndentationTestFixExtension::class) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/spaces/junit/IndentationTestFixInvocationContext.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3.spaces.junit import org.intellij.lang.annotations.Language import org.junit.jupiter.api.extension.Extension import java.util.SortedMap /** * @property customConfig non-default configuration for the indentation rule. * @property actualCode the original file content (may well get modified as * fixes are applied). * @property expectedCode the content the file is expected to have after the * fixes are applied. */ @Suppress("BLANK_LINE_BETWEEN_PROPERTIES") class IndentationTestFixInvocationContext( override val customConfig: SortedMap, @Language("kotlin") override val actualCode: String, @Language("kotlin") private val expectedCode: String = actualCode ) : IndentationTestInvocationContext { override val mode: String = "fix" override val correctlyIndented: Boolean = actualCode == expectedCode override val displayName: String = when { correctlyIndented -> "should remain unchanged if properly indented" else -> "should be reformatted if mis-indented" } override fun getAdditionalExtensions(): List = listOf(IndentationTestFixExtension(customConfig, actualCode, expectedCode)) } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/spaces/junit/IndentationTestInput.kt ================================================ @file:Suppress("FILE_UNORDERED_IMPORTS")// False positives, see #1494. package com.saveourtool.diktat.ruleset.chapter3.spaces.junit import com.saveourtool.diktat.ruleset.chapter3.spaces.ExpectedIndentationError import com.saveourtool.diktat.ruleset.chapter3.spaces.withCustomParameters import com.saveourtool.diktat.ruleset.utils.indentation.IndentationConfig.Companion.NEWLINE_AT_END import org.intellij.lang.annotations.Language import java.util.SortedMap import com.saveourtool.diktat.ruleset.chapter3.spaces.IndentationConfigFactory as IndentationConfig /** * The test data for indentation tests, extracted from annotations. * * @property code the code to check. * @property expectedErrors the expected lint errors (may be empty). * @property customConfig non-default configuration for the indentation rule. */ data class IndentationTestInput( @Language("kotlin") val code: String, val expectedErrors: List, val customConfig: SortedMap, ) { /** * The effective configuration for the indentation rule (contains both * default and non-default entries). */ @Suppress("CUSTOM_GETTERS_SETTERS") val effectiveConfig: Map get() = IndentationConfig(NEWLINE_AT_END to false).withCustomParameters(customConfig) } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/spaces/junit/IndentationTestInvocationContext.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3.spaces.junit import org.intellij.lang.annotations.Language import org.junit.jupiter.api.extension.TestTemplateInvocationContext import java.util.SortedMap /** * The [TestTemplateInvocationContext] implementation for indentation tests. */ interface IndentationTestInvocationContext : TestTemplateInvocationContext { /** * Non-default configuration for the indentation rule. */ val customConfig: SortedMap /** * The original file content (may well get modified as fixes are applied). */ @get:Language("kotlin") val actualCode: String /** * The mode of this invocation context, either "warn" or "fix". */ val mode: String /** * Whether the code in this test is indented in accordance with the * effective configuration. */ val correctlyIndented: Boolean /** * The detailed display name of this invocation context. */ val displayName: String override fun getDisplayName(invocationIndex: Int): String { val parameterDescription = when { customConfig.isEmpty() -> invocationIndex else -> customConfig.asParameterList() } return "[$mode] $displayName [$parameterDescription]" } private companion object { private fun Map<*, *>.asParameterList(): String = asSequence().map { (key, value) -> "$key = $value" }.joinToString() } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/spaces/junit/IndentationTestInvocationContextProvider.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3.spaces.junit import com.saveourtool.diktat.ruleset.chapter3.spaces.ExpectedIndentationError import com.saveourtool.diktat.ruleset.junit.RuleInvocationContextProvider import com.saveourtool.diktat.ruleset.utils.NEWLINE import com.saveourtool.diktat.ruleset.utils.indentation.IndentationConfig.Companion.EXTENDED_INDENT_AFTER_OPERATORS import com.saveourtool.diktat.ruleset.utils.indentation.IndentationConfig.Companion.EXTENDED_INDENT_BEFORE_DOT import com.saveourtool.diktat.ruleset.utils.indentation.IndentationConfig.Companion.EXTENDED_INDENT_FOR_EXPRESSION_BODIES import com.saveourtool.diktat.ruleset.utils.indentation.IndentationConfig.Companion.EXTENDED_INDENT_OF_PARAMETERS import com.saveourtool.diktat.ruleset.utils.leadingSpaceCount import com.saveourtool.diktat.util.assertNotNull import generated.WarningNames.WRONG_INDENTATION import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.fail import org.intellij.lang.annotations.Language import org.junit.jupiter.api.extension.ExtensionContext import org.junit.jupiter.api.extension.TestTemplateInvocationContext import org.junit.platform.commons.util.AnnotationUtils.findAnnotation import java.util.SortedMap import java.util.stream.Stream import java.util.stream.Stream.concat import kotlin.reflect.KClass /** * The `TestTemplateInvocationContextProvider` implementation for indentation * tests. */ class IndentationTestInvocationContextProvider : RuleInvocationContextProvider { override fun annotationType(): KClass = IndentationTest::class override fun expectedLintErrorFrom( @Language("kotlin") line: String, lineNumber: Int, tag: String, properties: Map ): ExpectedIndentationError { val message = properties[MESSAGE] @Suppress("AVOID_NULL_CHECKS") if (message != null) { return ExpectedIndentationError( line = lineNumber, message = "[$WRONG_INDENTATION] $message") } val expectedIndent = properties.expectedIndent() val actualIndent = line.leadingSpaceCount() assertThat(actualIndent) .describedAs("Expected and actual indent values are the same: $expectedIndent, line $lineNumber (\"$line\")") .isNotEqualTo(expectedIndent) assertThat(tag) .describedAs("Unexpected tag: $tag") .isEqualTo(WRONG_INDENTATION) return ExpectedIndentationError( line = lineNumber, expectedIndent = expectedIndent, actualIndent = actualIndent) } @Suppress("TOO_LONG_FUNCTION") override fun provideTestTemplateInvocationContexts(context: ExtensionContext, supportedTags: List): Stream { val testMethod = context.requiredTestMethod val indentationTest = findAnnotation(testMethod, annotationType().java).get() val includeWarnTests = indentationTest.includeWarnTests val singleConfiguration = indentationTest.singleConfiguration val testInput0 = indentationTest.first.extractTestInput( supportedTags, allowEmptyErrors = !includeWarnTests || singleConfiguration) val (code0, expectedErrors0, customConfig0) = testInput0 var contexts: Stream = Stream.of( IndentationTestFixInvocationContext(customConfig0, actualCode = code0) ) if (includeWarnTests) { /*- * In a double-configuration mode (the default), when the code is * checked against its own configuration, the actual list of errors * is expected to be empty (it's only used when the code is checked * against the opposite configuration. * * In a single-configuration mode, the opposite configuration is * empty, so let's allow a non-empty list of expected errors when * the code is checked against its own configuration. */ val expectedErrors = when { singleConfiguration -> expectedErrors0 else -> emptyList() } contexts += IndentationTestWarnInvocationContext(customConfig0, actualCode = code0, expectedErrors) } when { singleConfiguration -> { val code1 = indentationTest.second.code assertThat(code1) .describedAs("The 2nd code fragment should be empty if `singleConfiguration` is `true`: $NEWLINE$code1") .isEmpty() } else -> { val testInput1 = indentationTest.second.extractTestInput( supportedTags, allowEmptyErrors = !includeWarnTests) val (code1, expectedErrors1, customConfig1) = testInput1 assertThat(code0) .describedAs("Both code fragments are the same") .isNotEqualTo(code1) assertThat(customConfig0) .describedAs("Both custom configs are the same") .isNotEqualTo(customConfig1) assertThat(testInput0.effectiveConfig) .describedAs("Both effective configs are the same") .isNotEqualTo(testInput1.effectiveConfig) contexts += IndentationTestFixInvocationContext(customConfig1, actualCode = code1) contexts += IndentationTestFixInvocationContext(customConfig1, actualCode = code0, expectedCode = code1) contexts += IndentationTestFixInvocationContext(customConfig0, actualCode = code1, expectedCode = code0) if (includeWarnTests) { contexts += IndentationTestWarnInvocationContext(customConfig1, actualCode = code1) contexts += IndentationTestWarnInvocationContext(customConfig1, actualCode = code0, expectedErrors0) contexts += IndentationTestWarnInvocationContext(customConfig0, actualCode = code1, expectedErrors1) } } } return contexts.sorted { left, right -> left.getDisplayName(0).compareTo(right.getDisplayName(0)) } } /** * @param allowEmptyErrors whether the list of expected errors is allowed to * be empty (i.e. the code may contain no known annotations). */ private fun IndentedSourceCode.extractTestInput(supportedTags: List, allowEmptyErrors: Boolean): IndentationTestInput { val (code, expectedErrors) = extractExpectedErrors(code, supportedTags, allowEmptyErrors) return IndentationTestInput(code, expectedErrors, customConfig()) } private companion object { private const val EXPECTED_INDENT = "expectedIndent" private const val MESSAGE = "message" @Suppress("WRONG_NEWLINES") // False positives, see #1495. private fun IndentedSourceCode.customConfig(): SortedMap = mapOf( EXTENDED_INDENT_AFTER_OPERATORS to extendedIndentAfterOperators, EXTENDED_INDENT_BEFORE_DOT to extendedIndentBeforeDot, EXTENDED_INDENT_FOR_EXPRESSION_BODIES to extendedIndentForExpressionBodies, EXTENDED_INDENT_OF_PARAMETERS to extendedIndentOfParameters, ).mapValues { (_, value) -> value.valueOrNull }.filterValues { value -> value != null }.mapValues { (_, value) -> value!! }.toSortedMap() private fun Map.expectedIndent(): Int { val expectedIndentRaw = this[EXPECTED_INDENT].assertNotNull { "There's no `$EXPECTED_INDENT` key in $this" } assertThat(expectedIndentRaw) .describedAs("`$EXPECTED_INDENT` is empty") .isNotEmpty return try { expectedIndentRaw.toInt() } catch (_: NumberFormatException) { fail("Unparseable `$EXPECTED_INDENT`: $expectedIndentRaw") } } private operator fun Stream.plus(value: T): Stream = concat(this, Stream.of(value)) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/spaces/junit/IndentationTestWarnExtension.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3.spaces.junit import com.saveourtool.diktat.ruleset.chapter3.spaces.asRulesConfigList import com.saveourtool.diktat.ruleset.chapter3.spaces.withCustomParameters import com.saveourtool.diktat.ruleset.rules.chapter3.files.IndentationRule import com.saveourtool.diktat.ruleset.utils.NEWLINE import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import org.assertj.core.api.Assertions.assertThat import org.intellij.lang.annotations.Language import org.junit.jupiter.api.extension.ExtensionContext import kotlin.math.max import kotlin.math.min /** * The `Extension` implementation for indentation test templates (warn mode). * * @property customConfig non-default configuration for the indentation rule. * @property actualCode the original file content (may well get modified as * fixes are applied). */ @Suppress("TOO_MANY_BLANK_LINES") // Readability internal class IndentationTestWarnExtension( override val customConfig: Map, @Language("kotlin") override val actualCode: String, private val expectedErrors: Array ) : LintTestBase(::IndentationRule), IndentationTestExtension { override fun beforeTestExecution(context: ExtensionContext) { val actualErrors = lintResult( actualCode, defaultConfig.withCustomParameters(customConfig).asRulesConfigList()) val description = NEWLINE + actualCode.annotateWith(actualErrors) + NEWLINE when { expectedErrors.size == 1 && actualErrors.size == 1 -> { val actual = actualErrors[0] val expected = expectedErrors[0] assertThat(actual) .describedAs(description) .isEqualTo(expected) assertThat(actual.canBeAutoCorrected) .describedAs("canBeAutoCorrected") .isEqualTo(expected.canBeAutoCorrected) } else -> assertThat(actualErrors) .describedAs(description) .apply { when { expectedErrors.isEmpty() -> isEmpty() else -> containsExactly(*expectedErrors) } } } } private companion object { /** * Converts a lint error to the annotation text: * * ``` * ^____^ * ``` * * allowing in-code annotations like * * ``` * fun f() { * 1 + * 2 * ^________________________^ * ``` */ @Suppress("CUSTOM_GETTERS_SETTERS") private val DiktatError.annotationText: String get() { @Suppress("WRONG_NEWLINES") // False positives, see #1495. val columnNumbers = decimalNumber .findAll(detail) .map { match -> match.groups[1]?.value }.filterNotNull() .map(String::toInt) .toList() .takeLast(2) return when (columnNumbers.size) { 2 -> sequence { yield(NEWLINE) val columnNumber0 = columnNumbers[0] val columnNumber1 = columnNumbers[1] for (columnNumber in 1..max(columnNumber0, columnNumber1) + 1) { val ch = when { columnNumber <= min(columnNumber0, columnNumber1) -> ' ' columnNumber == columnNumber0 + 1 || columnNumber == columnNumber1 + 1 -> '^' else -> '_' } yield(ch) } }.joinToString(separator = "") else -> "" } } private val decimalNumber = Regex("""\b[+-]?(\d++)\b""") private fun String.annotateWith(errors: List): String = when { errors.isEmpty() -> this else -> { val linesAndErrors = errors.asSequence().map { error -> error.line to error }.toMap() lineSequence().mapIndexed { index, line -> when (val error = linesAndErrors[index + 1]) { null -> line else -> "$line${error.annotationText}" } }.joinToString(separator = NEWLINE.toString()) } } } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/spaces/junit/IndentationTestWarnInvocationContext.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3.spaces.junit import com.saveourtool.diktat.ruleset.chapter3.spaces.ExpectedIndentationError import com.saveourtool.diktat.ruleset.junit.ExpectedLintError import org.intellij.lang.annotations.Language import org.junit.jupiter.api.extension.Extension import java.util.SortedMap /** * The `TestTemplateInvocationContext` implementation for indentation tests * (warn mode). * * @property customConfig non-default configuration for the indentation rule. * @property actualCode the original file content (may well get modified as * fixes are applied). */ @Suppress("BLANK_LINE_BETWEEN_PROPERTIES") internal class IndentationTestWarnInvocationContext( override val customConfig: SortedMap, @Language("kotlin") override val actualCode: String, private val expectedErrors: List = emptyList() ) : IndentationTestInvocationContext { override val mode: String = "warn" override val correctlyIndented: Boolean = expectedErrors.isEmpty() override val displayName: String = when { correctlyIndented -> "should raise no warnings if properly indented" else -> "should be reported if mis-indented" } override fun getAdditionalExtensions(): List = listOf(IndentationTestWarnExtension( customConfig, actualCode, expectedErrors.map(ExpectedLintError::asLintError).toTypedArray())) } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter3/spaces/junit/IndentedSourceCode.kt ================================================ package com.saveourtool.diktat.ruleset.chapter3.spaces.junit import com.saveourtool.diktat.ruleset.junit.BooleanOrDefault import com.saveourtool.diktat.ruleset.junit.BooleanOrDefault.DEFAULT import org.intellij.lang.annotations.Language import kotlin.annotation.AnnotationRetention.RUNTIME /** * Requirements for [code]: * * - the code should contain at least one expected-error annotation in the * format `diktat:` or `diktat:[name1 = value1, name2 = value2]`; * - the exact format is `diktat:WRONG_INDENTATION[expectedIndent = ]`; * - the `expectedIndent` property should be present and its value should be a * number; * - the values of expected and actual indent (inferred from the code) should be * different; * - the code should be non-blank. * * @property code the source code to test. Common indentation will be trimmed using * [String.trimIndent]. * @property extendedIndentOfParameters describes the effective formatting of [code]. * @property extendedIndentForExpressionBodies describes the effective formatting of [code]. * @property extendedIndentAfterOperators describes the effective formatting of [code]. * @property extendedIndentBeforeDot describes the effective formatting of [code]. */ @Target @Retention(RUNTIME) @MustBeDocumented annotation class IndentedSourceCode( @Language("kotlin") val code: String, val extendedIndentOfParameters: BooleanOrDefault = DEFAULT, val extendedIndentForExpressionBodies: BooleanOrDefault = DEFAULT, val extendedIndentAfterOperators: BooleanOrDefault = DEFAULT, val extendedIndentBeforeDot: BooleanOrDefault = DEFAULT, ) ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter4/AccurateCalculationsWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter4 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings.FLOAT_IN_ACCURATE_CALCULATIONS import com.saveourtool.diktat.ruleset.rules.chapter4.calculations.AccurateCalculationsRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class AccurateCalculationsWarnTest : LintTestBase(::AccurateCalculationsRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${AccurateCalculationsRule.NAME_ID}" private fun warnText(ref: String, expr: String) = "${FLOAT_IN_ACCURATE_CALCULATIONS.warnText()} float value of <$ref> used in arithmetic expression in $expr" @Test @Tag(WarningNames.FLOAT_IN_ACCURATE_CALCULATIONS) fun `should detect comparison (equals) with float literal`() { lintMethod( """ |class Example { | fun foo() { | x == 1.0 | 1.0 == x | x.equals(1.0) | 1.0.equals(x) | } |} """.trimMargin(), DiktatError(3, 9, ruleId, warnText("1.0", "x == 1.0"), false), DiktatError(4, 9, ruleId, warnText("1.0", "1.0 == x"), false), DiktatError(5, 9, ruleId, warnText("1.0", "x.equals(1.0)"), false), DiktatError(6, 9, ruleId, warnText("1.0", "1.0.equals(x)"), false) ) } @Test @Tag(WarningNames.FLOAT_IN_ACCURATE_CALCULATIONS) fun `should detect comparison with float literal`() { lintMethod( """ |class Example { | fun foo() { | x > 1.0 | 1.0 > x | x.compareTo(1.0) | 1.0.compareTo(x) | } |} """.trimMargin(), DiktatError(3, 9, ruleId, warnText("1.0", "x > 1.0"), false), DiktatError(4, 9, ruleId, warnText("1.0", "1.0 > x"), false), DiktatError(5, 9, ruleId, warnText("1.0", "x.compareTo(1.0)"), false), DiktatError(6, 9, ruleId, warnText("1.0", "1.0.compareTo(x)"), false) ) } @Test @Tag(WarningNames.FLOAT_IN_ACCURATE_CALCULATIONS) fun `should detect comparisons with local floating-point variables - 1`() { lintMethod( """ |class Example { | fun foo() { | val x = 1.0 | x == 1 | } |} """.trimMargin(), DiktatError(4, 9, ruleId, warnText("x", "x == 1"), false) ) } @Test @Tag(WarningNames.FLOAT_IN_ACCURATE_CALCULATIONS) fun `should detect comparisons with local floating-point variables - 2`() { lintMethod( """ |class Example { | fun foo() { | val x = 1L | list.forEach { | val x = 1.0 | x == 1 | } | } |} """.trimMargin(), DiktatError(6, 13, ruleId, warnText("x", "x == 1"), false) ) } @Test @Tag(WarningNames.FLOAT_IN_ACCURATE_CALCULATIONS) fun `should detect comparisons with local floating-point variables - 3`() { lintMethod( """ |class Example { | fun foo() { | val x = 1L | list.forEach { | obj.let { | x == 1 | } | val x = 1.0 | } | } |} """.trimMargin() ) } @Test @Tag(WarningNames.FLOAT_IN_ACCURATE_CALCULATIONS) fun `should detect different operations with operators`() { lintMethod( """ |class Example { | fun foo() { | val x = 1.0 | x == 1 | x + 2 | // x++ | x += 2 | x - 2 | // x-- | x -= 2 | x * 2 | x *= 2 | x / 2 | x /= 2 | x % 2 | x %= 2 | } |} """.trimMargin(), DiktatError(4, 9, ruleId, warnText("x", "x == 1"), false), DiktatError(5, 9, ruleId, warnText("x", "x + 2"), false), // DiktatError(6, 9, ruleId, warnText("x", "x++"), false), DiktatError(7, 9, ruleId, warnText("x", "x += 2"), false), DiktatError(8, 9, ruleId, warnText("x", "x - 2"), false), // DiktatError(9, 9, ruleId, warnText("x", "x--"), false), DiktatError(10, 9, ruleId, warnText("x", "x -= 2"), false), DiktatError(11, 9, ruleId, warnText("x", "x * 2"), false), DiktatError(12, 9, ruleId, warnText("x", "x *= 2"), false), DiktatError(13, 9, ruleId, warnText("x", "x / 2"), false), DiktatError(14, 9, ruleId, warnText("x", "x /= 2"), false), DiktatError(15, 9, ruleId, warnText("x", "x % 2"), false), DiktatError(16, 9, ruleId, warnText("x", "x %= 2"), false) ) } @Test @Tag(WarningNames.FLOAT_IN_ACCURATE_CALCULATIONS) fun `should allow arithmetic operations inside abs in comparison`() { lintMethod( """ |import kotlin.math.abs | |fun foo() { | if (abs(1.0 - 0.999) < 1e-6) { | println("Comparison with tolerance") | } | | 1e-6 > abs(1.0 - 0.999) | abs(1.0 - 0.999).compareTo(1e-6) < 0 | 1e-6.compareTo(abs(1.0 - 0.999)) < 0 | abs(1.0 - 0.999) == 1e-6 | | abs(1.0 - 0.999) < eps | eps > abs(1.0 - 0.999) | | val x = 1.0 | val y = 0.999 | abs(x - y) < eps | eps > abs(x - y) | abs(1.0 - 0.999) == eps |} """.trimMargin(), DiktatError(11, 5, ruleId, warnText("1e-6", "abs(1.0 - 0.999) == 1e-6"), false), DiktatError(11, 9, ruleId, warnText("1.0", "1.0 - 0.999"), false), DiktatError(20, 9, ruleId, warnText("1.0", "1.0 - 0.999"), false) ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter4/NoVarRuleWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter4 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter4.ImmutableValNoVarRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames.SAY_NO_TO_VAR import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class NoVarRuleWarnTest : LintTestBase(::ImmutableValNoVarRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${ImmutableValNoVarRule.NAME_ID}" @Test @Tag(SAY_NO_TO_VAR) fun `valid case where x is used in while loop as some counter`() { lintMethod( """ | fun foo() { | var x = 0 | while (x < 10) { | x++ | } | } """.trimMargin(), ) } @Test @Tag(SAY_NO_TO_VAR) fun `valid case where y is used in for each loop as some counter, but a is not`() { lintMethod( """ | fun foo() { | var a = emptyList() | a = 15 | var y = 0 | a.forEach { x -> | y = x + 1 | } | } """.trimMargin(), DiktatError(2, 6, ruleId, "${Warnings.SAY_NO_TO_VAR.warnText()} var a = emptyList()", false) ) } @Test @Tag(SAY_NO_TO_VAR) fun `For loop with internal counter`() { lintMethod( """ | fun foo() { | for (x in 0..10) println(x) | } """.trimMargin() ) } @Test @Tag(SAY_NO_TO_VAR) fun `var in class`() { lintMethod( """ | class A { | var a = 0 | } """.trimMargin() ) } @Test @Tag(SAY_NO_TO_VAR) fun `var used simply in function`() { lintMethod( """ | fun foo(): Int { | var a = 0 | a = a + 15 | a = a + 56 | return a | } """.trimMargin(), DiktatError(2, 6, ruleId, "${Warnings.SAY_NO_TO_VAR.warnText()} var a = 0", false) ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter4/NullChecksRuleFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter4 import com.saveourtool.diktat.ruleset.rules.chapter4.NullChecksRule import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class NullChecksRuleFixTest : FixTestBase("test/paragraph4/null_checks", ::NullChecksRule) { @Test @Tag(WarningNames.AVOID_NULL_CHECKS) fun `should careful fix if conditions with break`() { fixAndCompare("IfConditionBreakCheckExpected.kt", "IfConditionBreakCheckTest.kt") } @Test @Tag(WarningNames.AVOID_NULL_CHECKS) fun `should fix if conditions`() { fixAndCompare("IfConditionNullCheckExpected.kt", "IfConditionNullCheckTest.kt") } @Test @Tag(WarningNames.AVOID_NULL_CHECKS) fun `should fix require function`() { fixAndCompare("RequireFunctionExpected.kt", "RequireFunctionTest.kt") } @Test @Tag(WarningNames.AVOID_NULL_CHECKS) fun `should fix if conditions when assigned`() { fixAndCompare("IfConditionAssignCheckExpected.kt", "IfConditionAssignCheckTest.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter4/NullChecksRuleWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter4 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter4.NullChecksRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class NullChecksRuleWarnTest : LintTestBase(::NullChecksRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${NullChecksRule.NAME_ID}" @Test @Tag(WarningNames.AVOID_NULL_CHECKS) fun `equals to null`() { lintMethod( """ | fun foo() { | var myVar: Int? = null | if (myVar == null) { | println("null") | return | } | } """.trimMargin(), DiktatError(3, 10, ruleId, "${Warnings.AVOID_NULL_CHECKS.warnText()} use '.let/.also/?:/e.t.c' instead of myVar == null", true), ) } @Test @Tag(WarningNames.AVOID_NULL_CHECKS) fun `equals to null in a chain of binary expressions`() { lintMethod( """ | fun foo() { | var myVar: Int? = null | if ((myVar == null) && (true) || isValid) { | println("null") | return | } | myVar ?: kotlin.run { | println("null") | } | } """.trimMargin(), DiktatError(3, 11, ruleId, Warnings.AVOID_NULL_CHECKS.warnText() + " use '.let/.also/?:/e.t.c' instead of myVar == null", true), ) } @Test @Tag(WarningNames.AVOID_NULL_CHECKS) fun `not equals to null`() { lintMethod( """ | fun foo() { | if (myVar != null) { | println("not null") | return | } | } """.trimMargin(), DiktatError(2, 10, ruleId, Warnings.AVOID_NULL_CHECKS.warnText() + " use '.let/.also/?:/e.t.c' instead of myVar != null", true), ) } @Test @Tag(WarningNames.AVOID_NULL_CHECKS) fun `if-else null comparison with return value`() { lintMethod( """ | fun foo() { | val anotherVal = if (myVar != null) { | println("not null") | 1 | } else { | 2 | } | } """.trimMargin(), DiktatError(2, 27, ruleId, Warnings.AVOID_NULL_CHECKS.warnText() + " use '.let/.also/?:/e.t.c' instead of myVar != null", true), ) } @Test @Tag(WarningNames.AVOID_NULL_CHECKS) fun `if-else null comparison with no return value`() { lintMethod( """ | fun foo() { | if (myVar !== null) { | println("not null") | } else { | println("null") | } | } """.trimMargin(), DiktatError(2, 10, ruleId, Warnings.AVOID_NULL_CHECKS.warnText() + " use '.let/.also/?:/e.t.c' instead of myVar !== null", true), ) } @Test @Tag(WarningNames.AVOID_NULL_CHECKS) fun `equals to null, but not in if`() { lintMethod( """ | fun foo0() { | if (true) { | fun foo() { | var myVar: Int? = null | val myVal = myVar == null | foo1(myVar == null) | println("null") | } | } | } """.trimMargin(), ) } @Test @Tag(WarningNames.AVOID_NULL_CHECKS) fun `equals to null, but in complex else-if statement`() { lintMethod( """ | fun foo0() { | if (myVar != null) { | println("not null") | } else if (true) { | println() | } | } """.trimMargin() ) } @Test @Tag(WarningNames.AVOID_NULL_CHECKS) fun `equals to null, but in complex else-if statement with dummy comment`() { lintMethod( """ | fun foo0() { | if (myVar != null) { | println("not null") | } else /* test comment */ if (true) { | println() | } | } """.trimMargin() ) } @Test @Tag(WarningNames.AVOID_NULL_CHECKS) fun `equals to null, but the expression is not a else-if`() { lintMethod( """ | fun foo0() { | if (myVar != null) { | println("not null") | } else { | if (true) { | println() | } | } | } """.trimMargin(), DiktatError(2, 10, ruleId, "${Warnings.AVOID_NULL_CHECKS.warnText()} use '.let/.also/?:/e.t.c'" + " instead of myVar != null", true), ) } @Test @Tag(WarningNames.AVOID_NULL_CHECKS) fun `require statements - adding `() { lintMethod( """ | fun foo0(myVar: String?) { | require(myVar != null) | } """.trimMargin(), DiktatError(2, 14, ruleId, Warnings.AVOID_NULL_CHECKS.warnText() + " use 'requireNotNull' instead of require(myVar != null)", true), ) } @Test @Tag(WarningNames.AVOID_NULL_CHECKS) fun `null check in lambda which is in if-statement is ok`() { lintMethod( """ |fun foo() { | if (leftSide?.any { it == null } == true) { | return | } |} """.trimMargin() ) } @Test @Tag(WarningNames.AVOID_NULL_CHECKS) fun `null check in lambda which is in require is ok`() { lintMethod( """ |fun foo() { | require(leftSide?.any { it == null }) |} """.trimMargin() ) } @Test @Tag(WarningNames.AVOID_NULL_CHECKS) fun `don't trigger inside 'init' block when more than one statement in 'else' block`() { lintMethod( """ |class Demo { | val one: Int | val two: String | | init { | val number = get() | if (number != null) { | one = number.toInt() | two = number | } else { | one = 0 | two = "0" | } | } | | private fun get(): String? = if (Math.random() > 0.5) { "1" } else { null } |} """.trimMargin() ) } @Test @Tag(WarningNames.AVOID_NULL_CHECKS) fun `trigger inside 'init' block when only one statement in 'else' block`() { lintMethod( """ |class Demo { | val one: Int = 0 | val two: String = "" | | init { | val number = get() | if (number != null) { | print(number + 1) | } else { | print(null) | } | } | | private fun get(): String? = if (Math.random() > 0.5) { "1" } else { null } |} """.trimMargin(), DiktatError(7, 13, ruleId, Warnings.AVOID_NULL_CHECKS.warnText() + " use '.let/.also/?:/e.t.c' instead of number != null", true), ) } @Test @Tag(WarningNames.AVOID_NULL_CHECKS) fun `trigger inside 'init' block when no 'else' block`() { lintMethod( """ |class Demo { | val one: Int = 0 | val two: String = "" | | init { | val number = get() | if (number != null) { | print(number) | } | } | | private fun get(): String? = if (Math.random() > 0.5) { "1" } else { null } |} """.trimMargin(), DiktatError(7, 13, ruleId, Warnings.AVOID_NULL_CHECKS.warnText() + " use '.let/.also/?:/e.t.c' instead of number != null", true), ) } @Test @Tag(WarningNames.AVOID_NULL_CHECKS) fun `don't trigger inside 'run', 'with', 'apply' scope functions when more than one statement in 'else' block`() { lintMethod( """ |class Demo { | | private fun set() { | val one: Int | val two: String | | run { | val number: String? = get() | if (number != null) { | one = number.toInt() | two = number | } else { | one = 0 | two = "0" | } | } | } | | private fun get(): String? = if (Math.random() > 0.5) { "1" } else { null } |} """.trimMargin() ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter4/SmartCastRuleFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter4 import com.saveourtool.diktat.ruleset.rules.chapter4.SmartCastRule import com.saveourtool.diktat.util.FixTestBase import org.junit.jupiter.api.Test class SmartCastRuleFixTest : FixTestBase("test/paragraph4/smart_cast", ::SmartCastRule) { @Test fun `should fix enum order`() { fixAndCompare("SmartCastExpected.kt", "SmartCastTest.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter4/SmartCastRuleWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter4 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter4.SmartCastRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames.SMART_CAST_NEEDED import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class SmartCastRuleWarnTest : LintTestBase(::SmartCastRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${SmartCastRule.NAME_ID}" @Test @Tag(SMART_CAST_NEEDED) fun `if with is smart cast good`() { lintMethod( """ |class Test { | val x = "" | fun someFun() { | if (x is String) { | val a = x.length | } | } |} """.trimMargin() ) } @Test @Tag(SMART_CAST_NEEDED) fun `if with is smart cast bad`() { lintMethod( """ |class Test { | val x = "" | fun someFun() { | if (x is String) { | val a = (x as String).length | } | } |} """.trimMargin(), DiktatError(5, 21, ruleId, "${Warnings.SMART_CAST_NEEDED.warnText()} x as String", true) ) } @Test @Tag(SMART_CAST_NEEDED) fun `if with another if with is smart cast good`() { lintMethod( """ |class Test { | val x = "" | fun someFun() { | if (x is String) { | val a = x.length | if (x is Int) { | val a = x.value | } | } | } |} """.trimMargin() ) } @Test @Tag(SMART_CAST_NEEDED) fun `if with another if with is smart cast bad`() { lintMethod( """ |class Test { | val x = "" | fun someFun() { | if (x is String) { | val a = x.length | if (x is Int) { | val a = (x as Int).value | } | } | } |} """.trimMargin(), DiktatError(7, 25, ruleId, "${Warnings.SMART_CAST_NEEDED.warnText()} x as Int", true) ) } @Test @Tag(SMART_CAST_NEEDED) fun `smart cast in else good`() { lintMethod( """ |class Test { | val x = "" | fun someFun() { | if (x !is String) { | val a = (x as String).length | } else { | val b = (x as String).length | } | } |} """.trimMargin(), DiktatError(7, 21, ruleId, "${Warnings.SMART_CAST_NEEDED.warnText()} x as String", true) ) } @Test @Tag(SMART_CAST_NEEDED) fun `smart cast in if without braces bad`() { lintMethod( """ |class Test { | val x = "" | fun someFun() { | if (x is String) | print((x as String).length) | } |} """.trimMargin(), DiktatError(5, 19, ruleId, "${Warnings.SMART_CAST_NEEDED.warnText()} x as String", true) ) } @Test @Tag(SMART_CAST_NEEDED) fun `smart cast in if without braces good`() { lintMethod( """ |class Test { | val x = "" | fun someFun() { | if (x is String) | print(x.length) | } |} """.trimMargin() ) } @Test @Tag(SMART_CAST_NEEDED) fun `smart cast in else without braces good`() { lintMethod( """ |class Test { | val x = "" | fun someFun() { | if (x !is String) { | print("asd") | } | else | print((x as String).length) | } |} """.trimMargin(), DiktatError(8, 19, ruleId, "${Warnings.SMART_CAST_NEEDED.warnText()} x as String", true) ) } @Test @Tag(SMART_CAST_NEEDED) fun `smart cast in else nested bad`() { lintMethod( """ |class Test { | val x = "" | fun someFun() { | if (x !is String) { | print("asd") | } | else { | print((x as String).length) | val a = "" | if (a !is String) { | | } else { | print((a as String).length) | } | } | } |} """.trimMargin(), DiktatError(8, 19, ruleId, "${Warnings.SMART_CAST_NEEDED.warnText()} x as String", true), DiktatError(13, 23, ruleId, "${Warnings.SMART_CAST_NEEDED.warnText()} a as String", true) ) } @Test @Disabled("Rule is simplified after https://github.com/saveourtool/diktat/issues/1168") @Tag(SMART_CAST_NEEDED) fun `smart cast in when bad`() { lintMethod( """ |class Test { | val x = "" | fun someFun() { | when (x) { | is Int -> print((x as Int).length) | is String -> print("String") | is Long -> x as Int | } | } |} """.trimMargin(), DiktatError(5, 29, ruleId, "${Warnings.SMART_CAST_NEEDED.warnText()} x as Int", true) ) } @Test @Tag(SMART_CAST_NEEDED) fun `smart cast in when good`() { lintMethod( """ |class Test { | val x = "" | fun someFun() { | when (x) { | is Int -> print(x.length) | is String -> print("String") | } | } |} """.trimMargin() ) } @Test @Tag(SMART_CAST_NEEDED) fun `smart cast in when good 2`() { lintMethod( """ |fun SomeClass.foo() = when (mutableProperty) { | is Foo -> (mutableProperty as Foo).fooFoo() // smart cast is required 'because 'mutableProperty' is a property that has open or custom getter' | else -> println("ok") |} """.trimMargin() ) } @Test @Tag(SMART_CAST_NEEDED) fun `if with multiple is good`() { lintMethod( """ |class Test { | val x = "" | val y = 3 | fun someFun() { | if (x is String || y is Int) { | val a = x.length | val b = y.value | } | } |} """.trimMargin() ) } @Test @Tag(SMART_CAST_NEEDED) fun `if with multiple is bad`() { lintMethod( """ |class Test { | val x = "" | val y = 3 | fun someFun() { | if (x is String || y is Int) { | val a = (x as String).length | val b = (y as Int).value | } | } |} """.trimMargin(), DiktatError(6, 21, ruleId, "${Warnings.SMART_CAST_NEEDED.warnText()} x as String", true), DiktatError(7, 21, ruleId, "${Warnings.SMART_CAST_NEEDED.warnText()} y as Int", true) ) } @Test @Tag(SMART_CAST_NEEDED) fun `if with function condition`() { lintMethod( """ |class Test { | val x = "" | val list = listOf(1,2,3) | fun someFun() { | if (list.filter { it is Foo }.all { it.bar() }) { | val a = x.length | val b = y.value | } | } |} """.trimMargin() ) } @Test @Tag(SMART_CAST_NEEDED) fun `if with shadowed var good`() { lintMethod( """ |class Test { | val x = "" | fun someFun() { | if (x is String) { | val x = 5 | val a = (x as String).length | } | } |} """.trimMargin() ) } @Test @Tag(SMART_CAST_NEEDED) fun `if with shadowed var bad`() { lintMethod( """ |class Test { | val x = "" | fun someFun() { | if (x is String) { | val x = 5 | val a = (x as String).length | if (x is Int) { | val b = (x as Int).value | } | } | } |} """.trimMargin(), DiktatError(8, 25, ruleId, "${Warnings.SMART_CAST_NEEDED.warnText()} x as Int", true) ) } @Test @Tag(SMART_CAST_NEEDED) fun `if with shadowed var bad 2`() { lintMethod( """ |class Test { | val x = "" | fun someFun() { | if (x is String) { | val x = 5 | val a = (x as Int).length | } | } |} """.trimMargin(), DiktatError(6, 21, ruleId, "${Warnings.SMART_CAST_NEEDED.warnText()} x as Int", true) ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter4/TypeAliasRuleWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter4 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.TYPE_ALIAS import com.saveourtool.diktat.ruleset.rules.chapter4.TypeAliasRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class TypeAliasRuleWarnTest : LintTestBase(::TypeAliasRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${TypeAliasRule.NAME_ID}" private val rulesConfigListShortType: List = listOf( RulesConfig(TYPE_ALIAS.name, true, mapOf("typeReferenceLength" to "4")) ) @Test @Tag(WarningNames.TYPE_ALIAS) fun `long reference with several MutableMaps`() { lintMethod( """ | val b: MutableMap> | val b = listof() """.trimMargin(), DiktatError(1, 9, ruleId, "${TYPE_ALIAS.warnText()} too long type reference", false) ) } @Test @Tag(WarningNames.TYPE_ALIAS) fun `check long lambda property`() { lintMethod( """ | var emitWarn: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit | var emitWarn: (offset: Int, (T) -> Boolean) -> Unit """.trimMargin(), DiktatError(1, 16, ruleId, "${TYPE_ALIAS.warnText()} too long type reference", false), DiktatError(2, 16, ruleId, "${TYPE_ALIAS.warnText()} too long type reference", false) ) } @Test @Tag(WarningNames.TYPE_ALIAS) fun `correct type length`() { lintMethod( """ | var emitWarn: Int | val b = mutableMapOf>() | | fun foo(): MutableMap> { | } | """.trimMargin(), DiktatError(4, 13, ruleId, "${TYPE_ALIAS.warnText()} too long type reference", false) ) } @Test @Tag(WarningNames.TYPE_ALIAS) fun `correct type length but with configuration`() { lintMethod( """ | var emitWarn: Int | val flag: (T) -> Boolean | val list: List> | """.trimMargin(), DiktatError(3, 12, ruleId, "${TYPE_ALIAS.warnText()} too long type reference", false), rulesConfigList = rulesConfigListShortType ) } @Test @Tag(WarningNames.TYPE_ALIAS) fun `should ignore inheritance`() { lintMethod( """ | class A : JsonResourceConfigReader>() { | fun foo() : JsonResourceConfigReader> {} | val q: JsonResourceConfigReader>? = null | fun goo() { | class B : JsonResourceConfigReader> {} | } | } """.trimMargin(), DiktatError(2, 16, ruleId, "${TYPE_ALIAS.warnText()} too long type reference", false), DiktatError(3, 11, ruleId, "${TYPE_ALIAS.warnText()} too long type reference", false) ) } @Test @Tag(WarningNames.TYPE_ALIAS) fun `check correct examle`() { lintMethod( """ |typealias jsonType = JsonResourceConfigReader> |class A : JsonResourceConfigReader>() { | | fun foo() : jsonType {} | val q: jsonType? = null | fun goo() { | class B : JsonResourceConfigReader> {} | } |} """.trimMargin() ) } @Test @Tag(WarningNames.TYPE_ALIAS) fun `check lazy property`() { lintMethod( """ |class A { | val q: List> by lazy { | emptyList>() | } |} """.trimMargin(), DiktatError(2, 11, ruleId, "${TYPE_ALIAS.warnText()} too long type reference", false), rulesConfigList = rulesConfigListShortType ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter4/VariableGenericTypeDeclarationRuleFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter4 import com.saveourtool.diktat.ruleset.rules.chapter4.VariableGenericTypeDeclarationRule import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class VariableGenericTypeDeclarationRuleFixTest : FixTestBase("test/paragraph4/generics", ::VariableGenericTypeDeclarationRule) { @Test @Tag(WarningNames.GENERIC_VARIABLE_WRONG_DECLARATION) fun `basic fix test`() { fixAndCompare("VariableGenericTypeDeclarationExpected.kt", "VariableGenericTypeDeclarationTest.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter4/VariableGenericTypeDeclarationRuleWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter4 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter4.VariableGenericTypeDeclarationRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames.GENERIC_VARIABLE_WRONG_DECLARATION import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class VariableGenericTypeDeclarationRuleWarnTest : LintTestBase(::VariableGenericTypeDeclarationRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${VariableGenericTypeDeclarationRule.NAME_ID}" @Test @Tag(GENERIC_VARIABLE_WRONG_DECLARATION) fun `property with generic type good`() { lintMethod( """ |class SomeClass { | val myVariable: Map = emptyMap() | val lazyValue: Map by lazy { | println("computed!") | emptyMap() | } | val sideRegex = Regex("<([a-zA-Z, <>?]+)>") | val str = someMethod("mapOf") | val x = foo.bar().baz() |} """.trimMargin() ) } @Test @Tag(GENERIC_VARIABLE_WRONG_DECLARATION) fun `property with generic type bad`() { lintMethod( """ |class SomeClass { | val myVariable: Map = emptyMap() | val any = Array(3) { "" } | val x = foo.bar().baz() |} """.trimMargin(), DiktatError(2, 4, ruleId, "${Warnings.GENERIC_VARIABLE_WRONG_DECLARATION.warnText()} type arguments are unnecessary in emptyMap()", true), DiktatError(3, 4, ruleId, "${Warnings.GENERIC_VARIABLE_WRONG_DECLARATION.warnText()} val any = Array(3) { \"\" }", false), DiktatError(4, 4, ruleId, "${Warnings.GENERIC_VARIABLE_WRONG_DECLARATION.warnText()} val x = foo.bar().baz()", false) ) } @Test @Tag(GENERIC_VARIABLE_WRONG_DECLARATION) fun `property in function as parameter good`() { lintMethod( """ |class SomeClass { | private fun someFunc(myVariable: Map = emptyMap()) { | | } |} """.trimMargin() ) } @Test @Tag(GENERIC_VARIABLE_WRONG_DECLARATION) fun `property in function as parameter with wildcard type good`() { lintMethod( """ |class SomeClass { | private fun someFunc(myVariable: List<*> = emptyList()) { | | } |} """.trimMargin() ) } @Test @Tag(GENERIC_VARIABLE_WRONG_DECLARATION) fun `property in function as parameter with wildcard type good 2`() { lintMethod( """ |class SomeClass { | private fun someFunc(myVariable: Map<*, String> = emptyMap()) { | | } |} """.trimMargin() ) } @Test @Tag(GENERIC_VARIABLE_WRONG_DECLARATION) fun `property in function as parameter bad`() { lintMethod( """ |class SomeClass { | private fun someFunc(myVariable: Map = emptyMap()) { | | } |} """.trimMargin(), DiktatError(2, 25, ruleId, "${Warnings.GENERIC_VARIABLE_WRONG_DECLARATION.warnText()} type arguments are unnecessary in emptyMap()", true) ) } @Test @Tag(GENERIC_VARIABLE_WRONG_DECLARATION) fun `property in function good`() { lintMethod( """ |class SomeClass { | private fun someFunc() { | val myVariable: Map = emptyMap() | } |} """.trimMargin() ) } @Test @Tag(GENERIC_VARIABLE_WRONG_DECLARATION) fun `property in function with wildcard type good`() { lintMethod( """ |class SomeClass { | private fun someFunc() { | val myVariable: List<*> = emptyList() | } |} """.trimMargin() ) } @Test @Tag(GENERIC_VARIABLE_WRONG_DECLARATION) fun `property in function bad`() { lintMethod( """ |class SomeClass { | private fun someFunc() { | val myVariable: Map = emptyMap() | } |} """.trimMargin(), DiktatError(3, 8, ruleId, "${Warnings.GENERIC_VARIABLE_WRONG_DECLARATION.warnText()} type arguments are unnecessary in emptyMap()", true) ) } @Test @Tag(GENERIC_VARIABLE_WRONG_DECLARATION) fun `property in class good`() { lintMethod( """ |class SomeClass(val myVariable: Map = emptyMap()) { | |} """.trimMargin() ) } @Test @Tag(GENERIC_VARIABLE_WRONG_DECLARATION) fun `property in class with wildcard type good`() { lintMethod( """ |class SomeClass(val myVariable: List<*> = emptyList()) { | |} """.trimMargin() ) } @Test @Tag(GENERIC_VARIABLE_WRONG_DECLARATION) fun `property in class bad`() { lintMethod( """ |class SomeClass(val myVariable: Map = emptyMap()) { | |} """.trimMargin(), DiktatError(1, 17, ruleId, "${Warnings.GENERIC_VARIABLE_WRONG_DECLARATION.warnText()} type arguments are unnecessary in emptyMap()", true) ) } @Test @Tag(GENERIC_VARIABLE_WRONG_DECLARATION) fun `property with multiple generics`() { lintMethod( """ |class SomeClass { | private fun someFunc() { | val myVariable: Map> = emptyMap>() | } |} """.trimMargin(), DiktatError(3, 8, ruleId, "${Warnings.GENERIC_VARIABLE_WRONG_DECLARATION.warnText()} type arguments are unnecessary in emptyMap>()", true) ) } @Test @Tag(GENERIC_VARIABLE_WRONG_DECLARATION) fun `should not trigger`() { lintMethod( """ |class SomeClass { | private fun someFunc() { | var myVariable: Map = emptyMap() | } |} """.trimMargin() ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter5/AsyncAndSyncRuleTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter5 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings.RUN_BLOCKING_INSIDE_ASYNC import com.saveourtool.diktat.ruleset.rules.chapter5.AsyncAndSyncRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class AsyncAndSyncRuleTest : LintTestBase(::AsyncAndSyncRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${AsyncAndSyncRule.NAME_ID}" @Test @Tag(WarningNames.RUN_BLOCKING_INSIDE_ASYNC) fun `test wrong case`() { lintMethod( """ |fun foo() { | GlobalScope.launch { | c.addAndGet(i) | } | | GlobalScope.async { | n | } | | GlobalScope.async { | runBlocking { | | } | } |} | |suspend fun foo() { | runBlocking { | delay(2000) | } |} | """.trimMargin(), DiktatError(11, 8, ruleId, "${RUN_BLOCKING_INSIDE_ASYNC.warnText()} runBlocking", false), DiktatError(18, 4, ruleId, "${RUN_BLOCKING_INSIDE_ASYNC.warnText()} runBlocking", false) ) } @Test @Tag(WarningNames.RUN_BLOCKING_INSIDE_ASYNC) fun `test dot qualified expression case`() { lintMethod( """ |fun foo() { | GlobalScope.async { | node.runBlocking() | runBlocking { | n++ | } | } |} | |fun goo() { | runBlocking { | GlobalScope.async { | n++ | } | } |} """.trimMargin(), DiktatError(4, 8, ruleId, "${RUN_BLOCKING_INSIDE_ASYNC.warnText()} runBlocking", false) ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter5/AvoidNestedFunctionsFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter5 import com.saveourtool.diktat.ruleset.rules.chapter5.AvoidNestedFunctionsRule import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class AvoidNestedFunctionsFixTest : FixTestBase("test/paragraph5/nested_functions", ::AvoidNestedFunctionsRule) { @Test @Tag(WarningNames.AVOID_NESTED_FUNCTIONS) fun `fix nested functions`() { fixAndCompare("AvoidNestedFunctionsExample.kt", "AvoidNestedFunctionsTest.kt") } @Test @Tag(WarningNames.AVOID_NESTED_FUNCTIONS) fun `fix several nested functions`() { fixAndCompare("AvoidNestedFunctionsSeveralExample.kt", "AvoidNestedFunctionsSeveralTest.kt") } @Test @Tag(WarningNames.AVOID_NESTED_FUNCTIONS) fun `should not change`() { fixAndCompare("AvoidNestedFunctionsNoTriggerExample.kt", "AvoidNestedFunctionsNoTriggerTest.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter5/AvoidNestedFunctionsWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter5 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter5.AvoidNestedFunctionsRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames.AVOID_NESTED_FUNCTIONS import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class AvoidNestedFunctionsWarnTest : LintTestBase(::AvoidNestedFunctionsRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${AvoidNestedFunctionsRule.NAME_ID}" @Test @Tag(AVOID_NESTED_FUNCTIONS) fun `nested function`() { lintMethod( """ |fun foo() { | someFunc() | fun bar(a:String, b:Int) { | val param = 5 | val repeatFun: String.(Int) -> String = { times -> this.repeat(times) } | param.value | repeatFun(45) | some(a) | some(b) | } | |} """.trimMargin(), DiktatError(3, 4, ruleId, "${Warnings.AVOID_NESTED_FUNCTIONS.warnText()} fun bar", false) ) } @Test @Tag(AVOID_NESTED_FUNCTIONS) fun `anonymous function`() { val code = """ package com.saveourtool.diktat.test fun foo() { val sum: (Int) -> Int = fun(x): Int = x + x } """.trimIndent() lintMethod(code) } @Test @Tag(AVOID_NESTED_FUNCTIONS) fun `several nested functions`() { lintMethod( """ |fun foo() { | | fun bar() { | fun baz() { | } | } | |} """.trimMargin(), DiktatError(3, 4, ruleId, "${Warnings.AVOID_NESTED_FUNCTIONS.warnText()} fun bar", true), DiktatError(4, 8, ruleId, "${Warnings.AVOID_NESTED_FUNCTIONS.warnText()} fun baz", true) ) } @Test @Tag(AVOID_NESTED_FUNCTIONS) fun `no nested functions`() { lintMethod( """ |class SomeClass { | fun someFunc() {} | | fun anotherFunc() {} | | fun moreFunction() {} |} """.trimMargin() ) } @Test @Tag(AVOID_NESTED_FUNCTIONS) fun `nested functions in anonymous class`() { lintMethod( """ |class SomeClass { |fun some() { | listOf( | RuleSet("test", object : Rule("astnode-utils-test") { | override fun visit(node: ASTNode, | autoCorrect: Boolean, | emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit) { | applyToNode(node, counter) | } | })) | } |} """.trimMargin() ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter5/CheckInverseMethodRuleFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter5 import com.saveourtool.diktat.ruleset.rules.chapter5.CheckInverseMethodRule import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames.INVERSE_FUNCTION_PREFERRED import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class CheckInverseMethodRuleFixTest : FixTestBase("test/paragraph5/method_call_names", ::CheckInverseMethodRule) { @Test @Tag(INVERSE_FUNCTION_PREFERRED) fun `should fix method calls`() { fixAndCompare("ReplaceMethodCallNamesExpected.kt", "ReplaceMethodCallNamesTest.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter5/CheckInverseMethodRuleWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter5 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter5.CheckInverseMethodRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames.INVERSE_FUNCTION_PREFERRED import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class CheckInverseMethodRuleWarnTest : LintTestBase(::CheckInverseMethodRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${CheckInverseMethodRule.NAME_ID}" @Test @Tag(INVERSE_FUNCTION_PREFERRED) fun `should not raise warning`() { lintMethod( """ |fun some() { | if (list.isEmpty()) { | // some cool logic | } |} """.trimMargin() ) } @Test @Tag(INVERSE_FUNCTION_PREFERRED) fun `should raise warning`() { lintMethod( """ |fun some() { | if (!list.isEmpty()) { | // some cool logic | } |} """.trimMargin(), DiktatError(2, 14, ruleId, "${Warnings.INVERSE_FUNCTION_PREFERRED.warnText()} isNotEmpty() instead of !isEmpty()", true) ) } @Test @Tag(INVERSE_FUNCTION_PREFERRED) fun `should consider white spaces between ! and call expression`() { lintMethod( """ |fun some() { | if (! list.isEmpty()) { | // some cool logic | } |} """.trimMargin(), DiktatError(2, 16, ruleId, "${Warnings.INVERSE_FUNCTION_PREFERRED.warnText()} isNotEmpty() instead of !isEmpty()", true) ) } @Test @Tag(INVERSE_FUNCTION_PREFERRED) fun `should consider comments between ! and call expression`() { lintMethod( """ |fun some() { | if (! /*cool comment*/ list.isEmpty()) { | // some cool logic | } |} """.trimMargin(), DiktatError(2, 32, ruleId, "${Warnings.INVERSE_FUNCTION_PREFERRED.warnText()} isNotEmpty() instead of !isEmpty()", true) ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter5/CustomLabelsTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter5 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings.CUSTOM_LABEL import com.saveourtool.diktat.ruleset.rules.chapter5.CustomLabel import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class CustomLabelsTest : LintTestBase(::CustomLabel) { private val ruleId = "$DIKTAT_RULE_SET_ID:${CustomLabel.NAME_ID}" @Test @Tag(WarningNames.CUSTOM_LABEL) fun `should trigger custom label`() { lintMethod( """ fun foo() { run qwe@ { q.forEach { return@qwe } } q.forEachIndexed { index, i -> return@forEachIndexed } loop@ for(i: Int in q) { println(i) break@loop } qq@ for(i: Int in q) { println(i) break@qq } q.run { it.map { it.foreach{ break@forEach } } } } """.trimMargin(), DiktatError(4, 39, ruleId, "${CUSTOM_LABEL.warnText()} @qwe", false), DiktatError(16, 34, ruleId, "${CUSTOM_LABEL.warnText()} @qq", false) ) } @Test @Tag(WarningNames.CUSTOM_LABEL) fun `should not trigger custom label in nested expression`() { lintMethod( """ fun foo() { qq@ for(i: Int in q) { for (j: Int in q) { println(i) break@qq } } q.forEach outer@ { it.forEach { if(it == 21) return@outer } } } """.trimMargin() ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter5/FunctionArgumentsSizeWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter5 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.TOO_MANY_PARAMETERS import com.saveourtool.diktat.ruleset.rules.chapter5.FunctionArgumentsSize import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class FunctionArgumentsSizeWarnTest : LintTestBase(::FunctionArgumentsSize) { private val ruleId = "$DIKTAT_RULE_SET_ID:${FunctionArgumentsSize.NAME_ID}" private val rulesConfigList: List = listOf( RulesConfig(TOO_MANY_PARAMETERS.name, true, mapOf("maxParameterListSize" to "1")) ) @Test @Tag(WarningNames.TOO_MANY_PARAMETERS) fun `check simple function`() { lintMethod( """ |fun foo(a: Int, b: Int, c: Int, d: Int, e: Int, f: Int) {} |fun foo(a: Int, b: Int, c: Int, d: Int, e: Int, myLambda: () -> Unit) {} |fun foo(a: Int, b: Int, c: Int, d: Int, myLambda: () -> Unit) {} |fun foo(a: Int, b: Int, c: Int, d: Int, e: Int, myLambda: () -> Unit) = 10 |abstract fun foo(a: Int, b: Int, c: Int, d: Int, e: Int, f: Int) """.trimMargin(), DiktatError(1, 1, ruleId, "${TOO_MANY_PARAMETERS.warnText()} foo has 6, but allowed 5", false), DiktatError(2, 1, ruleId, "${TOO_MANY_PARAMETERS.warnText()} foo has 6, but allowed 5", false), DiktatError(4, 1, ruleId, "${TOO_MANY_PARAMETERS.warnText()} foo has 6, but allowed 5", false), DiktatError(5, 1, ruleId, "${TOO_MANY_PARAMETERS.warnText()} foo has 6, but allowed 5", false) ) } @Test @Tag(WarningNames.TOO_MANY_PARAMETERS) fun `check function with config`() { lintMethod( """ |fun foo(a: Int, b: Int) {} """.trimMargin(), DiktatError(1, 1, ruleId, "${TOO_MANY_PARAMETERS.warnText()} foo has 2, but allowed 1", false), rulesConfigList = rulesConfigList ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter5/FunctionLengthWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter5 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.TOO_LONG_FUNCTION import com.saveourtool.diktat.ruleset.rules.chapter5.FunctionLength import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class FunctionLengthWarnTest : LintTestBase(::FunctionLength) { private val ruleId = "$DIKTAT_RULE_SET_ID:${FunctionLength.NAME_ID}" private val rulesConfigList: List = listOf( RulesConfig(TOO_LONG_FUNCTION.name, true, mapOf("maxFunctionLength" to "5")) ) private val shortRulesConfigList: List = listOf( RulesConfig(TOO_LONG_FUNCTION.name, true, mapOf("maxFunctionLength" to "2")) ) private val shortRulesWithoutHeaderConfigList: List = listOf( RulesConfig(TOO_LONG_FUNCTION.name, true, mapOf("maxFunctionLength" to "3", "isIncludeHeader" to "false")) ) @Test @Tag(WarningNames.TOO_LONG_FUNCTION) fun `check with all comment`() { lintMethod( """ |fun foo() { | | //dkfgvdf | | /* | * jkgh | */ | | /** | * dfkjvbhdfkjb | */ | | val x = 10 | val y = 100 | println(x) | val z = x + y | println(x) | |} """.trimMargin(), DiktatError(1, 1, ruleId, "${TOO_LONG_FUNCTION.warnText()} max length is 5, but you have 7", false), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.TOO_LONG_FUNCTION) fun `less than max`() { lintMethod( """ |fun foo() { | | | | | | | val x = 10 | val y = 100 | println(x) | |} """.trimMargin(), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.TOO_LONG_FUNCTION) fun `one line function`() { lintMethod( """ |fun foo(list: List) = list.forEach { | if (it.element == "dfscv") | println() |} | |fun goo() = | 10 """.trimMargin(), DiktatError(1, 1, ruleId, "${TOO_LONG_FUNCTION.warnText()} max length is 2, but you have 4", false), rulesConfigList = shortRulesConfigList ) } @Test @Tag(WarningNames.TOO_LONG_FUNCTION) fun `fun in class`() { lintMethod( """ |class A() { | val x = 10 | val y = 11 | | fun foo() { | if(true) { | while(true) { | println(x) | println(y) | } | } | } | |} """.trimMargin(), DiktatError(5, 4, ruleId, "${TOO_LONG_FUNCTION.warnText()} max length is 2, but you have 8", false), rulesConfigList = shortRulesConfigList ) } @Test @Tag(WarningNames.TOO_LONG_FUNCTION) fun `check suppress`() { lintMethod( """ |@Suppress("TOO_LONG_FUNCTION") |class A() { | val x = 10 | val y = 11 | | | fun foo() { | if(true) { | while(true) { | println(x) | println(y) | } | } | } | |} """.trimMargin(), rulesConfigList = shortRulesConfigList ) } @Test @Tag(WarningNames.TOO_LONG_FUNCTION) fun `only empty lines`() { lintMethod( """ |fun foo(list: List) { | | | | |} """.trimMargin(), rulesConfigList = shortRulesConfigList ) } @Test @Tag(WarningNames.TOO_LONG_FUNCTION) fun `fun longer but without body`() { lintMethod( """ |class A() { | val x = 10 | val y = 11 | | fun foo() | { | println(123) | } | |} | |abstract class B { | abstract fun foo() |} """.trimMargin(), rulesConfigList = shortRulesWithoutHeaderConfigList ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter5/LambdaLengthWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter5 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter5.LambdaLengthRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class LambdaLengthWarnTest : LintTestBase(::LambdaLengthRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${LambdaLengthRule.NAME_ID}" private val rulesConfigList: List = listOf( RulesConfig( Warnings.TOO_MANY_LINES_IN_LAMBDA.name, true, mapOf("maxLambdaLength" to "3")) ) @Test @Tag(WarningNames.TOO_MANY_LINES_IN_LAMBDA) fun `less than max`() { lintMethod( """ |fun foo() { | val x = 10 | val list = listOf(1, 2, 3, 4, 5) | .map {element -> element + x} |} """.trimMargin(), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.TOO_MANY_LINES_IN_LAMBDA) fun `nested lambda with implicit parameter`() { lintMethod( """ |fun foo() { | private val allTestsFromResources: List by lazy { | val fileUrl: URL? = javaClass.getResource("123") | val resource = fileUrl | ?.let { File(it.file) } | } |} """.trimMargin(), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.TOO_MANY_LINES_IN_LAMBDA) fun `lambda doesn't expect parameters`() { lintMethod( """ |fun foo() { | private val allTestsFromResources: List by lazy { | val fileUrl: URL? = javaClass.getResource("123") | list = listOf(1, 2, 3, 4, 5) | .removeAt(1) | } |} """.trimMargin(), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.TOO_MANY_LINES_IN_LAMBDA) fun `less than max without argument`() { lintMethod( """ |fun foo() { | val x = 10 | val list = listOf(1, 2, 3, 4, 5) | .map {it + x} |} """.trimMargin(), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.TOO_MANY_LINES_IN_LAMBDA) fun `more than max with argument`() { lintMethod( """ |fun foo() { | val calculateX = { x : Int -> | when(x) { | in 0..40 -> "Fail" | in 41..70 -> "Pass" | in 71..100 -> "Distinction" | else -> false | } | } |} """.trimMargin(), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.TOO_MANY_LINES_IN_LAMBDA) fun `more than maximum without argument`() { lintMethod( """ |fun foo() { | val list = listOf(1, 2, 3, 4, 5) | .map { | val x = 0 | val y = x + 1 | val z = y + 1 | it + z | | | | | } |} """.trimMargin(), DiktatError(3, 13, ruleId, "${Warnings.TOO_MANY_LINES_IN_LAMBDA.warnText()} max length lambda without arguments is 3, but you have 6", false), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.TOO_MANY_LINES_IN_LAMBDA) fun `two lambda more than maximum without argument`() { lintMethod( """ |fun foo() { | val list = listOf(1, 2, 3, 4, 5) | .filter { n -> n % 2 == 1 } | .map { | val x = 0 | val y = x + 1 | val z = y + 1 | it + z | | | | | } |} """.trimMargin(), DiktatError(4, 13, ruleId, "${Warnings.TOO_MANY_LINES_IN_LAMBDA.warnText()} max length lambda without arguments is 3, but you have 6", false), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.TOO_MANY_LINES_IN_LAMBDA) fun `lambda in lambda`() { lintMethod( """ |fun foo() { | val list = listOf(listOf(1,2,3), listOf(4,5,6)) | .map {l -> l.map { | val x = 0 | val y = x + 1 | val z = y + 1 | println(it) | } | } | } """.trimMargin(), DiktatError(3, 25, ruleId, "${Warnings.TOO_MANY_LINES_IN_LAMBDA.warnText()} max length lambda without arguments is 3, but you have 6", false), rulesConfigList = rulesConfigList ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter5/LambdaParameterOrderWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter5 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings.LAMBDA_IS_NOT_LAST_PARAMETER import com.saveourtool.diktat.ruleset.rules.chapter5.LambdaParameterOrder import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class LambdaParameterOrderWarnTest : LintTestBase(::LambdaParameterOrder) { private val ruleId = "$DIKTAT_RULE_SET_ID:${LambdaParameterOrder.NAME_ID}" @Test @Tag(WarningNames.LAMBDA_IS_NOT_LAST_PARAMETER) fun `check simple example`() { lintMethod( """ |fun foo(a: Int, myLambda: () -> Unit, b: Int) { } | |fun foo(a: Int, b: Int, myLambda: () -> Unit) = true | |@Suppress("LAMBDA_IS_NOT_LAST_PARAMETER") |fun foo(a: Int, myLambda: () -> Unit, b: Int) { } | |fun foo(a: Int, myLambdab: () -> Unit, myLambda: () -> Unit) | |fun foo(a: Int? = null, myLambdab: () -> Unit, myLambda: () -> Unit) | |fun foo(lambda1: () -> Unit, lambda2: (() -> Unit)?) {} """.trimMargin(), DiktatError(1, 17, ruleId, "${LAMBDA_IS_NOT_LAST_PARAMETER.warnText()} foo", false) ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter5/NestedFunctionBlockWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter5 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.NESTED_BLOCK import com.saveourtool.diktat.ruleset.rules.chapter5.NestedFunctionBlock import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class NestedFunctionBlockWarnTest : LintTestBase(::NestedFunctionBlock) { private val ruleId = "$DIKTAT_RULE_SET_ID:${NestedFunctionBlock.NAME_ID}" private val rulesConfigList = listOf( RulesConfig(NESTED_BLOCK.name, true, mapOf("maxNestedBlockQuantity" to "2")) ) @Test @Tag(WarningNames.NESTED_BLOCK) fun `should ignore lambda expression`() { lintMethod( """ |fun foo() { | while(true) { | println() | } | | when(x) { | 10 -> {10} | else -> { | if (true) { | println(1) | } | } | } | | val x = { | if (true) { | if (false) { | while(false) { | 10 | } | } | } | } | | for(x in 1..2){ | println(x) | } |} """.trimMargin() ) } @Test @Tag(WarningNames.NESTED_BLOCK) fun `check simple nested block`() { lintMethod( """ |fun foo() { | | if (true) { | if (false) { | if (true) { | do { | println("nested") | } while(true) | } | } | } else { | println("dscsds") | } |} """.trimMargin(), DiktatError(1, 1, ruleId, "${NESTED_BLOCK.warnText()} foo", false) ) } @Test @Tag(WarningNames.NESTED_BLOCK) fun `check simple nested block with try`() { lintMethod( """ |fun foo() { | | if (true) { | if (false) { | try { | try{ | try{ | | } catch(ex: Exception){ | try{ | println("hi") | } catch(ex: Exception){} | } | } catch(ex: Exception){} | } catch(ex: Exception){} | } | } else { | println("dscsds") | } |} """.trimMargin(), DiktatError(1, 1, ruleId, "${NESTED_BLOCK.warnText()} foo", false) ) } @Test @Tag(WarningNames.NESTED_BLOCK) fun `check simple nested block of function`() { lintMethod( """ |fun foo() { | | if (true) { | if (false) { | fun goo() { | if(true) { | | } | } | } | } else { | println("dscsds") | } |} """.trimMargin() ) } @Test @Tag(WarningNames.NESTED_BLOCK) fun `check simple nested block of local class`() { lintMethod( """ |fun foo() { | class A() { | fun goo() { | if (true) { | if (false) { | while(true) { | if(false) { | try { | println("ne") | } catch (e: Exception) {} | } | } | } | } else { | println("dscsds") | } | } | } | if(true) { | while(true) { | } | } |} """.trimMargin(), DiktatError(3, 8, ruleId, "${NESTED_BLOCK.warnText()} goo", false) ) } @Test @Tag(WarningNames.NESTED_BLOCK) fun `check with lambda`() { lintMethod( """ private fun findBlocks(node: ASTNode): List { val result = mutableListOf() node.getChildren(null).forEach { when (it.elementType) { IF -> Pair(it.findChildByType(THEN)?.findChildByType(BLOCK), it.findChildByType(ELSE)?.findChildByType(BLOCK)) WHEN -> Pair(it, null) WHEN_ENTRY -> Pair(it.findChildByType(BLOCK), null) FUN -> Pair(it.findChildByType(BLOCK), null) else -> Pair(it.findChildByType(BODY)?.findChildByType(BLOCK), null) }.let { pair -> pair.let { pair.first?.let { it1 -> result.add(it1) } pair.second?.let { it2 -> result.add(it2) } } } } return result } """.trimMargin() ) } @Test @Tag(WarningNames.NESTED_BLOCK) fun `check with anonymous class`() { lintMethod( """ val q = list.filter {it == 0} val keyListener = KeyAdapter { keyEvent -> if (true) { } else if (false) { while(true) { if(true) { println(10) } } } } val keyListener = object : KeyAdapter() { override fun keyPressed(keyEvent : KeyEvent) { } } """.trimMargin(), DiktatError(4, 50, ruleId, "${NESTED_BLOCK.warnText()} { keyEvent ->...", false), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.NESTED_BLOCK) fun `check simple nested block inside class`() { lintMethod( """ |class A { | fun foo() { | if(true) { | if(false) { | if(true) { | when(x) { | } | } | } | } | } | | fun goo() { | if(true){ | if(false){ | if(true){ | } | } | } | } |} """.trimMargin(), DiktatError(2, 4, ruleId, "${NESTED_BLOCK.warnText()} foo", false) ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter5/OverloadingArgumentsFunctionWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter5 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings.WRONG_OVERLOADING_FUNCTION_ARGUMENTS import com.saveourtool.diktat.ruleset.rules.chapter5.OverloadingArgumentsFunction import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class OverloadingArgumentsFunctionWarnTest : LintTestBase(::OverloadingArgumentsFunction) { private val ruleId = "$DIKTAT_RULE_SET_ID:${OverloadingArgumentsFunction.NAME_ID}" @Test @Tag(WarningNames.WRONG_OVERLOADING_FUNCTION_ARGUMENTS) fun `check simple example`() { lintMethod( """ |fun foo() {} | |fun foo(a: Int) {} | |fun goo(a: Double) {} | |fun goo(a: Float, b: Double) {} | |fun goo(b: Float, a: Double, c: Int) {} | |@Suppress("WRONG_OVERLOADING_FUNCTION_ARGUMENTS") |fun goo(a: Float) | |fun goo(a: Double? = 0.0) {} | |override fun goo() {} // this definitely is not an overload case... why we were treating it as an overload? New diktat rule! | |class A { | fun foo() {} |} | |abstract class B { | abstract fun foo(a: Int) // modifiers are different. This is not related to default arguments. New diktat rule! | | fun foo(){} |} """.trimMargin(), DiktatError(1, 1, ruleId, "${WRONG_OVERLOADING_FUNCTION_ARGUMENTS.warnText()} foo", false), ) } @Test @Tag(WarningNames.WRONG_OVERLOADING_FUNCTION_ARGUMENTS) fun `functions with modifiers`() { lintMethod( """ |public fun foo() {} |private fun foo(a: Int) {} |inline fun foo(a: Int, b: Int) {} """.trimMargin(), ) } @Test @Tag(WarningNames.WRONG_OVERLOADING_FUNCTION_ARGUMENTS) fun `functions with override modifiers`() { lintMethod( """ |fun list(projectCoordinates: ProjectCoordinates): Flux = list() | .filter { it.projectCoordinates == projectCoordinates } | |override fun list(): Flux = newFileStorage.list() | .map { fileDto -> | FileKey( | projectCoordinates = fileDto.projectCoordinates, | name = fileDto.name, | uploadedMillis = fileDto.uploadedTime.toInstant(TimeZone.UTC).toEpochMilliseconds() | ) | } """.trimMargin(), ) } @Test @Tag(WarningNames.WRONG_OVERLOADING_FUNCTION_ARGUMENTS) fun `functions with override modifiers simple`() { lintMethod( """ |fun foo(a: Int) {} |override fun foo() {} """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_OVERLOADING_FUNCTION_ARGUMENTS) fun `functions with unordered, but same modifiers`() { lintMethod( """ |fun foo(a: Double) {} |fun foo(a: Double, b: Int) {} """.trimMargin(), DiktatError(1, 1, ruleId, "${WRONG_OVERLOADING_FUNCTION_ARGUMENTS.warnText()} foo", false) ) } @Test @Tag(WarningNames.WRONG_OVERLOADING_FUNCTION_ARGUMENTS) fun `functions with unordered, but same modifiers and different names`() { lintMethod( """ |fun foo(a: Double) {} |fun foo(b: Double, b: Int) {} """.trimMargin(), ) } @Test @Tag(WarningNames.WRONG_OVERLOADING_FUNCTION_ARGUMENTS) fun `check for extensions`() { lintMethod( """ private fun isComparisonWithAbs(psiElement: PsiElement) = when (psiElement) { is KtBinaryExpression -> psiElement.isComparisonWithAbs() is KtDotQualifiedExpression -> psiElement.isComparisonWithAbs() else -> false } private fun KtBinaryExpression.isComparisonWithAbs() = takeIf { it.operationToken in comparisonOperators } ?.run { left as? KtCallExpression ?: right as? KtCallExpression } ?.run { calleeExpression as? KtNameReferenceExpression } ?.getReferencedName() ?.equals("abs") ?: false private fun KtBinaryExpression.isComparisonWithAbs(a: Int) = takeIf { it.operationToken in comparisonOperators } ?.run { left as? KtCallExpression ?: right as? KtCallExpression } ?.run { calleeExpression as? KtNameReferenceExpression } ?.getReferencedName() ?.equals("abs") ?: false private fun KtBinaryExpression.isComparisonWithAbs(a: Int): Boolean { return takeIf { it.operationToken in comparisonOperators } ?.run { left as? KtCallExpression ?: right as? KtCallExpression } ?.run { calleeExpression as? KtNameReferenceExpression } ?.getReferencedName() ?.equals("abs") ?: false } """.trimMargin(), DiktatError(8, 21, ruleId, "${WRONG_OVERLOADING_FUNCTION_ARGUMENTS.warnText()} isComparisonWithAbs", false) ) } @Test @Tag(WarningNames.WRONG_OVERLOADING_FUNCTION_ARGUMENTS) fun `check methods with different return types`() { lintMethod( """ private fun KtBinaryExpression.isComparisonWithAbs(a: Int): Boolean { return takeIf { it.operationToken in comparisonOperators } ?.run { left as? KtCallExpression ?: right as? KtCallExpression } ?.run { calleeExpression as? KtNameReferenceExpression } ?.getReferencedName() ?.equals("abs") ?: false } private fun KtBinaryExpression.isComparisonWithAbs(a: Int): Int { val q = takeIf { it.operationToken in comparisonOperators } ?.run { left as? KtCallExpression ?: right as? KtCallExpression } ?.run { calleeExpression as? KtNameReferenceExpression } ?.getReferencedName() ?.equals("abs") ?: false if (q) return 10 return 11 } """.trimMargin() ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter5/ParameterNameInOuterLambdaRuleWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter5 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter5.ParameterNameInOuterLambdaRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class ParameterNameInOuterLambdaRuleWarnTest : LintTestBase(::ParameterNameInOuterLambdaRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${ParameterNameInOuterLambdaRule.NAME_ID}" private val rulesConfigList: List = listOf( RulesConfig(Warnings.PARAMETER_NAME_IN_OUTER_LAMBDA.name, true) ) private val rulesConfigParameterNameInOuterLambda = listOf( RulesConfig( Warnings.PARAMETER_NAME_IN_OUTER_LAMBDA.name, true, mapOf( "strictMode" to "false" )) ) @Test @Tag(WarningNames.PARAMETER_NAME_IN_OUTER_LAMBDA) fun `lambda has specific parameter`() { lintMethod( """ |fun foo(lambda: (s: String) -> Unit) { | lambda("foo") |} | |fun test() { | foo { s -> | println(s) | } |} """.trimMargin(), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.PARAMETER_NAME_IN_OUTER_LAMBDA) fun `lambda has implicit parameter`() { lintMethod( """ |fun foo(lambda: (s: String) -> Unit) { | lambda("foo") |} | |fun test() { | foo { | println(it) | } |} """.trimMargin(), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.PARAMETER_NAME_IN_OUTER_LAMBDA) fun `outer lambda and inner lambda have specific parameter`() { lintMethod( """ |fun bar(lambda: (s: String) -> Unit) { | lambda("bar") |} | |fun foo(lambda: (s: String) -> Unit) { | lambda("foo") |} | |fun test() { | foo { f -> | bar { b -> | println(f + " -> " + b) | } | } |} """.trimMargin(), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.PARAMETER_NAME_IN_OUTER_LAMBDA) fun `outer lambda has specific parameter but inner lambda has implicit parameter`() { lintMethod( """ |fun bar(lambda: (s: String) -> Unit) { | lambda("bar") |} | |fun foo(lambda: (s: String) -> Unit) { | lambda("foo") |} | |fun test() { | foo { f -> | bar { | println(f + " -> " + it) | } | } |} """.trimMargin(), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.PARAMETER_NAME_IN_OUTER_LAMBDA) fun `outer lambda has implicit parameter but inner lambda has specific parameter`() { lintMethod( """ |fun bar(lambda: (s: String) -> Unit) { | lambda("bar") |} | |fun foo(lambda: (s: String) -> Unit) { | lambda("foo") |} | |fun test() { | foo { | bar { b -> | println(it + " -> " + b) | } | } |} """.trimMargin(), DiktatError(10, 8, ruleId, "${Warnings.PARAMETER_NAME_IN_OUTER_LAMBDA.warnText()} lambda without arguments has inner lambda", false), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.PARAMETER_NAME_IN_OUTER_LAMBDA) fun `outer lambda has implicit parameter but inner lambda has no parameter`() { lintMethod( """ |fun bar(lambda: () -> Unit) { | lambda() |} | |fun foo(lambda: (s: String) -> Unit) { | lambda("foo") |} | |fun test() { | foo { | bar { | println(it + " -> bar") | } | } |} """.trimMargin(), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.PARAMETER_NAME_IN_OUTER_LAMBDA) fun `outer lambda has no parameter but inner lambda has implicit parameter`() { lintMethod( """ |fun bar(lambda: (s: String) -> Unit) { | lambda("bar") |} | |fun foo(lambda: () -> Unit) { | lambda() |} | |fun test() { | foo { | bar { | println("foo -> " + it) | } | } |} """.trimMargin(), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.PARAMETER_NAME_IN_OUTER_LAMBDA) fun `shouldn't warn if nested lambda has explicit it`() { lintMethod( """ |fun test() { | run { | l.map { it -> | println(it) | } | } |} """.trimMargin(), rulesConfigList = rulesConfigList ) } @Test @Tag(WarningNames.PARAMETER_NAME_IN_OUTER_LAMBDA) fun `lambdas at the same level`() { lintMethod( """ |fun testA() { | val overrideFunctions: List = emptyList() | overrideFunctions.forEach { functionNameMap.compute(it.getIdentifierName()!!.text) { _, oldValue -> (oldValue ?: 0) + 1 } } |} """.trimMargin(), rulesConfigList = this.rulesConfigParameterNameInOuterLambda ) } @Test @Tag(WarningNames.PARAMETER_NAME_IN_OUTER_LAMBDA) fun `lambdas at the same level 2`() { lintMethod( """ |private fun isCheckNeeded(whiteSpace: PsiWhiteSpace) = | whiteSpace.parent | .node | .elementType | .let { it == VALUE_PARAMETER_LIST || it == VALUE_ARGUMENT_LIST } && | whiteSpace.siblings(forward = false, withItself = false).none { it is PsiWhiteSpace && it.textContains('\n') } && | whiteSpace.siblings(forward = true, withItself = false).any { | it.node.elementType.run { this == VALUE_ARGUMENT || this == VALUE_PARAMETER } | } """.trimMargin(), rulesConfigList = this.rulesConfigParameterNameInOuterLambda ) } @Test @Tag(WarningNames.PARAMETER_NAME_IN_OUTER_LAMBDA) fun `lambdas at the same level 21`() { lintMethod( """ |private fun isCheckNeeded(w: PsiWhiteSpace, h: PsiWhiteSpace) = | w.let { it == VALUE_PARAMETER_LIST || it == VALUE_ARGUMENT_LIST } && | h.none { it is PsiWhiteSpace && it.textContains('\n') } && | h.any { | it.node.elementType.run { this == VALUE_ARGUMENT || this == VALUE_PARAMETER } | } """.trimMargin(), rulesConfigList = this.rulesConfigParameterNameInOuterLambda ) } @Test @Tag(WarningNames.PARAMETER_NAME_IN_OUTER_LAMBDA) fun `lambdas at the same level 3`() { lintMethod( """ |private fun checkBlankLineAfterKdoc(node: ASTNode) { | commentType.forEach { | val kdoc = node.getFirstChildWithType(it) | kdoc?.treeNext?.let { nodeAfterKdoc -> | if (nodeAfterKdoc.elementType == WHITE_SPACE && nodeAfterKdoc.numNewLines() > 1) { | WRONG_NEWLINES_AROUND_KDOC.warnAndFix(configRules, emitWarn, isFixMode, "redundant blank line after ${'$'}{kdoc.text}", nodeAfterKdoc.startOffset, nodeAfterKdoc) { | nodeAfterKdoc.leaveOnlyOneNewLine() | } | } | } | } |} """.trimMargin(), rulesConfigList = this.rulesConfigParameterNameInOuterLambda ) } @Test @Tag(WarningNames.PARAMETER_NAME_IN_OUTER_LAMBDA) fun `lambdas at the same level 4`() { lintMethod( """ |private fun KSAnnotation.getArgumentValue(argumentName: String): String = arguments | .singleOrNull { it.name?.asString() == argumentName } | .let { | requireNotNull(it) { | "Not found ${'$'}argumentName in ${'$'}this" | } | } | .value | ?.let { it as? String } | .let { | requireNotNull(it) { | "Not found a value for ${'$'}argumentName in ${'$'}this" | } | } """.trimMargin(), rulesConfigList = this.rulesConfigParameterNameInOuterLambda ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter6/AbstractClassesFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter6 import com.saveourtool.diktat.ruleset.rules.chapter6.classes.AbstractClassesRule import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames.CLASS_SHOULD_NOT_BE_ABSTRACT import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class AbstractClassesFixTest : FixTestBase("test/chapter6/abstract_classes", ::AbstractClassesRule) { @Test @Tag(CLASS_SHOULD_NOT_BE_ABSTRACT) fun `fix abstract class`() { fixAndCompare("ShouldReplaceAbstractKeywordExpected.kt", "ShouldReplaceAbstractKeywordTest.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter6/AbstractClassesWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter6 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter6.classes.AbstractClassesRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames.CLASS_SHOULD_NOT_BE_ABSTRACT import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class AbstractClassesWarnTest : LintTestBase(::AbstractClassesRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${AbstractClassesRule.NAME_ID}" @Test @Tag(CLASS_SHOULD_NOT_BE_ABSTRACT) fun `should not replace abstract with open`() { lintMethod( """ |abstract class Some(val a: Int = 5) { | abstract fun func() {} | | fun another() {} |} """.trimMargin() ) } @Test @Tag(CLASS_SHOULD_NOT_BE_ABSTRACT) fun `should replace abstract on open`() { lintMethod( """ |abstract class Some(val a: Int = 5) { | fun func() {} |} """.trimMargin(), DiktatError(1, 37, ruleId, "${Warnings.CLASS_SHOULD_NOT_BE_ABSTRACT.warnText()} Some", true) ) } @Test @Tag(CLASS_SHOULD_NOT_BE_ABSTRACT) fun `should replace abstract on open with inner`() { lintMethod( """ |class Some(val a: Int = 5) { | fun func() {} | | inner abstract class Inner { | fun another() | } |} """.trimMargin(), DiktatError(4, 32, ruleId, "${Warnings.CLASS_SHOULD_NOT_BE_ABSTRACT.warnText()} Inner", true) ) } @Test @Tag(CLASS_SHOULD_NOT_BE_ABSTRACT) fun `should replace abstract on open in actual or expect classes`() { lintMethod( """ |actual abstract class CoroutineTest actual constructor() { | /** | * Test rule | */ | @get:Rule | var coroutineTestRule = CoroutineTestRule() | | /** | * Run test | * | * @param T | * @param block | * @receiver a Coroutine Scope | */ | actual fun runTest(block: suspend CoroutineScope.() -> T) { | runBlocking { | block() | } | } |} """.trimMargin(), DiktatError(1, 58, ruleId, "${Warnings.CLASS_SHOULD_NOT_BE_ABSTRACT.warnText()} CoroutineTest", true) ) } @Test @Tag(CLASS_SHOULD_NOT_BE_ABSTRACT) fun `should not remove abstract on class if there are only abstract properties`() { lintMethod( """ |abstract class BaseUsesProcessor() { | // Store uses by file | abstract val a: String | | fun foo() {} |} """.trimMargin() ) } @Test @Tag(CLASS_SHOULD_NOT_BE_ABSTRACT) fun `should not trigger on classes that extend other classes`() { lintMethod( """ |abstract class Example: Base() { | fun foo() {} |} """.trimMargin() ) } @Test @Tag(CLASS_SHOULD_NOT_BE_ABSTRACT) fun `should not trigger on classes that implement interfaces`() { lintMethod( """ |abstract class Example: Base { | fun foo() {} |} """.trimMargin(), ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter6/AvoidUtilityClassWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter6 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings.AVOID_USING_UTILITY_CLASS import com.saveourtool.diktat.ruleset.rules.chapter6.AvoidUtilityClass import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir import java.nio.file.Path class AvoidUtilityClassWarnTest : LintTestBase(::AvoidUtilityClass) { private val ruleId = "$DIKTAT_RULE_SET_ID:${AvoidUtilityClass.NAME_ID}" @Test @Tag(WarningNames.AVOID_USING_UTILITY_CLASS) fun `simple test`() { lintMethod( """ |object StringUtil { | fun stringInfo(myString: String): Int { | return myString.count{ "something".contains(it) } | } |} | |class A() { | fun foo() { } |} | |class StringUtils { | fun goo(tex: String): Int { | return myString.count{ "something".contains(it) } | } |} | |class StringUtil { | val z = "hello" | fun goo(tex: String): Int { | return myString.count{ "something".contains(it) } | } |} """.trimMargin(), DiktatError(1, 1, ruleId, "${AVOID_USING_UTILITY_CLASS.warnText()} StringUtil"), DiktatError(11, 1, ruleId, "${AVOID_USING_UTILITY_CLASS.warnText()} StringUtils") ) } @Test @Tag(WarningNames.AVOID_USING_UTILITY_CLASS) fun `test with comment anf companion`() { lintMethod( """ | |class StringUtils { | companion object { | private val name = "Hello" | } | /** | * @param tex | */ | fun goo(tex: String): Int { | //hehe | return myString.count{ "something".contains(it) } | } |} | |class StringUtil { | /* | | */ | companion object { | } | val z = "hello" | fun goo(tex: String): Int { | return myString.count{ "something".contains(it) } | } |} """.trimMargin(), DiktatError(2, 1, ruleId, "${AVOID_USING_UTILITY_CLASS.warnText()} StringUtils") ) } @Test @Tag(WarningNames.AVOID_USING_UTILITY_CLASS) fun `test with class without identifier`() { lintMethod( """ fun foo() { window.addMouseListener(object : MouseAdapter() { override fun mouseClicked(e: MouseEvent) { /*...*/ } override fun mouseEntered(e: MouseEvent) { /*...*/ } }) } """.trimMargin() ) } @Test @Tag(WarningNames.AVOID_USING_UTILITY_CLASS) fun `check test-class`(@TempDir tempDir: Path) { lintMethodWithFile( """ |class StringUtils { | fun goo(tex: String): Int { | return myString.count{ "something".contains(it) } | } |} """.trimMargin(), tempDir = tempDir, fileName = "src/main/kotlin/com/saveourtool/diktat/Example.kt", DiktatError(1, 1, ruleId, "${AVOID_USING_UTILITY_CLASS.warnText()} StringUtils"), ) lintMethodWithFile( """ |@Test |class StringUtils1 { | fun goo(tex: String): Int { | return myString.count{ "something".contains(it) } | } |} """.trimMargin(), tempDir = tempDir, fileName = "src/test/kotlin/com/saveourtool/diktat/Example.kt" ) lintMethodWithFile( """ |class StringUtils2 { | fun goo(tex: String): Int { | return myString.count{ "something".contains(it) } | } |} """.trimMargin(), tempDir = tempDir, fileName = "src/test/kotlin/com/saveourtool/diktat/UtilTest.kt" ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter6/CompactInitializationFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter6 import com.saveourtool.diktat.ruleset.rules.chapter6.classes.CompactInitialization import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class CompactInitializationFixTest : FixTestBase("test/chapter6/compact_initialization", ::CompactInitialization) { @Test @Tag(WarningNames.COMPACT_OBJECT_INITIALIZATION) fun `should wrap properties into apply`() { fixAndCompare("SimpleExampleExpected.kt", "SimpleExampleTest.kt") } @Test @Tag(WarningNames.COMPACT_OBJECT_INITIALIZATION) fun `should wrap properties into apply also moving comments`() { fixAndCompare("ExampleWithCommentsExpected.kt", "ExampleWithCommentsTest.kt") } @Test @Tag(WarningNames.COMPACT_OBJECT_INITIALIZATION) fun `should wrap properties into apply - existing apply with value argument`() { fixAndCompare("ApplyWithValueArgumentExpected.kt", "ApplyWithValueArgumentTest.kt") } @Test @Tag(WarningNames.COMPACT_OBJECT_INITIALIZATION) fun `should not move statements with this keyword into apply block`() { fixAndCompare("ApplyOnStatementsWithThisKeywordExpected.kt", "ApplyOnStatementsWithThisKeywordTest.kt") } @Test @Tag(WarningNames.COMPACT_OBJECT_INITIALIZATION) fun `should rename field in apply block to this keyword`() { fixAndCompare("StatementUseFieldMultipleTimesExpected.kt", "StatementUseFieldMultipleTimesTest.kt") } @Test @Tag(WarningNames.COMPACT_OBJECT_INITIALIZATION) fun `should wrap receiver in parentheses if required`() { fixAndCompare("ParenthesizedReceiverExpected.kt", "ParenthesizedReceiverTest.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter6/CompactInitializationWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter6 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings.COMPACT_OBJECT_INITIALIZATION import com.saveourtool.diktat.ruleset.rules.chapter6.classes.CompactInitialization import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class CompactInitializationWarnTest : LintTestBase(::CompactInitialization) { private val ruleId = "$DIKTAT_RULE_SET_ID:${CompactInitialization.NAME_ID}" @Test @Tag(WarningNames.COMPACT_OBJECT_INITIALIZATION) fun `compact class instantiation - positive example`() { lintMethod( """ |fun main() { | val httpClient = HttpClient("myConnection") | .apply { | url = "http://example.com" | port = "8080" | timeout = 100 | } | httpClient.doRequest() |} """.trimMargin() ) } @Test @Tag(WarningNames.COMPACT_OBJECT_INITIALIZATION) fun `should suggest compact class instantiation`() { lintMethod( """ |fun main() { | val httpClient = HttpClient("myConnection") | httpClient.url = "http://example.com" | httpClient.port = "8080" | httpClient.timeout = 100 | httpClient.doRequest() |} """.trimMargin(), DiktatError(3, 5, ruleId, "${COMPACT_OBJECT_INITIALIZATION.warnText()} url", true), DiktatError(4, 5, ruleId, "${COMPACT_OBJECT_INITIALIZATION.warnText()} port", true), DiktatError(5, 5, ruleId, "${COMPACT_OBJECT_INITIALIZATION.warnText()} timeout", true), ) } @Test @Tag(WarningNames.COMPACT_OBJECT_INITIALIZATION) fun `should suggest compact class instantiation - with comments`() { lintMethod( """ |fun main() { | val httpClient = HttpClient("myConnection") | // setting url for http requests | httpClient.url = "http://example.com" | // setting port | httpClient.port = "8080" | | // setting timeout to 100 | httpClient.timeout = 100 | httpClient.doRequest() |} """.trimMargin(), DiktatError(4, 5, ruleId, "${COMPACT_OBJECT_INITIALIZATION.warnText()} url", true), DiktatError(6, 5, ruleId, "${COMPACT_OBJECT_INITIALIZATION.warnText()} port", true), DiktatError(9, 5, ruleId, "${COMPACT_OBJECT_INITIALIZATION.warnText()} timeout", true), ) } @Test @Tag(WarningNames.COMPACT_OBJECT_INITIALIZATION) fun `class instantiation partially in apply`() { lintMethod( """ |fun main() { | val httpClient = HttpClient("myConnection") | .apply { | url = "http://example.com" | port = "8080" | } | httpClient.timeout = 100 | httpClient.doRequest() |} """.trimMargin(), DiktatError(7, 5, ruleId, "${COMPACT_OBJECT_INITIALIZATION.warnText()} timeout", true) ) } // Creating the `apply` block here breaks the compilation @Test @Tag(WarningNames.COMPACT_OBJECT_INITIALIZATION) fun `should not trigger to infix function 1`() { lintMethod( """ |fun foo(line: String) = line | .split(",", ", ") | .associate { | val pair = it.split("=", limit = 2).map { | it.replace("\\=", "=") | } | pair.first() to pair.last() | } """.trimMargin(), ) } // Apply block doesn't break the compilation, however such changes can break the user logic @Test @Tag(WarningNames.COMPACT_OBJECT_INITIALIZATION) fun `should not trigger to infix function 2`() { lintMethod( """ |fun foo(line: String) { | val pair = line.split("=", limit = 2).map { | it.replace("\\=", "=") | } | pair.first() to pair.last() |} """.trimMargin(), ) } // For generality don't trigger on any infix function, despite the fact, that with apply block all will be okay @Test @Tag(WarningNames.COMPACT_OBJECT_INITIALIZATION) fun `should not trigger to infix function 3`() { lintMethod( """ |fun `translate text`() { | val res = translateText(text = "dummy") | (res is TranslationsSuccess) shouldBe true | val translationsSuccess = res as TranslationsSuccess | translationsSuccess.translations shouldHaveSize 1 |} """.trimMargin(), ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter6/CustomGetterSetterWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter6 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter6.CustomGetterSetterRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames.CUSTOM_GETTERS_SETTERS import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class CustomGetterSetterWarnTest : LintTestBase(::CustomGetterSetterRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${CustomGetterSetterRule.NAME_ID}" @Test @Tag(CUSTOM_GETTERS_SETTERS) fun `no custom getters and setters allowed`() { lintMethod( """ |class A { | var size: Int = 0 | set(value) { | println("Side effect") | field = value | } | get() = this.hashCode() * 2 |} """.trimMargin(), DiktatError(3, 9, ruleId, "${Warnings.CUSTOM_GETTERS_SETTERS.warnText()} set"), DiktatError(7, 9, ruleId, "${Warnings.CUSTOM_GETTERS_SETTERS.warnText()} get"), ) } @Test @Tag(CUSTOM_GETTERS_SETTERS) fun `no custom getters allowed`() { lintMethod( """ |class A { | | fun set(value) { | println("Side effect") | } | | fun get() = 47 |} """.trimMargin(), ) } @Test @Tag(CUSTOM_GETTERS_SETTERS) fun `override getter`() { lintMethod( """ |interface A { | val a: Int |} | |object B: A { | override val a: int | get() = 0 |} """.trimMargin(), ) } @Test @Tag(CUSTOM_GETTERS_SETTERS) fun `exception case with private setter`() { lintMethod( """ |class A { | var size: Int = 0 | private set(value) { | println("Side effect") | field = value | } | get() = this.hashCode() * 2 |} """.trimMargin(), DiktatError(7, 9, ruleId, "${Warnings.CUSTOM_GETTERS_SETTERS.warnText()} get"), ) } @Test @Tag(CUSTOM_GETTERS_SETTERS) fun `exception case with protected setter`() { lintMethod( """ |class A { | var size: Int = 0 | protected set(value) { | println("Side effect") | field = value | } | get() = this.hashCode() * 2 |} """.trimMargin(), DiktatError(3, 19, ruleId, "${Warnings.CUSTOM_GETTERS_SETTERS.warnText()} set"), DiktatError(7, 9, ruleId, "${Warnings.CUSTOM_GETTERS_SETTERS.warnText()} get"), ) } @Test @Tag(CUSTOM_GETTERS_SETTERS) fun `should not trigger on property with backing field`() { lintMethod( """ |package com.example | |class MutableTableContainer { | private var _table: Map? = null | | val table: Map | get() { | if (_table == null) { | _table = hashMapOf() | } | return _table ?: throw AssertionError("Set to null by another thread") | } | set(value) { | field = value | } | |} """.trimMargin(), ) } @Test @Tag(CUSTOM_GETTERS_SETTERS) fun `should trigger on backing field with setter`() { val code = """ |package com.example | |class MutableTableContainer { | var _table: Map? = null | set(value) { | field = value | } | | val table: Map | get() { | if (_table == null) { | _table = hashMapOf() | } | return _table ?: throw AssertionError("Set to null by another thread") | } | set(value) { | field = value | } | |} """.trimMargin() lintMethod(code, DiktatError(5, 17, ruleId, "${Warnings.CUSTOM_GETTERS_SETTERS.warnText()} set", false), DiktatError(10, 8, ruleId, "${Warnings.CUSTOM_GETTERS_SETTERS.warnText()} get", false), DiktatError(16, 8, ruleId, "${Warnings.CUSTOM_GETTERS_SETTERS.warnText()} set", false) ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter6/DataClassesRuleWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter6 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter6.classes.DataClassesRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames.USE_DATA_CLASS import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class DataClassesRuleWarnTest : LintTestBase(::DataClassesRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${DataClassesRule.NAME_ID}" @Test @Tag(USE_DATA_CLASS) fun `trigger on default class`() { lintMethod( """ |class Some(val a: Int = 5) { | |} """.trimMargin(), DiktatError(1, 1, ruleId, "${Warnings.USE_DATA_CLASS.warnText()} Some") ) } @Test @Tag(USE_DATA_CLASS) fun `_regression_ trigger on default class without a body`() { lintMethod( """ |class Some(val a: Int = 5) | """.trimMargin(), DiktatError(1, 1, ruleId, "${Warnings.USE_DATA_CLASS.warnText()} Some") ) } @Test @Tag(USE_DATA_CLASS) fun `should trigger - dont forget to consider this class in fix`() { lintMethod( """ |class Test { | var a: Int = 0 | get() = field | set(value: Int) { field = value} |} """.trimMargin(), DiktatError(1, 1, ruleId, "${Warnings.USE_DATA_CLASS.warnText()} Test") ) } @Test @Tag(USE_DATA_CLASS) fun `should not trigger if there is some logic in accessor`() { lintMethod( """ |class Test { | var a: Int = 0 | get() = field | set(value: Int) { | field = value | someFun(value) | } |} """.trimMargin() ) } @Test @Tag(USE_DATA_CLASS) fun `should not trigger on class with bad modifiers`() { lintMethod( """ |data class Some(val a: Int = 5) { | |} | |abstract class Another() {} | |open class Open(){} | |sealed class Clazz{} | |data class CheckInner { | inner class Inner {} |} | |enum class Num { | |} """.trimMargin() ) } @Test @Tag(USE_DATA_CLASS) fun `should not trigger on classes with functions`() { lintMethod( """ |class Some { | val prop = 5 | private fun someFunc() {} |} """.trimMargin() ) } @Test @Tag(USE_DATA_CLASS) fun `should not trigger on classes with no property in constructor`() { lintMethod( """ |class B(map: Map) {} | |class Ab(val map: Map, map: Map) {} | |class A(val map: Map) {} | """.trimMargin(), DiktatError(5, 1, ruleId, "${Warnings.USE_DATA_CLASS.warnText()} A") ) } @Test @Tag(USE_DATA_CLASS) fun `should not trigger on empty class`() { lintMethod( """ |class B() {} | |class Ab{} | """.trimMargin() ) } @Test @Tag(USE_DATA_CLASS) fun `should trigger on class without constructor but with property`() { lintMethod( """ |class B() { | val q = 10 |} | |class Ab { | val qw = 10 |} | |class Ba { | val q = 10 | fun foo() = 10 |} """.trimMargin(), DiktatError(1, 1, ruleId, "${Warnings.USE_DATA_CLASS.warnText()} B"), DiktatError(5, 1, ruleId, "${Warnings.USE_DATA_CLASS.warnText()} Ab") ) } @Test @Tag(USE_DATA_CLASS) fun `shouldn't trigger on class with init block`() { lintMethod( """ |class Credentials(auth: String) { | val gitHubUserName: String | val gitHubAuthToken: String | | init { | auth.let { | | } | } |} """.trimMargin() ) } @Test @Tag(USE_DATA_CLASS) fun `should not trigger on init block with if`() { lintMethod( """ |class Credentials(auth: String, second: Int?, third: Double) { | val gitHubUserName: String | val gitHubAuthToken: String | | init { | if (second != null) { | } | } |} """.trimMargin() ) } @Test @Tag(USE_DATA_CLASS) fun `should not trigger on init block with function call`() { lintMethod( """ |class Credentials(auth: String, second: Int?, third: Double) { | val gitHubUserName: String | val gitHubAuthToken: String | | init { | foo(third) | } |} """.trimMargin() ) } @Test @Tag(USE_DATA_CLASS) fun `should not trigger on class with several parameters`() { lintMethod( """ |class Credentials(auth: String, second: Int?, third: Double) { | val gitHubUserName: String | val gitHubAuthToken: String | | init { | auth.let { | | } | | if (second != null) { | } | | foo(third) | } |} """.trimMargin() ) } @Test @Tag(USE_DATA_CLASS) fun `should not trigger on enums`() { lintMethod( """ |enum class Style(val str: String) { | PASCAL_CASE("PascalCase"), | SNAKE_CASE("UPPER_SNAKE_CASE"), | ; |} """.trimMargin() ) } @Test @Tag(USE_DATA_CLASS) fun `should trigger on class with parameter in constructor`() { lintMethod( """ |class Credentials(auth: String) { | val gitHubUserName: String = auth.toUpperCase() | val gitHubAuthToken: String = auth.toLowerCase() |} """.trimMargin(), DiktatError(1, 1, ruleId, "${Warnings.USE_DATA_CLASS.warnText()} Credentials") ) } @Test @Tag(USE_DATA_CLASS) fun `should trigger on class with parameter in constructor and init block`() { lintMethod( """ |class Credentials(auth: String) { | val gitHubUserName: String = auth.toUpperCase() | val gitHubAuthToken: String = auth.toLowerCase() | | init { | // some logic | } |} """.trimMargin(), DiktatError(1, 1, ruleId, "${Warnings.USE_DATA_CLASS.warnText()} Credentials") ) } @Test @Tag(USE_DATA_CLASS) fun `should not trigger on init block with one ref expression`() { lintMethod( """ |class Credentials(auth: String, some: Int?) { | val gitHubUserName: String = auth.toUpperCase() | val gitHubAuthToken: String = auth.toLowerCase() | | init { | val a = auth | } |} """.trimMargin() ) } @Test @Tag(USE_DATA_CLASS) fun `annotation classes bug`() { lintMethod( """ |@Retention(AnnotationRetention.SOURCE) |@Target(AnnotationTarget.CLASS) |annotation class NavGraphDestination( | val name: String = Defaults.NULL, | val routePrefix: String = Defaults.NULL, | val deepLink: Boolean = false, |) { | object Defaults { | const val NULL = "@null" | } |} """.trimMargin() ) } @Test @Tag(USE_DATA_CLASS) fun `value or inline classes bug`() { lintMethod( """ |@JvmInline |value class Password(private val s: String) |val securePassword = Password("Don't try this in production") """.trimMargin() ) } @Test @Tag(USE_DATA_CLASS) fun `sealed classes bug`() { lintMethod( """ |sealed class Password(private val s: String) """.trimMargin() ) } @Test @Tag(USE_DATA_CLASS) fun `inner classes bug`() { lintMethod( """ |inner class Password(private val s: String) """.trimMargin() ) } @Test @Tag(USE_DATA_CLASS) fun `shouldn't trigger on interface`() { lintMethod( """ |interface Credentials { | val code: String | val success: Boolean | val message: String |} """.trimMargin() ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter6/EmptyPrimaryConstructorFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter6 import com.saveourtool.diktat.ruleset.rules.chapter6.AvoidEmptyPrimaryConstructor import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class EmptyPrimaryConstructorFixTest : FixTestBase("test/chapter6/primary_constructor", ::AvoidEmptyPrimaryConstructor) { @Test @Tag(WarningNames.EMPTY_PRIMARY_CONSTRUCTOR) fun `should remove empty primary constructor`() { fixAndCompare("EmptyPCExpected.kt", "EmptyPCTest.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter6/EmptyPrimaryConstructorWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter6 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings.EMPTY_PRIMARY_CONSTRUCTOR import com.saveourtool.diktat.ruleset.rules.chapter6.AvoidEmptyPrimaryConstructor import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class EmptyPrimaryConstructorWarnTest : LintTestBase(::AvoidEmptyPrimaryConstructor) { private val ruleId = "$DIKTAT_RULE_SET_ID:${AvoidEmptyPrimaryConstructor.NAME_ID}" @Test @Tag(WarningNames.EMPTY_PRIMARY_CONSTRUCTOR) fun `simple classes with empty primary constructor`() { lintMethod( """ |class Some() { | val a = 10 | constructor(a: String): this() { | this.a = a | } |} | |class Some1() { | val a = 10 | companion object {} |} | |class Some2 { | val a = 10 | constructor(a: String): this() { | this.a = a | } |} | |class Some3 private constructor () { | |} """.trimMargin(), DiktatError(1, 1, ruleId, "${EMPTY_PRIMARY_CONSTRUCTOR.warnText()} Some", true), DiktatError(8, 1, ruleId, "${EMPTY_PRIMARY_CONSTRUCTOR.warnText()} Some1", true) ) } @Test @Tag(WarningNames.EMPTY_PRIMARY_CONSTRUCTOR) fun `correct example with empty primary constructor and modifiers`() { lintMethod( """ |class Some1 private constructor () { | |} | |class Some2 @Inject constructor() { |} """.trimMargin() ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter6/ExtensionFunctionsInFileWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter6 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter6.ExtensionFunctionsInFileRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames.EXTENSION_FUNCTION_WITH_CLASS import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class ExtensionFunctionsInFileWarnTest : LintTestBase(::ExtensionFunctionsInFileRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${ExtensionFunctionsInFileRule.NAME_ID}" @Test @Tag(EXTENSION_FUNCTION_WITH_CLASS) fun `should warn on function`() { lintMethod( """ |class Some private constructor () { | |} | |private fun Some.coolStr() { | |} """.trimMargin(), DiktatError(5, 1, ruleId, "${Warnings.EXTENSION_FUNCTION_WITH_CLASS.warnText()} fun coolStr") ) } @Test @Tag(EXTENSION_FUNCTION_WITH_CLASS) fun `should warn on several functions`() { lintMethod( """ |class Another private constructor () { | |} | |private fun /* Random comment */ Another.coolStr() { | |} | |private fun Another.extMethod() { | |} """.trimMargin(), DiktatError(5, 1, ruleId, "${Warnings.EXTENSION_FUNCTION_WITH_CLASS.warnText()} fun coolStr"), DiktatError(9, 1, ruleId, "${Warnings.EXTENSION_FUNCTION_WITH_CLASS.warnText()} fun extMethod"), ) } @Test @Tag(EXTENSION_FUNCTION_WITH_CLASS) fun `should not raise a warning when there is no class`() { lintMethod( """ |private fun String.coolStr() { | |} | |private fun /* Random comment */ Another.extMethod() { | |} """.trimMargin() ) } @Test @Tag(EXTENSION_FUNCTION_WITH_CLASS) fun `should not raise a warning when there is no extension functions`() { lintMethod( """ |class Some { | |} """.trimMargin() ) } @Test @Tag(EXTENSION_FUNCTION_WITH_CLASS) fun `should not raise a warning when extension function is in the class`() { lintMethod( """ |class Some { | | fun Some.str() { | | } |} """.trimMargin() ) } @Test @Tag(EXTENSION_FUNCTION_WITH_CLASS) fun `should not trigger on regular functions in the same file with class`() { lintMethod( """ |class Some { | fun foo() { | | } |} | |fun bar() { | |} """.trimMargin() ) } @Test @Tag(EXTENSION_FUNCTION_WITH_CLASS) fun `should not trigger on extension functions with different class`() { lintMethod( """ |class Some { | fun foo() { | | } |} | |fun String.bar() { | |} """.trimMargin() ) } /** * See [#1586](https://github.com/saveourtool/diktat/issues/1586). * * @since 1.2.5 */ @Test @Tag(EXTENSION_FUNCTION_WITH_CLASS) fun `should raise no warnings for external interfaces`() { lintMethod( """ |internal external interface External { | val a: Int |} | |fun External.extension() = println(a) """.trimMargin() ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter6/ExtensionFunctionsSameNameWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter6 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter6.ExtensionFunctionsSameNameRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames.EXTENSION_FUNCTION_SAME_SIGNATURE import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class ExtensionFunctionsSameNameWarnTest : LintTestBase(::ExtensionFunctionsSameNameRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${ExtensionFunctionsSameNameRule.NAME_ID}" @Test @Tag(EXTENSION_FUNCTION_SAME_SIGNATURE) fun `should trigger on functions with same signatures`() { lintMethod( """ |open class A |class B: A(), C |class D: A() | |fun A.foo() = "A" |fun B.foo() = "B" |fun D.foo() = "D" | |fun printClassName(s: A) { print(s.foo()) } | |fun main() { printClassName(B()) } """.trimMargin(), DiktatError(5, 1, ruleId, "${Warnings.EXTENSION_FUNCTION_SAME_SIGNATURE.warnText()} fun A.foo[] and fun B.foo[]"), DiktatError(5, 1, ruleId, "${Warnings.EXTENSION_FUNCTION_SAME_SIGNATURE.warnText()} fun A.foo[] and fun D.foo[]"), DiktatError(6, 1, ruleId, "${Warnings.EXTENSION_FUNCTION_SAME_SIGNATURE.warnText()} fun A.foo[] and fun B.foo[]"), DiktatError(7, 1, ruleId, "${Warnings.EXTENSION_FUNCTION_SAME_SIGNATURE.warnText()} fun A.foo[] and fun D.foo[]"), ) } @Test @Tag(EXTENSION_FUNCTION_SAME_SIGNATURE) fun `should trigger on functions with same signatures 2`() { lintMethod( """ |open class A |class B: A(), C | |fun A.foo(some: Int) = "A" |fun B.foo(some: Int) = "B" | |fun printClassName(s: A) { print(s.foo()) } | |fun main() { printClassName(B()) } """.trimMargin(), DiktatError(4, 1, ruleId, "${Warnings.EXTENSION_FUNCTION_SAME_SIGNATURE.warnText()} fun A.foo[some] and fun B.foo[some]"), DiktatError(5, 1, ruleId, "${Warnings.EXTENSION_FUNCTION_SAME_SIGNATURE.warnText()} fun A.foo[some] and fun B.foo[some]") ) } @Test @Tag(EXTENSION_FUNCTION_SAME_SIGNATURE) fun `should not trigger on functions with different signatures`() { lintMethod( """ |open class A |class B: A(), C | |fun A.foo(): Boolean = return true |fun B.foo() = "B" | |fun printClassName(s: A) { print(s.foo()) } | |fun main() { printClassName(B()) } """.trimMargin() ) } @Test @Tag(EXTENSION_FUNCTION_SAME_SIGNATURE) fun `should not trigger on functions with different signatures 2`() { lintMethod( """ |open class A |class B: A(), C | |fun A.foo() = return true |fun B.foo(some: Int) = "B" | |fun printClassName(s: A) { print(s.foo()) } | |fun main() { printClassName(B()) } """.trimMargin() ) } @Test @Tag(EXTENSION_FUNCTION_SAME_SIGNATURE) fun `should not trigger on functions with unrelated classes`() { lintMethod( """ |interface A |class B: A |class C | |fun C.foo() = "C" |fun B.foo() = "B" | |fun printClassName(s: A) { print(s.foo()) } | |fun main() { printClassName(B()) } """.trimMargin() ) } @Test @Tag(EXTENSION_FUNCTION_SAME_SIGNATURE) fun `should not trigger on regular func`() { lintMethod( """ |interface A |class B: A |class C | |fun foo() = "C" |fun bar() = "B" | |fun printClassName(s: A) { print(s.foo()) } | |fun main() { printClassName(B()) } """.trimMargin() ) } @Test @Disabled @Tag(EXTENSION_FUNCTION_SAME_SIGNATURE) fun `should trigger on classes in other files`() { lintMethod( """ |fun A.foo() = "A" |fun B.foo() = "B" | |fun printClassName(s: A) { print(s.foo()) } | |fun main() { printClassName(B()) } """.trimMargin(), DiktatError(1, 1, ruleId, "${Warnings.EXTENSION_FUNCTION_SAME_SIGNATURE.warnText()} fun A.foo() and fun B.foo()"), DiktatError(2, 1, ruleId, "${Warnings.EXTENSION_FUNCTION_SAME_SIGNATURE.warnText()} fun A.foo() and fun B.foo()") ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter6/ImplicitBackingPropertyWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter6 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter6.ImplicitBackingPropertyRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames.NO_CORRESPONDING_PROPERTY import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class ImplicitBackingPropertyWarnTest : LintTestBase(::ImplicitBackingPropertyRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${ImplicitBackingPropertyRule.NAME_ID}" @Test @Tag(NO_CORRESPONDING_PROPERTY) fun `not trigger on backing property`() { lintMethod( """ |class Some(val a: Int = 5) { | private var _table: Map? = null | val table:Map | get() { | if (_table == null) { | _table = HashMap() | } | return _table ?: throw AssertionError("Set to null by another thread") | } | set(value) { field = value } |} """.trimMargin() ) } @Test @Tag(NO_CORRESPONDING_PROPERTY) fun `trigger on backing property`() { lintMethod( """ |class Some(val a: Int = 5) { | private var a: Map? = null | val table:Map | get() { | if (a == null) { | a = HashMap() | } | return a ?: throw AssertionError("Set to null by another thread") | } | set(value) { field = value } |} """.trimMargin(), DiktatError(3, 4, ruleId, "${Warnings.NO_CORRESPONDING_PROPERTY.warnText()} table has no corresponding property with name _table") ) } @Test @Tag(NO_CORRESPONDING_PROPERTY) fun `don't trigger on regular backing property`() { lintMethod( """ |class Some(val a: Int = 5) { | private var _a: Map? = null | private val _some:Int? = null |} """.trimMargin() ) } @Test @Tag(NO_CORRESPONDING_PROPERTY) fun `don't trigger on regular property`() { lintMethod( """ |class Some(val a: Int = 5) { | private var a: Map? = null | private val some:Int? = null | private val _prop: String? = null |} """.trimMargin() ) } @Test @Tag(NO_CORRESPONDING_PROPERTY) fun `should not trigger if property has field in accessor`() { lintMethod( """ |class Some(val a: Int = 5) { | val table:Map | set(value) { field = value } | val _table: Map? = null | | val some: Int | get() = 3 |} """.trimMargin() ) } @Test @Tag(NO_CORRESPONDING_PROPERTY) fun `should not trigger on property with constant return`() { lintMethod( """ |class Some(val a: Int = 5) { | val table:Int | get() { | return 3 | } | set(value) { field = value } |} """.trimMargin() ) } @Test @Tag(NO_CORRESPONDING_PROPERTY) fun `should not trigger on property with chain call return`() { lintMethod( """ |class Some(val a: Int = 5) { | val table:Int | get() { | val some = listOf(1,2,3) | return some.filter { it -> it == 3}.first() | } | set(value) { field = value } |} """.trimMargin() ) } @Test @Tag(NO_CORRESPONDING_PROPERTY) fun `should not trigger set accessor`() { lintMethod( """ |class Some(val a: Int = 5) { | val foo | set(value) { | if(isDelegate) log.debug(value) | field = value | } |} """.trimMargin() ) } @Test @Tag(NO_CORRESPONDING_PROPERTY) fun `should trigger set accessor`() { lintMethod( """ |class Some(val a: Int = 5) { | val foo | set(value) { | if(isDelegate) log.debug(value) | a = value | } |} """.trimMargin(), DiktatError(2, 4, ruleId, "${Warnings.NO_CORRESPONDING_PROPERTY.warnText()} foo has no corresponding property with name _foo") ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter6/InlineClassesWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter6 import com.saveourtool.diktat.common.config.rules.DIKTAT_COMMON import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.INLINE_CLASS_CAN_BE_USED import com.saveourtool.diktat.ruleset.rules.chapter6.classes.InlineClassesRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class InlineClassesWarnTest : LintTestBase(::InlineClassesRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${InlineClassesRule.NAME_ID}" private val rulesConfigListEarlierVersion: List = listOf( RulesConfig( DIKTAT_COMMON, true, mapOf("kotlinVersion" to "1.2.9")) ) private val rulesConfigListSameVersion: List = listOf( RulesConfig( DIKTAT_COMMON, true, mapOf("kotlinVersion" to "1.3")) ) private val rulesConfigListLateVersion: List = listOf( RulesConfig( DIKTAT_COMMON, true, mapOf("kotlinVersion" to "1.4.30")) ) private val rulesConfigListUnsupportedVersion: List = listOf( RulesConfig( DIKTAT_COMMON, true, mapOf("kotlinVersion" to "1.5.20")) ) @Test @Tag(WarningNames.INLINE_CLASS_CAN_BE_USED) fun `should not trigger on inline class`() { lintMethod( """ |inline class Name(val s: String) {} """.trimMargin(), rulesConfigList = rulesConfigListSameVersion ) } @Test @Tag(WarningNames.INLINE_CLASS_CAN_BE_USED) fun `should trigger on regular class`() { lintMethod( """ |class Some { | val config = Config() |} """.trimMargin(), DiktatError(1, 1, ruleId, "${INLINE_CLASS_CAN_BE_USED.warnText()} class Some", false), rulesConfigList = rulesConfigListSameVersion ) } @Test @Tag(WarningNames.INLINE_CLASS_CAN_BE_USED) fun `should trigger on class with appropriate modifiers`() { lintMethod( """ |final class Some { | val config = Config() |} """.trimMargin(), DiktatError(1, 1, ruleId, "${INLINE_CLASS_CAN_BE_USED.warnText()} class Some", false), rulesConfigList = rulesConfigListSameVersion ) } @Test @Tag(WarningNames.INLINE_CLASS_CAN_BE_USED) fun `should not trigger on class with inappropriate modifiers`() { lintMethod( """ |abstract class Some { | val config = Config() |} """.trimMargin(), rulesConfigList = rulesConfigListSameVersion ) } @Test @Tag(WarningNames.INLINE_CLASS_CAN_BE_USED) fun `should not trigger on interface`() { lintMethod( """ |interface Some { | val config = Config() |} """.trimMargin(), rulesConfigList = rulesConfigListSameVersion ) } @Test @Tag(WarningNames.INLINE_CLASS_CAN_BE_USED) fun `should trigger on class with val prop in constructor`() { lintMethod( """ |class Some(val anything: Int) { | |} """.trimMargin(), DiktatError(1, 1, ruleId, "${INLINE_CLASS_CAN_BE_USED.warnText()} class Some", false), rulesConfigList = rulesConfigListSameVersion ) } @Test @Tag(WarningNames.INLINE_CLASS_CAN_BE_USED) fun `should not trigger on class with var prop #1`() { lintMethod( """ |class Some(var anything: Int) { | |} """.trimMargin(), rulesConfigList = rulesConfigListSameVersion ) } @Test @Tag(WarningNames.INLINE_CLASS_CAN_BE_USED) fun `should not trigger on class with var prop #2`() { lintMethod( """ |class Some { | var some = 3 |} """.trimMargin(), rulesConfigList = rulesConfigListSameVersion ) } @Test @Tag(WarningNames.INLINE_CLASS_CAN_BE_USED) fun `should not trigger on class that extends class`() { lintMethod( """ |class Some : Any() { | val some = 3 |} """.trimMargin(), rulesConfigList = rulesConfigListSameVersion ) } @Test @Tag(WarningNames.INLINE_CLASS_CAN_BE_USED) fun `should trigger on class that extends interface`() { lintMethod( """ |class Some : Any { | val some = 3 |} """.trimMargin(), DiktatError(1, 1, ruleId, "${INLINE_CLASS_CAN_BE_USED.warnText()} class Some", false), rulesConfigList = rulesConfigListSameVersion ) } @Test @Tag(WarningNames.INLINE_CLASS_CAN_BE_USED) fun `should not trigger on class with internal constructor`() { lintMethod( """ |class LocalCommandExecutor internal constructor(private val command: String) { | |} """.trimMargin(), rulesConfigList = rulesConfigListSameVersion ) } @Test @Tag(WarningNames.INLINE_CLASS_CAN_BE_USED) fun `should trigger on class with public constructor`() { lintMethod( """ |class LocalCommandExecutor public constructor(private val command: String) { | |} """.trimMargin(), DiktatError(1, 1, ruleId, "${INLINE_CLASS_CAN_BE_USED.warnText()} class LocalCommandExecutor", false), rulesConfigList = rulesConfigListSameVersion ) } @Test @Tag(WarningNames.INLINE_CLASS_CAN_BE_USED) fun `should trigger on class with annotation before the constructor`() { lintMethod( """ |class LocalCommandExecutor @Inject constructor(private val command: String) { | |} """.trimMargin(), DiktatError(1, 1, ruleId, "${INLINE_CLASS_CAN_BE_USED.warnText()} class LocalCommandExecutor", false), rulesConfigList = rulesConfigListSameVersion ) } @Test @Tag(WarningNames.INLINE_CLASS_CAN_BE_USED) @Suppress("TOO_LONG_FUNCTION") fun `check kotlin version`() { lintMethod( """ |class Some { | val config = Config() |} """.trimMargin(), DiktatError(1, 1, ruleId, "${INLINE_CLASS_CAN_BE_USED.warnText()} class Some", false), rulesConfigList = rulesConfigListLateVersion ) lintMethod( """ |class Some { | val config = Config() |} """.trimMargin(), rulesConfigList = rulesConfigListEarlierVersion ) lintMethod( """ |class Some { | val config = Config() |} """.trimMargin(), DiktatError(1, 1, ruleId, "${INLINE_CLASS_CAN_BE_USED.warnText()} class Some", false), rulesConfigList = rulesConfigListSameVersion ) lintMethod( """ |class Some { | val config = Config() |} """.trimMargin(), rulesConfigList = rulesConfigListUnsupportedVersion ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter6/PropertyAccessorFieldsWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter6 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings.WRONG_NAME_OF_VARIABLE_INSIDE_ACCESSOR import com.saveourtool.diktat.ruleset.rules.chapter6.PropertyAccessorFields import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class PropertyAccessorFieldsWarnTest : LintTestBase(::PropertyAccessorFields) { private val ruleId = "$DIKTAT_RULE_SET_ID:${PropertyAccessorFields.NAME_ID}" @Test @Tag(WarningNames.WRONG_NAME_OF_VARIABLE_INSIDE_ACCESSOR) fun `check simple correct examples`() { lintMethod( """ |class A { | | var isEmpty: Boolean = false | set(value) { | println("Side effect") | field = value | } | get() = field | | var isNotEmpty: Boolean = true | set(value) { | val q = isEmpty.and(true) | field = value | } | get() { | println(12345) | return field | } |} """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_NAME_OF_VARIABLE_INSIDE_ACCESSOR) fun `check wrong setter and getter examples`() { lintMethod( """ |class A { | | var isEmpty: Boolean = false | set(values) { | println("Side effect") | isEmpty = values | } | | var isNotEmpty: Boolean = true | set(value) { | val q = isNotEmpty.and(true) | field = value | } | get() { | println(12345) | return isNotEmpty | } | | var isNotOk: Boolean = false | set(values) { | this.isNotOk = values | } |} """.trimMargin(), DiktatError(4, 4, ruleId, "${WRONG_NAME_OF_VARIABLE_INSIDE_ACCESSOR.warnText()} set(values) {..."), DiktatError(14, 4, ruleId, "${WRONG_NAME_OF_VARIABLE_INSIDE_ACCESSOR.warnText()} get() {..."), DiktatError(20, 4, ruleId, "${WRONG_NAME_OF_VARIABLE_INSIDE_ACCESSOR.warnText()} set(values) {...") ) } @Test @Tag(WarningNames.WRONG_NAME_OF_VARIABLE_INSIDE_ACCESSOR) @Suppress("TOO_LONG_FUNCTION") fun `check examples with local var`() { lintMethod( """ |class A { | | var isEmpty: Boolean = false | set(values) { | fun foo() { | val isEmpty = false | } | isEmpty = values | } | | var isNotOk: Boolean = false | set(valuess) { | var isNotOk = true | isNotOk = valuess | } | | var isOk: Boolean = false | set(valuess) { | isOk = valuess | var isOk = true | } | | var isNotEmpty: Boolean = true | set(value) { | val q = isNotEmpty | field = value | } | get() = field |} """.trimMargin(), DiktatError(4, 4, ruleId, "${WRONG_NAME_OF_VARIABLE_INSIDE_ACCESSOR.warnText()} set(values) {..."), DiktatError(18, 4, ruleId, "${WRONG_NAME_OF_VARIABLE_INSIDE_ACCESSOR.warnText()} set(valuess) {..."), DiktatError(24, 4, ruleId, "${WRONG_NAME_OF_VARIABLE_INSIDE_ACCESSOR.warnText()} set(value) {...") ) } @Test @Tag(WarningNames.WRONG_NAME_OF_VARIABLE_INSIDE_ACCESSOR) fun `shouldn't be triggered when there's a method with the same name as the property`() { lintMethod( """ |class A { | | val blaBla: String | get() = "bla".blaBla("bla") | | fun blaBla(string: String): String = this + string | |} """.trimMargin() ) } @Test @Tag(WarningNames.WRONG_NAME_OF_VARIABLE_INSIDE_ACCESSOR) fun `shouldn't be triggered when the property is an extension property`() { lintMethod( """ |class A { | | fun String.foo() = 42 | val String.foo: Int | get() = foo | |} """.trimMargin() ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter6/RunInScriptFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter6 import com.saveourtool.diktat.ruleset.rules.chapter6.RunInScript import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class RunInScriptFixTest : FixTestBase("test/chapter6/script", ::RunInScript) { @Test @Tag(WarningNames.RUN_IN_SCRIPT) fun `should wrap into run`() { fixAndCompare("SimpleRunInScriptExpected.kts", "SimpleRunInScriptTest.kts") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter6/RunInScriptWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter6 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings.RUN_IN_SCRIPT import com.saveourtool.diktat.ruleset.rules.chapter6.RunInScript import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir import java.nio.file.Path class RunInScriptWarnTest : LintTestBase(::RunInScript) { private val ruleId: String = "$DIKTAT_RULE_SET_ID:${RunInScript.NAME_ID}" @Test @Tag(WarningNames.RUN_IN_SCRIPT) fun `check simple example`(@TempDir tempDir: Path) { lintMethodWithFile( """ class A {} fun foo() { } diktat {} diktat({}) foo/*df*/() foo( //dfdg 10 ) println("hello") w.map { it -> it } tasks.register("a") { dependsOn("b") doFirst { generateCodeStyle(file("rootDir/guide"), file("rootDir/../wp")) } } """.trimMargin(), tempDir = tempDir, fileName = "src/main/kotlin/com/saveourtool/diktat/Example.kts", DiktatError(10, 17, ruleId, "${RUN_IN_SCRIPT.warnText()} foo/*df*/()", true), DiktatError(12, 17, ruleId, "${RUN_IN_SCRIPT.warnText()} foo( //dfdg...", true), DiktatError(15, 17, ruleId, "${RUN_IN_SCRIPT.warnText()} println(\"hello\")", true), DiktatError(17, 17, ruleId, "${RUN_IN_SCRIPT.warnText()} w.map { it -> it }", true), DiktatError(19, 17, ruleId, "${RUN_IN_SCRIPT.warnText()} tasks.register(\"a\") {...", true) ) } @Test @Tag(WarningNames.RUN_IN_SCRIPT) fun `check correct examples`(@TempDir tempDir: Path) { lintMethodWithFile( """ run { println("hello") } run{println("hello")} val task = tasks.register("a") { } """.trimMargin(), tempDir = tempDir, fileName = "src/main/kotlin/com/saveourtool/diktat/Example.kts" ) } @Test @Tag(WarningNames.RUN_IN_SCRIPT) fun `check correct with custom wrapper`(@TempDir tempDir: Path) { lintMethodWithFile( """ custom { println("hello") } oneMore{println("hello")} another { println("hello") } """.trimMargin(), tempDir = tempDir, fileName = "src/main/kotlin/com/saveourtool/diktat/Example.kts" ) } @Test @Tag(WarningNames.RUN_IN_SCRIPT) fun `check gradle file`(@TempDir tempDir: Path) { lintMethodWithFile( """ class A {} fun foo() { } if(true) { goo() } diktat {} diktat({}) foo/*df*/() foo( //dfdg 10 ) println("hello") w.map { it -> it } (tasks.register("a") { dependsOn("b") doFirst { generateCodeStyle(file("rootDir/guide"), file("rootDir/../wp")) } }) """.trimMargin(), tempDir = tempDir, fileName = "src/main/kotlin/com/saveourtool/diktat/builds.gradle.kts", DiktatError(6, 17, ruleId, "${RUN_IN_SCRIPT.warnText()} if(true) {...", true) ) } @Test @Tag(WarningNames.RUN_IN_SCRIPT) fun `check gradle script with eq expression`(@TempDir tempDir: Path) { lintMethodWithFile( """ version = "0.1.0-SNAPSHOT" diktat {} diktat({}) foo/*df*/() foo().goo() """.trimMargin(), tempDir = tempDir, fileName = "src/main/kotlin/com/saveourtool/diktat/builds.gradle.kts" ) } @Test @Tag(WarningNames.RUN_IN_SCRIPT) fun `check kts script with eq expression`(@TempDir tempDir: Path) { lintMethodWithFile( """ version = "0.1.0-SNAPSHOT" diktat {} diktat({}) foo/*df*/() """.trimMargin(), tempDir = tempDir, fileName = "src/main/kotlin/com/saveourtool/diktat/Example.kts", DiktatError(1, 17, ruleId, "${RUN_IN_SCRIPT.warnText()} version = \"0.1.0-SNAPSHOT\"", true), DiktatError(7, 17, ruleId, "${RUN_IN_SCRIPT.warnText()} foo/*df*/()", true) ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter6/SingleConstructorRuleFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter6 import com.saveourtool.diktat.ruleset.rules.chapter6.classes.SingleConstructorRule import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class SingleConstructorRuleFixTest : FixTestBase("test/chapter6/classes", ::SingleConstructorRule) { @Test @Tag(WarningNames.SINGLE_CONSTRUCTOR_SHOULD_BE_PRIMARY) fun `should convert simple secondary constructor to primary`() { fixAndCompare("SimpleConstructorExpected.kt", "SimpleConstructorTest.kt") } @Test @Tag(WarningNames.SINGLE_CONSTRUCTOR_SHOULD_BE_PRIMARY) fun `should convert secondary constructor to a primary and init block`() { fixAndCompare("ConstructorWithInitExpected.kt", "ConstructorWithInitTest.kt") } @Test @Tag(WarningNames.SINGLE_CONSTRUCTOR_SHOULD_BE_PRIMARY) fun `should convert secondary constructor to a primary saving modifiers`() { fixAndCompare("ConstructorWithModifiersExpected.kt", "ConstructorWithModifiersTest.kt") } @Test @Tag(WarningNames.SINGLE_CONSTRUCTOR_SHOULD_BE_PRIMARY) fun `should keep custom assignments when converting secondary constructor`() { fixAndCompare("ConstructorWithCustomAssignmentsExpected.kt", "ConstructorWithCustomAssignmentsTest.kt") } @Test @Tag(WarningNames.SINGLE_CONSTRUCTOR_SHOULD_BE_PRIMARY) fun `should keep assignments and required local variables in an init block`() { fixAndCompare("AssignmentWithLocalPropertyExpected.kt", "AssignmentWithLocalPropertyTest.kt") } @Test @Tag(WarningNames.SINGLE_CONSTRUCTOR_SHOULD_BE_PRIMARY) fun `should not remove different comments`() { fixAndCompare("ConstructorWithCommentsExpected.kt", "ConstructorWithCommentsTest.kt") } @Test @Tag(WarningNames.SINGLE_CONSTRUCTOR_SHOULD_BE_PRIMARY) fun `should not replace constructor with init block`() { fixAndCompare("ConstructorWithComplexAssignmentsExpected.kt", "ConstructorWithComplexAssignmentsTest.kt") } @Test @Tag(WarningNames.SINGLE_CONSTRUCTOR_SHOULD_BE_PRIMARY) fun `should keep expression order`() { fixAndCompare("ConstructorShouldKeepExpressionsOrderExpected.kt", "ConstructorShouldKeepExpressionsOrderTest.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter6/SingleConstructorRuleWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter6 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings.SINGLE_CONSTRUCTOR_SHOULD_BE_PRIMARY import com.saveourtool.diktat.ruleset.rules.chapter6.classes.SingleConstructorRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class SingleConstructorRuleWarnTest : LintTestBase(::SingleConstructorRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${SingleConstructorRule.NAME_ID}" @Test @Tag(WarningNames.SINGLE_CONSTRUCTOR_SHOULD_BE_PRIMARY) fun `should suggest to convert single constructor to primary - positive example`() { lintMethod( """ |class Test(var a: Int) { } | |class Test private constructor(var a: Int) { } """.trimMargin() ) } @Test @Tag(WarningNames.SINGLE_CONSTRUCTOR_SHOULD_BE_PRIMARY) fun `should suggest to convert single constructor to primary`() { lintMethod( """ |class Test { | var a: Int | | constructor(a: Int) { | this.a = a | } |} """.trimMargin(), DiktatError(1, 1, ruleId, "${SINGLE_CONSTRUCTOR_SHOULD_BE_PRIMARY.warnText()} in class ", true) ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter6/SingleInitRuleFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter6 import com.saveourtool.diktat.ruleset.rules.chapter6.classes.SingleInitRule import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class SingleInitRuleFixTest : FixTestBase("test/chapter6/init_blocks", ::SingleInitRule) { @Test @Tag(WarningNames.MULTIPLE_INIT_BLOCKS) fun `should merge init blocks`() { fixAndCompare("InitBlocksExpected.kt", "InitBlocksTest.kt") } @Test @Tag(WarningNames.MULTIPLE_INIT_BLOCKS) fun `should move property assignments from init blocks to declarations`() { fixAndCompare("InitBlockWithAssignmentsExpected.kt", "InitBlockWithAssignmentsTest.kt") } @Test @Tag(WarningNames.MULTIPLE_INIT_BLOCKS) fun `should merge init blocks and move property assignments from init blocks to declarations`() { fixAndCompare("InitBlocksWithAssignmentsExpected.kt", "InitBlocksWithAssignmentsTest.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter6/SingleInitRuleWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter6 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter6.classes.SingleInitRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class SingleInitRuleWarnTest : LintTestBase(::SingleInitRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${SingleInitRule.NAME_ID}" @Test @Tag(WarningNames.MULTIPLE_INIT_BLOCKS) fun `should allow single init block`() { lintMethod( """ |class Example { | init { println("Lorem ipsum") } |} """.trimMargin() ) } @Test @Tag(WarningNames.MULTIPLE_INIT_BLOCKS) fun `should forbid multiple init blocks`() { lintMethod( """ |class Example { | init { println("Lorem ipsum") } | | val foo = 0 | | init { println("Dolor sit amet") } |} """.trimMargin(), DiktatError(1, 15, ruleId, "${Warnings.MULTIPLE_INIT_BLOCKS.warnText()} in class found 2 `init` blocks", true) ) } @Test @Tag(WarningNames.MULTIPLE_INIT_BLOCKS) fun `should warn if properties are assigned in init block`() { lintMethod( """ |class A(baseUrl: String, hardUrl: String) { | private val customUrl: String | init { | customUrl = "${'$'}baseUrl/myUrl" | } |} """.trimMargin(), DiktatError(3, 5, ruleId, "${Warnings.MULTIPLE_INIT_BLOCKS.warnText()} `init` block has assignments that can be moved to declarations", true) ) } @Test @Tag(WarningNames.MULTIPLE_INIT_BLOCKS) fun `shouldn't warn if property are assigned on property in init block`() { lintMethod( """ |class A { | var a: String | var c: String | | init { | val b: String = "a" | a = b | | val d: String = "c" | c = foo(d) | } |} """.trimMargin() ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter6/StatelessClassesRuleFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter6 import com.saveourtool.diktat.ruleset.rules.chapter6.classes.StatelessClassesRule import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames.OBJECT_IS_PREFERRED import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class StatelessClassesRuleFixTest : FixTestBase("test/chapter6/stateless_classes", ::StatelessClassesRule) { @Test @Tag(OBJECT_IS_PREFERRED) fun `fix class to object keyword`() { fixAndCompare("StatelessClassExpected.kt", "StatelessClassTest.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter6/StatelessClassesRuleWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter6 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter6.classes.StatelessClassesRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames.OBJECT_IS_PREFERRED import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class StatelessClassesRuleWarnTest : LintTestBase(::StatelessClassesRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${StatelessClassesRule.NAME_ID}" @Test @Tag(OBJECT_IS_PREFERRED) fun `should not trigger on class not extending any interface`() { lintMethod( """ |class Some : I() { | override fun some() |} """.trimMargin() ) } @Test @Tag(OBJECT_IS_PREFERRED) fun `should trigger on class extending interface`() { lintMethod( """ |class Some : I { | override fun some() |} | |interface I { | fun some() |} """.trimMargin(), DiktatError(1, 1, ruleId, "${Warnings.OBJECT_IS_PREFERRED.warnText()} class Some", true) ) } @Test @Tag(OBJECT_IS_PREFERRED) fun `should not trigger on class with constructor`() { lintMethod( """ |class Some(b: Int) : I { | | override fun some() |} """.trimMargin() ) } @Test @Tag(OBJECT_IS_PREFERRED) fun `should not trigger on class with no interface in this file`() { lintMethod( """ |class Some : I { | | override fun some() |} """.trimMargin() ) } @Test @Tag(OBJECT_IS_PREFERRED) fun `should not trigger on class with state`() { lintMethod( """ |class Some : I { | val a = 5 | override fun some() |} """.trimMargin() ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter6/TrivialPropertyAccessorsFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter6 import com.saveourtool.diktat.ruleset.rules.chapter6.TrivialPropertyAccessors import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames.TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class TrivialPropertyAccessorsFixTest : FixTestBase("test/chapter6/properties", ::TrivialPropertyAccessors) { @Test @Tag(TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED) fun `fix trivial setters and getters`() { fixAndCompare("TrivialPropertyAccessorsExpected.kt", "TrivialPropertyAccessorsTest.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter6/TrivialPropertyAccessorsWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter6 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter6.TrivialPropertyAccessors import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames.TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class TrivialPropertyAccessorsWarnTest : LintTestBase(::TrivialPropertyAccessors) { private val ruleId = "$DIKTAT_RULE_SET_ID:${TrivialPropertyAccessors.NAME_ID}" @Test @Tag(TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED) fun `should trigger on trivial getter and setter`() { lintMethod( """ |class Test { | val prop: Int = 0 | get() { return field } | set(value) { field = value } |} """.trimMargin(), DiktatError(3, 8, ruleId, "${Warnings.TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED.warnText()} get() { return field }", true), DiktatError(4, 8, ruleId, "${Warnings.TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED.warnText()} set(value) { field = value }", true) ) } @Test @Tag(TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED) fun `should trigger on trivial getter and setter equal case`() { lintMethod( """ |class Test { | val prop: Int = 0 | get() = field |} """.trimMargin(), DiktatError(3, 8, ruleId, "${Warnings.TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED.warnText()} get() = field", true) ) } @Test @Tag(TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED) fun `should not trigger on getter and setter`() { lintMethod( """ |class Test { | val prop: Int = 0 | get() { | val b = someLogic(field) | return b | } | set(value) { | val res = func(value) | field = res | } |} """.trimMargin() ) } @Test @Tag(TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED) fun `should not trigger on private setter`() { lintMethod( """ |class Test { | var testName: String? = null | private set |} """.trimMargin() ) } @Test @Tag(TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED) fun `should trigger on getter without braces`() { lintMethod( """ |class Test { | val testName = 0 | get |} """.trimMargin(), DiktatError(3, 8, ruleId, "${Warnings.TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED.warnText()} get", true) ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter6/UseLastIndexFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter6 import com.saveourtool.diktat.ruleset.rules.chapter6.UseLastIndex import com.saveourtool.diktat.util.FixTestBase import org.junit.jupiter.api.Test class UseLastIndexFixTest : FixTestBase("test/chapter6/lastIndex_change", ::UseLastIndex) { @Test fun `fix example with white spaces`() { fixAndCompare("UseAnyWhiteSpacesExpected.kt", "UseAnyWhiteSpacesTest.kt") } @Test fun `fix example with incorrect use length`() { fixAndCompare("IncorrectUseLengthMinusOneExpected.kt", "IncorrectUseLengthMinusOneTest.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter6/UseLastIndexWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter6 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter6.UseLastIndex import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class UseLastIndexWarnTest : LintTestBase(::UseLastIndex) { private val ruleId = "$DIKTAT_RULE_SET_ID:${UseLastIndex.NAME_ID}" @Test @Tag(WarningNames.USE_LAST_INDEX) fun `find method Length - 1 with many dot expressions`() { lintMethod( """ |val A = "AAAAAAAA" |val D = A.B.C.length - 1 | """.trimMargin(), DiktatError(2, 9, ruleId, "${Warnings.USE_LAST_INDEX.warnText()} A.B.C.length - 1", true) ) } @Test @Tag(WarningNames.USE_LAST_INDEX) fun `find method Length - 1 for mane line`() { lintMethod( """ |fun foo() { | val A : String = "AAAA" | var B = A.length | - | 1 |} """.trimMargin() ) } @Test @Tag(WarningNames.USE_LAST_INDEX) fun `find method Length - 1 with many spaces and tabulation`() { lintMethod( """ |val A : String = "AAAA" |var B = A.length - 1 + 214 |var C = A.length - 19 | """.trimMargin(), DiktatError(2, 12, ruleId, "${Warnings.USE_LAST_INDEX.warnText() } A.length - 1", true) ) } @Test @Tag(WarningNames.USE_LAST_INDEX) fun `find method Length - 1 without spaces`() { lintMethod( """ |val A : String = "AAAA" |var B = A.length-1 | """.trimMargin(), DiktatError(2, 9, ruleId, "${Warnings.USE_LAST_INDEX.warnText()} A.length-1", true) ) } @Test @Tag(WarningNames.USE_LAST_INDEX) fun `find method Length - 1 without length`() { lintMethod( """ |val A = "AAAA" |val B = -1 |val C = 6 + 121 |var D = B + C |var E = A.length + 1 """.trimMargin() ) } @Test @Tag(WarningNames.USE_LAST_INDEX) fun `find method Length - 1 without -1`() { lintMethod( """ |val A = "AAAA" |val B = -1 |val C = 6 + 4 |val D = "AAAA".length - 1 | |val M = "ASDFG".length | """.trimMargin(), DiktatError(4, 9, ruleId, "${Warnings.USE_LAST_INDEX.warnText()} \"AAAA\".length - 1", true) ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter6/UselessSupertypeFixTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter6 import com.saveourtool.diktat.ruleset.rules.chapter6.UselessSupertype import com.saveourtool.diktat.util.FixTestBase import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class UselessSupertypeFixTest : FixTestBase("test/paragraph6/useless-override", ::UselessSupertype) { @Test @Tag(WarningNames.USELESS_SUPERTYPE) fun `fix example with one super`() { fixAndCompare("UselessOverrideExpected.kt", "UselessOverrideTest.kt") } @Test @Tag(WarningNames.USELESS_SUPERTYPE) fun `fix several super`() { fixAndCompare("SeveralSuperTypesExpected.kt", "SeveralSuperTypesTest.kt") } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/chapter6/UselessSupertypeWarnTest.kt ================================================ package com.saveourtool.diktat.ruleset.chapter6 import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings.USELESS_SUPERTYPE import com.saveourtool.diktat.ruleset.rules.chapter6.UselessSupertype import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import generated.WarningNames import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class UselessSupertypeWarnTest : LintTestBase(::UselessSupertype) { private val ruleId = "$DIKTAT_RULE_SET_ID:${UselessSupertype.NAME_ID}" @Test @Tag(WarningNames.USELESS_SUPERTYPE) @Suppress("TOO_LONG_FUNCTION") fun `check simple wrong example`() { lintMethod( """ open class Rectangle { open fun draw() { /* ... */ } } class Square() : Rectangle() { override fun draw() { /** * * hehe */ super.draw() } } class Square2() : Rectangle() { override fun draw() { //hehe /* hehe */ super.draw() } } class Square2() : Rectangle() { override fun draw() { val q = super.draw() } } class A: Runnable { override fun run() { } } """.trimMargin(), DiktatError(11, 35, ruleId, "${USELESS_SUPERTYPE.warnText()} Rectangle", true), DiktatError(21, 35, ruleId, "${USELESS_SUPERTYPE.warnText()} Rectangle", true) ) } @Test @Tag(WarningNames.USELESS_SUPERTYPE) fun `check example with two super`() { lintMethod( """ open class Rectangle { open fun draw() { /* ... */ } } interface KK { fun draw() {} fun kk() {} } class Square2() : Rectangle(), KK { override fun draw() { super.draw() super.draw() } private fun goo() { super.kk() } } """.trimMargin(), DiktatError(17, 35, ruleId, "${USELESS_SUPERTYPE.warnText()} KK", true) ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/config/DiktatRuleConfigYamlReaderTest.kt ================================================ package com.saveourtool.diktat.ruleset.config import com.saveourtool.diktat.common.config.rules.DIKTAT_COMMON import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.common.config.rules.getCommonConfiguration import org.junit.jupiter.api.Test import java.nio.file.Paths import kotlin.io.path.inputStream class DiktatRuleConfigYamlReaderTest { @Test fun `testing json reading`() { val rulesConfigList: List = DiktatRuleConfigYamlReader() .invoke(Paths.get("src/test/resources/test-rules-config.yml").inputStream()) assert(rulesConfigList.any { it.name == "CLASS_NAME_INCORRECT" && it.enabled }) assert(rulesConfigList.find { it.name == "CLASS_NAME_INCORRECT" }?.configuration == emptyMap()) assert(rulesConfigList.find { it.name == "DIKTAT_COMMON" } ?.configuration?.get("domainName") == "com.saveourtool.diktat") } @Test fun `testing kotlin version`() { val rulesConfigList: List = DiktatRuleConfigYamlReader() .invoke(Paths.get("src/test/resources/test-rules-config.yml").inputStream()) assert(rulesConfigList.getCommonConfiguration().kotlinVersion == kotlinVersion) assert(rulesConfigList.getCommonConfiguration().testAnchors.contains("androidUnitTest")) assert(rulesConfigList.find { it.name == DIKTAT_COMMON } ?.configuration ?.get("kotlinVersion") ?.kotlinVersion() == kotlinVersion) } companion object { private val kotlinVersion = KotlinVersion(1, 4, 21) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/junit/BooleanOrDefault.kt ================================================ package com.saveourtool.diktat.ruleset.junit /** * @property valueOrNull a boolean value, or `null` (meaning the default value * will be used). */ @Suppress("WRONG_DECLARATIONS_ORDER") enum class BooleanOrDefault(val valueOrNull: Boolean?) { FALSE(false), TRUE(true), DEFAULT(null), ; } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/junit/CloseablePath.kt ================================================ package com.saveourtool.diktat.ruleset.junit import com.saveourtool.diktat.test.framework.util.resetPermissions import com.saveourtool.diktat.test.framework.util.tryToDeleteOnExit import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource import java.io.IOException import java.nio.file.DirectoryNotEmptyException import java.nio.file.FileVisitResult import java.nio.file.FileVisitResult.CONTINUE import java.nio.file.Files.walkFileTree import java.nio.file.NoSuchFileException import java.nio.file.Path import java.nio.file.SimpleFileVisitor import java.nio.file.attribute.BasicFileAttributes import java.util.SortedMap import kotlin.io.path.absolute import kotlin.io.path.deleteExisting import kotlin.io.path.isDirectory import kotlin.io.path.notExists import kotlin.io.path.relativeToOrSelf /** * @property directory the temporary directory (will be recursively deleted once * the test completes). */ data class CloseablePath(val directory: Path) : CloseableResource { @Throws(IOException::class) override fun close() { val failures = deleteAllFilesAndDirectories() if (failures.isNotEmpty()) { throw failures.toIoException() } } @Suppress("TOO_LONG_FUNCTION") @Throws(IOException::class) private fun deleteAllFilesAndDirectories(): SortedMap { if (directory.notExists()) { return emptyMap().toSortedMap() } val failures: SortedMap = sortedMapOf() directory.resetPermissions() walkFileTree(directory, object : SimpleFileVisitor() { override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult { if (dir != directory) { dir.resetPermissions() } return CONTINUE } override fun visitFileFailed(file: Path, exc: IOException): FileVisitResult { /* * `IOException` includes `AccessDeniedException` thrown by * non-readable or non-executable flags. */ resetPermissionsAndTryToDeleteAgain(file, exc) return CONTINUE } override fun visitFile(file: Path, attributes: BasicFileAttributes): FileVisitResult = file.deleteAndContinue() override fun postVisitDirectory(dir: Path, exc: IOException?): FileVisitResult = dir.deleteAndContinue() private fun Path.deleteAndContinue(): FileVisitResult { try { deleteExisting() } catch (_: NoSuchFileException) { /* * Ignore. */ } catch (dnee: DirectoryNotEmptyException) { failures[this] = dnee } catch (ioe: IOException) { /* * `IOException` includes `AccessDeniedException` thrown by * non-readable or non-executable flags. */ resetPermissionsAndTryToDeleteAgain(this, ioe) } return CONTINUE } private fun resetPermissionsAndTryToDeleteAgain(path: Path, ioe: IOException) { try { path.resetPermissions() if (path.isDirectory()) { walkFileTree(path, this) } else { path.deleteExisting() } } catch (suppressed: Exception) { ioe.addSuppressed(suppressed) failures[path] = ioe } } }) return failures } private fun SortedMap.toIoException(): IOException { @Suppress("WRONG_NEWLINES") // False positives, see #1495. val joinedPaths = keys .asSequence() .map(Path::tryToDeleteOnExit) .map { path -> path.relativeToOrSelf(directory) } .map(Any::toString) .joinToString() return IOException("Failed to delete temp directory ${directory.absolute()}. " + "The following paths could not be deleted (see suppressed exceptions for details): $joinedPaths").apply { values.forEach(this::addSuppressed) } } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/junit/ExpectedLintError.kt ================================================ package com.saveourtool.diktat.ruleset.junit import com.saveourtool.diktat.api.DiktatError /** * The common super-interface for expected lint errors (extracted from the * annotated code). */ interface ExpectedLintError { /** * The line number (1-based). */ val line: Int /** * The column number (1-based). */ val column: Int /** * Converts this instance to a [DiktatError]. * * @return the [DiktatError] which corresponds to this instance. */ fun asLintError(): DiktatError } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/junit/ExpectedLintErrors.kt ================================================ package com.saveourtool.diktat.ruleset.junit import com.saveourtool.diktat.ruleset.utils.leadingSpaceCount import org.assertj.core.api.Assertions.assertThat import org.intellij.lang.annotations.Language /** * @property code the filtered code w/o any annotation markers. * @property expectedErrors the list of expected lint errors. */ data class ExpectedLintErrors( @Language("kotlin") val code: String, val expectedErrors: List, ) { init { code.checkIndentTrimmed() } private companion object { private fun String.checkIndentTrimmed() { val commonIndent = lineSequence() .filter(String::isNotEmpty) .map(String::leadingSpaceCount) .minOrNull() ?: return assertThat(commonIndent) .describedAs("The whole code fragment is indented with $commonIndent space(s). Did you forget to call `trimIndent()`?") .isZero } } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/junit/NaturalDisplayName.kt ================================================ package com.saveourtool.diktat.ruleset.junit import org.junit.jupiter.api.MethodOrderer.DisplayName import org.junit.jupiter.api.MethodOrdererContext /** * Like [DisplayName], but uses the _natural sort_ order * (i. e. `test 9` < `test 10`). * * @see DisplayName */ class NaturalDisplayName : DisplayName() { /** * Sort the methods encapsulated in the supplied [MethodOrdererContext] * alphanumerically based on their display names. */ override fun orderMethods(context: MethodOrdererContext) = context.methodDescriptors.sortWith { left, right -> val leftDisplayName = left.displayName val rightDisplayName = right.displayName val leftArgs = callArguments.find(leftDisplayName) ?.groups ?.get("args") ?.value val rightArgs = callArguments.find(rightDisplayName) ?.groups ?.get("args") ?.value /* * If two methods are invoked with the same arguments, exclude the * arguments from the comparison, i. e. order `foo()` before `foobar()`. */ val (leftName, rightName) = when { leftArgs != null && leftArgs == rightArgs -> { val withoutArgs: String.() -> String = { substring(0, length - leftArgs.length - 2) } leftDisplayName.withoutArgs() to rightDisplayName.withoutArgs() } else -> leftDisplayName to rightDisplayName } naturalComparator(leftName)(rightName) } private companion object { private val callArguments = Regex("""\((?[^()]*)\)$""") /** * For "case 1" and "case 10", returns "case " as the common prefix. */ @Suppress("TYPE_ALIAS") private val commonNonNumericPrefix: (String) -> (String) -> String = { left -> { right -> left.commonPrefixWith(right).takeWhile(isNotDigit) } } /** * Returns `true` if this `Char` is not a digit, `false` otherwise. */ private val isNotDigit: Char.() -> Boolean = { !isDigit() } /** * Parses this string as an [Int] number and returns the result. Returns * `null` if the string is not a valid representation of a number. */ private val asIntOrNull: String.() -> Int? = { when { isEmpty() -> null else -> try { toInt() } catch (_: NumberFormatException) { null } } } private fun naturalComparator(left: String): (String) -> Int = { right -> val commonNonNumericPrefix = commonNonNumericPrefix(left)(right) val numericInfix: String.() -> String = { val tail = subSequence(commonNonNumericPrefix.length, length) tail.takeWhile(Char::isDigit).toString() } val leftInfixRaw = left.numericInfix() val rightInfixRaw = right.numericInfix() val leftInfix = leftInfixRaw.asIntOrNull() val rightInfix = rightInfixRaw.asIntOrNull() when { leftInfix != null && rightInfix != null -> when (leftInfix) { /* * When infixes are equal, recursively compare the * remainder suffixes. Thus, "foo9Bar" < "foo9bar". */ rightInfix -> { val leftSuffix = left.substring(commonNonNumericPrefix.length + leftInfixRaw.length) val rightSuffix = right.substring(commonNonNumericPrefix.length + rightInfixRaw.length) naturalComparator(leftSuffix)(rightSuffix) } /* * "foo9bar" < "foo10bar". */ else -> leftInfix - rightInfix } else -> left.compareTo(right) } } } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/junit/RuleInvocationContextProvider.kt ================================================ package com.saveourtool.diktat.ruleset.junit import com.saveourtool.diktat.ruleset.utils.NEWLINE import generated.WarningNames import org.assertj.core.api.Assertions.assertThat import org.intellij.lang.annotations.Language import org.junit.jupiter.api.TestTemplate import org.junit.jupiter.api.extension.ExtensionContext import org.junit.jupiter.api.extension.TestTemplateInvocationContext import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider import org.junit.platform.commons.util.AnnotationUtils.isAnnotated import java.util.stream.Stream import kotlin.reflect.KClass import kotlin.reflect.full.declaredMemberProperties /** * A common super-interface for rule-specific * [TestTemplateInvocationContextProvider] implementations. */ interface RuleInvocationContextProvider : TestTemplateInvocationContextProvider { /** * @return the [TestTemplate] annotation supported by this * [TestTemplateInvocationContextProvider] implementation. */ fun annotationType(): KClass override fun supportsTestTemplate(context: ExtensionContext): Boolean = isAnnotated(context.testMethod, annotationType().java) /** * @param context the extension context for the test template method about * to be invoked. * @param supportedTags the list of check names that should be recognized * (implementation-dependent). * @return a `Stream` of `TestTemplateInvocationContext` instances for the * invocation of the test template method. */ fun provideTestTemplateInvocationContexts(context: ExtensionContext, supportedTags: List): Stream override fun provideTestTemplateInvocationContexts(context: ExtensionContext): Stream { @Suppress("WRONG_NEWLINES") // False positives, see #1495. val supportedTags = context .tags .asSequence() .filter { tag -> tag in warningNames }.toList() assertThat(supportedTags).describedAs("Please annotate `${annotationType().simpleName}` with `@Tag`").isNotEmpty return provideTestTemplateInvocationContexts(context, supportedTags) } /** * Creates an (expected) lint error from the parsed annotation data. * * @param line the line of code which had the annotation (with the * annotation stripped). * @param lineNumber the 1-based line number. * @param tag the name of the check, one of [WarningNames]. * @param properties the properties of the lint error, if any (the map may * be empty). * @return the lint error created. */ fun expectedLintErrorFrom( @Language("kotlin") line: String, lineNumber: Int, tag: String, properties: Map ): E /** * Extracts list errors from the annotated code, using * [expectedLintErrorFrom] as the factory method. * * @param code the annotated code. * @param supportedTags the list of check names that should be recognized * (implementation-dependent). * @param allowEmptyErrors whether the list of expected errors is allowed to * be empty (i.e. the code may contain no known annotations). * @return the list of expected errors as well as the filtered code. * @see expectedLintErrorFrom */ @Suppress("TOO_LONG_FUNCTION") fun extractExpectedErrors(@Language("kotlin") code: String, supportedTags: List, allowEmptyErrors: Boolean ): ExpectedLintErrors { require(supportedTags.isNotEmpty()) { "The list of supported tags is empty" } val codeAnnotationRegex = codeAnnotationRegex(supportedTags) val expectedErrors: MutableList = mutableListOf() @Suppress( "AVOID_NULL_CHECKS", "WRONG_NEWLINES") // False positives, see #1495. val filteredCode = code .trimIndent() .lineSequence() .mapIndexed { index, line -> extractExpectedError(index, line, codeAnnotationRegex) }.map { (line, expectedError) -> if (expectedError != null) { expectedErrors += expectedError } line }.joinToString(separator = NEWLINE.toString()) assertThat(filteredCode) .describedAs("The code is empty, please add some") .isNotEmpty assertThat(filteredCode) .describedAs("The code is blank, please add some non-whitespace") .isNotBlank val supportedTagsDescription = when (supportedTags.size) { 1 -> supportedTags[0] else -> "any of $supportedTags" } if (!allowEmptyErrors) { assertThat(expectedErrors) .describedAs("The code contains no expected-error annotations or an unsupported tag is used (should be $supportedTagsDescription). " + "Please annotate your code or set `includeWarnTests` to `false`:$NEWLINE$filteredCode") .isNotEmpty } return ExpectedLintErrors(filteredCode, expectedErrors) } @Suppress("NESTED_BLOCK") private fun extractExpectedError( index: Int, line: String, codeAnnotationRegex: Regex ): Pair = when (val result = codeAnnotationRegex.matchEntire(line)) { null -> line to null else -> { val groups = result.groups val filteredLine = groups[CODE]?.value ?: line val expectedError = when (val tag = groups[TAG]?.value) { null -> null else -> { val properties = when (val rawProperties = groups[PROPERTIES]?.value) { null -> emptyMap() else -> parseProperties(rawProperties) } expectedLintErrorFrom( line = filteredLine, lineNumber = index + 1, tag = tag, properties = properties) } } filteredLine to expectedError } } private companion object { private const val CODE = "code" /** * The common prefix code annotation comments will have. */ private const val CODE_ANNOTATION_PREFIX = "diktat" @Language("RegExp") private const val KEY = """[^=,\h]+""" private const val KEY_GROUP = "key" private const val PROPERTIES = "properties" private const val TAG = "tag" @Language("RegExp") private const val VALUE = """[^,\]]*?""" private const val VALUE_GROUP = "value" private val entryRegex = Regex("""\h*(?<$KEY_GROUP>$KEY)\h*=\h*(?<$VALUE_GROUP>$VALUE)\h*""") @Suppress( "WRONG_NEWLINES", // False positives, see #1495. "BLANK_LINE_BETWEEN_PROPERTIES") // False positives, see #1496. private val warningNames = WarningNames::class .declaredMemberProperties .asSequence() .map { property -> property.name }.toSet() /** * Matches single-line (trailing) comments which contain strings like * * ``` * diktat:CHECK_NAME * ``` * * or * * ``` * diktat:CHECK_NAME[name1 = value1, name2 = value2] * ``` * Example: * * ``` * diktat:WRONG_INDENTATION[expectedIndent = 4] * ``` */ private fun codeAnnotationRegex(supportedTags: List): Regex { require(supportedTags.isNotEmpty()) { "The list of supported tags is empty" } val tagRegex = supportedTags.asSequence().map { tag -> """\Q$tag\E""" }.joinToString(prefix = "(?<$TAG>", separator = "|", postfix = ")") return Regex("""^(?<$CODE>.*?)\h*//\h*\Q$CODE_ANNOTATION_PREFIX\E:$tagRegex(?:\[\h*(?<$PROPERTIES>$KEY\h*=\h*$VALUE\h*(?:,\h*$KEY\h*=\h*$VALUE\h*?)*)\h*,?\h*])?\h*$""") } private fun parseProperties(rawProperties: String): Map = rawProperties.splitToSequence(',').mapNotNull { entry -> entryRegex.matchEntire(entry)?.let { result -> val groups = result.groups val key = groups[KEY_GROUP]?.value val value = groups[VALUE_GROUP]?.value when (key) { null -> null else -> key to value } } }.toMap() } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/smoke/RulesConfigValidationTest.kt ================================================ package com.saveourtool.diktat.ruleset.smoke import com.saveourtool.diktat.ruleset.config.DiktatRuleConfigYamlReader import com.saveourtool.diktat.ruleset.rules.DiktatRuleSetFactoryImpl import com.saveourtool.diktat.test.framework.util.deleteIfExistsSilently import com.charleskorn.kaml.InvalidPropertyValueException import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import java.io.File import java.lang.IllegalArgumentException import kotlin.io.path.createTempFile class RulesConfigValidationTest { private lateinit var file: File @BeforeEach fun setUp() { file = createTempFile().toFile() } @AfterEach fun tearDown() { file.toPath().deleteIfExistsSilently() } @Test @Suppress("GENERIC_VARIABLE_WRONG_DECLARATION") fun `should throw error if name is missing in Warnings`() { file.writeText( """ |- name: MISSING_DOC_TOP_LEVEL | enabled: true | configuration: {} """.trimMargin() ) val exception = assertThrows { diktatRuleSetFactory(diktatRuleConfigReader(file.inputStream())) } Assertions.assertEquals("Warning name in configuration file is invalid, did you mean ?", exception.message) } @Test fun `should throw error on invalid yml config`() { file.writeText( """ |- name: PACKAGE_NAME_MISSING | enabled: true | configuration: """.trimMargin() ) assertThrows { diktatRuleSetFactory(diktatRuleConfigReader(file.inputStream())) } } @Test @Disabled("https://github.com/saveourtool/diKTat/issues/395") fun `should throw error on invalid configuration section`() { file.writeText( """ |- name: TOO_LONG_FUNCTION | enabled: true | configuration: | maxFunctionLength: 1o | isIncludeHeader: Fslse """.trimMargin() ) diktatRuleSetFactory(diktatRuleConfigReader(file.inputStream())) } companion object { private val diktatRuleSetFactory = DiktatRuleSetFactoryImpl() private val diktatRuleConfigReader = DiktatRuleConfigYamlReader() } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/utils/AstNodeUtilsTest.kt ================================================ @file:Suppress( "HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE", "LOCAL_VARIABLE_EARLY_DECLARATION", "AVOID_NULL_CHECKS", ) package com.saveourtool.diktat.ruleset.utils import com.saveourtool.diktat.api.DiktatErrorEmitter import com.saveourtool.diktat.api.DiktatRule import com.saveourtool.diktat.api.DiktatRuleSet import com.saveourtool.diktat.ktlint.check import com.saveourtool.diktat.util.applyToCode import org.jetbrains.kotlin.KtNodeTypes import org.jetbrains.kotlin.KtNodeTypes.CLASS import org.jetbrains.kotlin.KtNodeTypes.CLASS_BODY import org.jetbrains.kotlin.lexer.KtTokens.EOL_COMMENT import org.jetbrains.kotlin.lexer.KtTokens.EQ import org.jetbrains.kotlin.KtNodeTypes.FUN import org.jetbrains.kotlin.lexer.KtTokens.IDENTIFIER import org.jetbrains.kotlin.KtNodeTypes.INTEGER_CONSTANT import org.jetbrains.kotlin.KtNodeTypes.MODIFIER_LIST import org.jetbrains.kotlin.KtNodeTypes.PROPERTY import org.jetbrains.kotlin.KtNodeTypes.TYPE_REFERENCE import org.jetbrains.kotlin.KtNodeTypes.VALUE_PARAMETER_LIST import org.jetbrains.kotlin.lexer.KtTokens.VAL_KEYWORD import org.jetbrains.kotlin.lexer.KtTokens.WHITE_SPACE import org.intellij.lang.annotations.Language import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType import org.jetbrains.kotlin.psi.stubs.elements.KtFileElementType import org.jetbrains.kotlin.utils.addToStdlib.ifNotEmpty import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test @Suppress("LargeClass", "UnsafeCallOnNullableType") class AstNodeUtilsTest { @Test @Suppress("TOO_LONG_FUNCTION") fun `String representation of ASTNode`() { val code = """ class Test { val x = 0 } """.trimIndent() PrettyPrintingVisitor.assertStringRepr(KtFileElementType.INSTANCE, code, 0, 2, """ |kotlin.FILE: "class Test { | val x = 0 |}" |- PACKAGE_DIRECTIVE: "" |- IMPORT_LIST: "" |- CLASS: "class Test { | val x = 0 |}" |-- class: "class" |-- WHITE_SPACE: " " |-- IDENTIFIER: "Test" |-- WHITE_SPACE: " " |-- CLASS_BODY: "{ | val x = 0 |}" | """.trimMargin()) PrettyPrintingVisitor.assertStringRepr(KtFileElementType.INSTANCE, """val x = 0""", expected = """ |kotlin.FILE: "val x = 0" |- PACKAGE_DIRECTIVE: "" |- IMPORT_LIST: "" |- PROPERTY: "val x = 0" |-- val: "val" |-- WHITE_SPACE: " " |-- IDENTIFIER: "x" |-- WHITE_SPACE: " " |-- EQ: "=" |-- WHITE_SPACE: " " |-- INTEGER_CONSTANT: "0" |--- INTEGER_LITERAL: "0" | """.trimMargin()) } @Test fun `test node's check text length`() { val code = """ class Test { /** * test method * @param a - dummy int */ fun foo(a: Int): Int = 2 * a } """.trimIndent() applyToCode(code, 1) { node, counter -> if (node.elementType == CLASS) { Assertions.assertTrue(node.isTextLengthInRange(IntRange(code.length, code.length))) counter.incrementAndGet() } } } @Test fun `test IdentifierName`() { val code = """ class Test { /** * test method * @param a - dummy int */ fun foo(a: Int): Int = 2 * a } """.trimIndent() val list = listOf("Test", "foo", "a", "a", "Int", "Int", "a") applyToCode(code, 7) { node, counter -> node.getIdentifierName()?.let { Assertions.assertEquals(list[counter.get()], it.text) counter.incrementAndGet() } } } @Test fun `test getTypeParameterList`() { val code = """ class Array(val size: Int) { } """.trimIndent() applyToCode(code, 1) { node, counter -> if (node.getTypeParameterList() != null) { Assertions.assertEquals("", node.getTypeParameterList()!!.text) counter.incrementAndGet() } } } @Test fun `test getAllIdentifierChildren`() { val code = """ class Test() { /** * test method * @param a - dummy int */ fun foo(a: Int): Int = 2 * a } """.trimIndent() val list = listOf("Test", "foo", "a", "a", "Int", "Int", "a") applyToCode(code, 7) { node, counter -> node.getAllChildrenWithType(IDENTIFIER).ifNotEmpty { this.forEach { Assertions.assertEquals(list[counter.get()], it.text) } counter.incrementAndGet() } } } @Test fun `test getAllChildrenWithType`() { val code = """ class Test() { /** * test method * @param a - dummy int */ fun foo(a: Int): Int = 2 * a } """.trimIndent() applyToCode(code, 2) { node, counter -> node.getAllChildrenWithType(CLASS).ifNotEmpty { Assertions.assertEquals(map { it.text }, listOf(code)) counter.incrementAndGet() } if (node.getAllChildrenWithType(IDENTIFIER).isNotEmpty() && node.treeParent.elementType == KtFileElementType.INSTANCE) { Assertions.assertEquals(node.getAllChildrenWithType(IDENTIFIER)[0].text, "Test") counter.incrementAndGet() } } } @Test fun `test getFirstChildWithType`() { val code = """ class Test() { /** * test method * @param a - dummy int */ fun foo(a: Int): Int = 2 * a } """.trimIndent() applyToCode(code, 1) { node, counter -> if (node.getAllChildrenWithType(IDENTIFIER).isNotEmpty() && node.treeParent.elementType == KtFileElementType.INSTANCE) { Assertions.assertEquals(node.getFirstChildWithType(IDENTIFIER)!!.text, "Test") counter.incrementAndGet() } } } @Test fun `test hasChildOfType`() { val code = """ class Test { val x = 0 } """.trimIndent() applyToCode(code, 2) { node, counter -> if (node.getIdentifierName() != null) { Assertions.assertTrue(node.hasChildOfType(IDENTIFIER)) counter.incrementAndGet() } } } @Test fun `test hasAnyChildOfTypes`() { val code = """ class Test { val x = 0 } """.trimIndent() applyToCode(code, 3) { node, counter -> if (node.getAllChildrenWithType(IDENTIFIER).isNotEmpty() || node.getAllChildrenWithType(CLASS).isNotEmpty()) { Assertions.assertTrue(node.hasAnyChildOfTypes(IDENTIFIER, CLASS)) counter.incrementAndGet() } } } @Test fun `test findChildBefore`() { val code = """ class Test() { /** * test method * @param a - dummy int */ fun foo(a: Int): Int = 2 * a } """.trimIndent() applyToCode(code, 1) { node, counter -> if (node.findChildBefore(CLASS_BODY, CLASS) != null) { Assertions.assertEquals(node.findChildBefore(CLASS_BODY, CLASS)!!.text, code) counter.incrementAndGet() } } } @Test fun `test findChildBefore - with siblings`() { val code = """ class Test() { /** * test method * @param a - dummy int */ fun foo(a: Int): Int = 2 * a } """.trimIndent() val list = listOf("Test", "foo", "a", "a", "Int", "Int", "a") applyToCode(code, 7) { node, counter -> if (node.findChildBefore(CLASS_BODY, IDENTIFIER) != null) { Assertions.assertEquals(node.findChildBefore(CLASS_BODY, IDENTIFIER)!!.text, list[counter.get()]) counter.incrementAndGet() } } } @Test fun `test findChildAfter`() { val code = """ class Test() { /** * test method * @param a - dummy int */ fun foo(a: Int): Int = 2 * a } """.trimIndent() applyToCode(code, 1) { node, counter -> node.findChildAfter(VALUE_PARAMETER_LIST, TYPE_REFERENCE)?.let { Assertions.assertEquals("Int", it.text) counter.incrementAndGet() } } } @Test fun `test allSiblings withSelf - true`() { val code = """ class Test() { /** * test method * @param a - dummy int */ fun foo(a: Int): Int = 2 * a } """.trimIndent() applyToCode(code, 0) { node, _ -> val setParent = if (node.treeParent != null) { node.treeParent.getChildren(null).toSet() } else { setOf(node) } val setSibling = node.allSiblings(true).toSet() Assertions.assertEquals(setParent, setSibling) Assertions.assertTrue(setParent.isNotEmpty()) } } @Test fun `regression - check for companion object`() { applyToCode(""" object Test { val id = 1 } """.trimIndent(), 1) { node, counter -> if (node.elementType == PROPERTY) { Assertions.assertFalse(node.isNodeFromCompanionObject()) counter.incrementAndGet() } } applyToCode(""" companion object Test { val id = 1 } """.trimIndent(), 1) { node, counter -> if (node.elementType == PROPERTY) { Assertions.assertTrue(node.isNodeFromCompanionObject()) counter.incrementAndGet() } } } @Test fun `test isNodeFromCompanionObject`() { val positiveExample = """ class Something{ companion object { val id = 1 } } """.trimIndent() applyToCode(positiveExample, 1) { node, counter -> if (node.elementType == PROPERTY) { Assertions.assertTrue(node.isNodeFromCompanionObject()) counter.incrementAndGet() } } val negativeExample = """ class Test() { /** * test method * @param a - dummy int */ fun foo(a: Int): Int = 2 * a } """.trimIndent() applyToCode(negativeExample, 1) { node, counter -> if (node.elementType == FUN) { Assertions.assertFalse(node.isNodeFromCompanionObject()) counter.incrementAndGet() } } } @Test fun `test node is from object `() { val code = """ object Something{ val id = 1 } """.trimIndent() applyToCode(code, 1) { node, counter -> if (node.elementType == PROPERTY) { Assertions.assertTrue(node.isNodeFromObject()) counter.incrementAndGet() } } } @Test fun `test isNodeFromFileLevel - node from file level`() { val code = """ class Test() { /** * test method * @param a - dummy int */ fun foo(a: Int): Int = 2 * a } """.trimIndent() applyToCode(code, 1) { node, counter -> if (node.treeParent != null && node.elementType == CLASS) { Assertions.assertTrue(node.isNodeFromFileLevel()) counter.incrementAndGet() } } } @Test fun `test isNodeFromFileLevel - node isn't from file level`() { val code = """ val x = 2 """.trimIndent() applyToCode(code, 8) { node, counter -> if (node.elementType != KtFileElementType.INSTANCE) { node.getChildren(null).forEach { Assertions.assertFalse(it.isNodeFromFileLevel()) counter.incrementAndGet() } } } } @Test fun `test isValProperty`() { val code = """ class Test() { private val name = "John" /** * test method * @param a - dummy int */ fun foo(a: Int): Int = 2 * a } """.trimIndent() var isVal = false applyToCode(code, 0) { node, _ -> if (node.isValProperty()) { isVal = true } } Assertions.assertTrue(isVal) } @Test fun `test isConst`() { val code = """ class Test() { const val SPEED = 10 /** * test method * @param a - dummy int */ fun foo(a: Int): Int = 2 * a } """.trimIndent() var isConst = false applyToCode(code, 0) { node, _ -> if (node.isConst()) { isConst = true } } Assertions.assertTrue(isConst) } @Test fun `test isVarProperty`() { val code = """ class Test() { private var name: String? = null /** * test method * @param a - dummy int */ fun foo(a: Int): Int = 2 * a } """.trimIndent() var isVar = false applyToCode(code, 0) { node, _ -> if (node.isVarProperty()) { isVar = true } } Assertions.assertTrue(isVar) } @Test fun `test getAllLLeafsWithSpecificType`() { val code = """ class Test() { /** * test method * @param a - dummy int */ fun foo(a: Int): Int = 2 * a } """.trimIndent() val list: MutableList = mutableListOf() val leafWithTypeList: MutableList = mutableListOf() var firstNode: ASTNode? = null applyToCode(code, 0) { node, _ -> if (firstNode == null) { firstNode = node } if (node.isLeaf() && node.elementType == WHITE_SPACE) { leafWithTypeList.add(node) } } firstNode?.getAllLeafsWithSpecificType(WHITE_SPACE, list) Assertions.assertEquals(list, leafWithTypeList) } @Test @Suppress("UnsafeCallOnNullableType") fun `test findLeafWithSpecificType`() { val code = """ class Test() { /** * test method * @param a - dummy int */ fun foo(a: Int): Int = 2 * a } """.trimIndent() var firstNode: ASTNode? = null var resultNode: ASTNode? = null applyToCode(code, 0) { node, _ -> if (firstNode == null) { firstNode = node } if (resultNode == null && node.elementType == CLASS_BODY) { resultNode = node } } firstNode = firstNode?.findLeafWithSpecificType(CLASS_BODY) Assertions.assertEquals(resultNode!!.text, firstNode!!.text) } @Test fun `test findAllNodesWithSpecificType`() { val code = """ class Test() { /** * test method * @param a - dummy int */ fun foo(a: Int): Int = 2 * a } """.trimIndent() var firstNode: ASTNode? = null val listResults: MutableList = mutableListOf() applyToCode(code, 0) { node, _ -> if (firstNode == null) { firstNode = node } if (node.elementType == IDENTIFIER) { listResults.add(node) } } val listTypes = firstNode?.findAllDescendantsWithSpecificType(IDENTIFIER) Assertions.assertEquals(listResults, listTypes) } @Test fun `test findParentNodeWithSpecificType`() { val code = """ val a = "" class Test() { fun foo() { try { } catch (e: Exception) { } } } """.trimIndent() val listResults: MutableList = mutableListOf() applyToCode(code, 0) { node, _ -> if (node.elementType == IDENTIFIER) { listResults.add(node) } } listResults.forEach { node -> if (node.findParentNodeWithSpecificType(KtNodeTypes.CATCH) == null) { val identifiers = listOf("Test", "foo", "a") Assertions.assertTrue(identifiers.contains(node.text)) { "Identifier <${node.text}> expected not to have CATCH parent node" } } else { val identifiers = listOf("e", "Exception") Assertions.assertTrue(identifiers.contains(node.text)) { "Identifier <${node.text}> expected to have CATCH parent node" } } } } @Test fun `test isAccessibleOutside`() { val negativeExample = """ class Test() { /** * test method * @param a - dummy int */ private fun foo(a: Int): Int = 2 * a } """.trimIndent() applyToCode(negativeExample, 1) { node, counter -> if (node.elementType == MODIFIER_LIST) { Assertions.assertFalse(node.isAccessibleOutside()) counter.incrementAndGet() } } val positiveExample = """ class Test() { /** * test method * @param a - dummy int */ public fun foo(a: Int): Int = 2 * a } """.trimIndent() applyToCode(positiveExample, 1) { node, counter -> if (node.elementType == MODIFIER_LIST) { Assertions.assertTrue(node.isAccessibleOutside()) counter.incrementAndGet() } } } @Test fun `test leaveOnlyOneNewLine`() { val code = """ var x = 2 """.trimIndent() applyToCode(code, 1) { node, counter -> if (node.elementType == WHITE_SPACE && node.text.contains("\n\n")) { val parent = node.treeParent val firstText = node.text node.leaveOnlyOneNewLine() val secondText = parent .getChildren(null) .last() .text Assertions.assertEquals("\n", secondText) Assertions.assertEquals("\n\n", firstText) counter.incrementAndGet() } } } @Test fun `moveChildBefore 1 - reverse`() { applyToCode(""" |val a = 0 |val b = 1 """.trimMargin(), 5) { node, counter -> if (node.getChildren(null).isNotEmpty()) { val listBeforeMove = node.getChildren(null).map { it.elementType } node.getChildren(null).forEachIndexed { index, astNode -> node.moveChildBefore(astNode, node.getChildren(null)[node.getChildren(null).size - index - 1]) } val listAfterMove = node.getChildren(null).map { it.elementType } Assertions.assertEquals(listBeforeMove, listAfterMove.reversed()) counter.incrementAndGet() } } } @Test fun `moveChildBefore 2 - Should correctly move node child to the end`() { applyToCode(""" |val a = 0 |val b = 1""".trimMargin(), 1) { node, counter -> if (node.elementType == KtFileElementType.INSTANCE) { val val1 = node.getFirstChildWithType(PROPERTY)!! val val2 = val1.nextSibling { it.elementType == PROPERTY }!! node.moveChildBefore(val2, val1, true) node.addChild(PsiWhiteSpaceImpl("\n"), val1) Assertions.assertTrue(node.text == """ |val b = 1 |val a = 0 | """.trimMargin() ) counter.incrementAndGet() } } } @Test fun `isChildAfterGroup test`() { applyToCode("val x = 0", 1) { node, counter -> if (node.elementType == PROPERTY) { val valNode = node.getFirstChildWithType(VAL_KEYWORD)!! val identifier = node.getFirstChildWithType(IDENTIFIER)!! val eq = node.getFirstChildWithType(EQ)!! val zero = node.getFirstChildWithType(INTEGER_CONSTANT)!! Assertions.assertTrue(node.isChildAfterAnother(zero, valNode)) Assertions.assertTrue(node.isChildAfterGroup(zero, listOf(identifier, eq))) Assertions.assertFalse(node.isChildAfterAnother(valNode, zero)) Assertions.assertFalse(node.isChildAfterGroup(identifier, listOf(zero, eq))) Assertions.assertTrue(node.isChildBeforeAnother(identifier, zero)) Assertions.assertTrue(node.isChildBeforeGroup(identifier, listOf(eq, zero))) Assertions.assertTrue(node.areChildrenBeforeChild(listOf(valNode, identifier, eq), zero)) Assertions.assertTrue(node.areChildrenBeforeGroup(listOf(valNode, identifier), listOf(eq, zero))) Assertions.assertFalse(node.isChildBeforeAnother(zero, identifier)) Assertions.assertFalse(node.isChildBeforeGroup(zero, listOf(identifier, eq))) Assertions.assertFalse(node.areChildrenBeforeChild(listOf(identifier, eq, zero), valNode)) Assertions.assertFalse(node.areChildrenBeforeGroup(listOf(eq, zero), listOf(valNode, identifier))) counter.incrementAndGet() } } } @Test fun `test line of text extraction`() { applyToCode(""" class Example { fun foo() { } } """.trimIndent(), 1) { node, counter -> if (node.elementType == IDENTIFIER && node.text == "foo") { Assertions.assertEquals("foo() { }", node.extractLineOfText()) counter.incrementAndGet() } } } @Test @Suppress("TOO_LONG_FUNCTION", "TOO_MANY_LINES_IN_LAMBDA") fun `test lambda contains it`() { applyToCode(""" |fun bar(lambda: (s: String) -> Unit) { | lambda("bar") |} | |fun foo(lambda: (s: String) -> Unit) { | lambda("foo") |} | |fun test() { | // test1 | foo { f1 -> | bar { b1 -> | println(f1 + " -> " + b1) | } | } | // test2 | foo { | bar { b2 -> | println(it + " -> " + b2) | } | } | // test3 | foo { f3 -> | bar { | println(f3 + " -> " + it) | } | } |} """.trimMargin(), 3) { node, counter -> if (node.elementType == EOL_COMMENT) { node.nextCodeSibling() ?.lastChildNode ?.firstChildNode ?.let { when (node.text) { "// test1" -> Assertions.assertFalse(doesLambdaContainIt(it)) "// test2" -> Assertions.assertTrue(doesLambdaContainIt(it)) "// test3" -> Assertions.assertFalse(doesLambdaContainIt(it)) else -> { // this is a generated else block } } counter.incrementAndGet() } } } } } private class PrettyPrintingVisitor(private val elementType: IElementType, private val level: Int, private val maxLevel: Int, private val expected: String, ) : DiktatRule { override val id: String get() = "print-ast" override fun invoke(node: ASTNode, autoCorrect: Boolean, emitter: DiktatErrorEmitter) { if (node.elementType == elementType) { Assertions.assertEquals( expected.replace("\n", System.lineSeparator()), node.prettyPrint(level, maxLevel) ) } } companion object { fun assertStringRepr( elementType: IElementType, @Language("kotlin") code: String, level: Int = 0, maxLevel: Int = -1, expected: String ) { check( ruleSetSupplier = { DiktatRuleSet(listOf(PrettyPrintingVisitor(elementType, level, maxLevel, expected))) }, text = code, ) } } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/utils/AvailableRulesDocTest.kt ================================================ package com.saveourtool.diktat.ruleset.utils import com.saveourtool.diktat.ruleset.constants.Warnings import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import java.io.File /** * Special test to check that developer has not forgotten to write documentation for each warning. * Documentation should be added to available-rules.md */ class AvailableRulesDocTest { private fun getAllRulesFromDoc(): List { val listWithRulesFromDoc: MutableList = mutableListOf() File(AVAILABLE_RULES_FILE).forEachLine { line -> val splitMarkDown = line .split("|") val ruleName = splitMarkDown[SPLIT_MARK].trim() if (!ruleName.startsWith(TABLE_DELIMITER) && !ruleName.startsWith(RULE_NAME_HEADER)) { listWithRulesFromDoc.add(ruleName) } } return listWithRulesFromDoc } @Test fun `read rules from documentation`() { val allRulesFromCode = Warnings.values().filterNot { it == Warnings.DUMMY_TEST_WARNING } val allRulesFromDoc = getAllRulesFromDoc() allRulesFromCode.forEach { warning -> val ruleName = warning.ruleName() val ruleFound = allRulesFromDoc.any { it.trim() == ruleName } Assertions.assertTrue(ruleFound) { val docs = "| | | $ruleName" + "| | | | |" """ Cannot find warning $ruleName in $AVAILABLE_RULES_FILE. You can fix it by adding the following description below with more info to $AVAILABLE_RULES_FILE: add $docs to $AVAILABLE_RULES_FILE """ } } allRulesFromDoc.forEach { warning -> val trimmedWarning = warning.trim() val ruleFound = allRulesFromCode.any { it.ruleName() == trimmedWarning } Assertions.assertTrue(ruleFound) { """ Found rule (warning) in documentation: <$trimmedWarning> that does not exist in the code. Misprint or configuration was renamed? """.trimIndent() } } } companion object { const val AVAILABLE_RULES_FILE = "../info/available-rules.md" const val RULE_NAME_HEADER = "Rule name" const val SPLIT_MARK = 3 const val TABLE_DELIMITER = "-----" } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/utils/FunctionAstNodeUtilsTest.kt ================================================ package com.saveourtool.diktat.ruleset.utils import com.saveourtool.diktat.util.applyToCode import org.jetbrains.kotlin.KtNodeTypes.FUN import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test @Suppress("UnsafeCallOnNullableType") class FunctionAstNodeUtilsTest { @Test fun `should detect parameters in function - no parameters`() { applyToCode("fun foo() { }", 1) { node, counter -> if (node.elementType == FUN) { Assertions.assertFalse(node.hasParameters()) Assertions.assertTrue(node.parameterNames().isEmpty()) counter.incrementAndGet() } } } @Test fun `should detect parameters in function`() { applyToCode("fun foo(a: Int) { }", 1) { node, counter -> if (node.elementType == FUN) { Assertions.assertTrue(node.hasParameters()) Assertions.assertEquals(listOf("a"), node.parameterNames()) counter.incrementAndGet() } } } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/utils/KotlinParserTest.kt ================================================ package com.saveourtool.diktat.ruleset.utils import com.saveourtool.diktat.util.applyToCode import org.jetbrains.kotlin.KtNodeTypes.CALL_EXPRESSION import org.jetbrains.kotlin.KtNodeTypes.CLASS import org.jetbrains.kotlin.KtNodeTypes.CLASS_BODY import org.jetbrains.kotlin.lexer.KtTokens.CLASS_KEYWORD import org.jetbrains.kotlin.KtNodeTypes.FUN import org.jetbrains.kotlin.KtNodeTypes.IMPORT_DIRECTIVE import org.jetbrains.kotlin.lexer.KtTokens.IMPORT_KEYWORD import org.jetbrains.kotlin.KtNodeTypes.IMPORT_LIST import org.jetbrains.kotlin.kdoc.lexer.KDocTokens.KDOC import org.jetbrains.kotlin.KtNodeTypes.PACKAGE_DIRECTIVE import org.jetbrains.kotlin.KtNodeTypes.PROPERTY import org.jetbrains.kotlin.lexer.KtTokens.RBRACE import org.jetbrains.kotlin.KtNodeTypes.SECONDARY_CONSTRUCTOR import org.jetbrains.kotlin.lexer.KtTokens.WHITE_SPACE import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl import org.jetbrains.kotlin.psi.stubs.elements.KtFileElementType import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows class KotlinParserTest { @Test fun `test simple property`() { val node = KotlinParser().createNode("val x: Int = 10") Assertions.assertEquals(PROPERTY, node.elementType) Assertions.assertEquals("val x: Int = 10", node.text) Assertions.assertEquals(4, node.findAllDescendantsWithSpecificType(WHITE_SPACE).size) } @Test @Suppress("UnsafeCallOnNullableType") fun `test oneline function`() { val node = KotlinParser().createNode("fun foo(text: String) = text.toUpperCase()") Assertions.assertEquals(FUN, node.elementType) Assertions.assertEquals("fun foo(text: String) = text.toUpperCase()", node.text) Assertions.assertEquals("foo", node.getIdentifierName()!!.text) Assertions.assertEquals(4, node.findAllDescendantsWithSpecificType(WHITE_SPACE).size) } @Test fun `test invalidate code`() { assertThrows { KotlinParser().createNode("simple text") } assertThrows { KotlinParser().createNode("") } assertThrows { KotlinParser().createNode("fuc fun() = 1") } } @Test fun `test multiline code with import and package`() { val code = """ |package com.saveourtool.diktat.ruleset.utils | |import org.junit.jupiter.api.Test |import org.junit.jupiter.api.Tests | |class A { | fun foo(){ | println("hello") | } |} """.trimMargin() val node = KotlinParser().createNode(code, true) Assertions.assertEquals(KtFileElementType.INSTANCE, node.elementType) Assertions.assertEquals(PACKAGE_DIRECTIVE, node.firstChildNode.elementType) } @Test fun `test multiline class code`() { val code = """ |class A { | fun foo(){ | println("hello") | } |} """.trimMargin() val node = KotlinParser().createNode(code) Assertions.assertEquals(CLASS, node.elementType) Assertions.assertEquals(CLASS_KEYWORD, node.firstChildNode.elementType) } @Test fun `test multiline class code with import`() { val code = """ |import org.junit.jupiter.api.Test |import org.junit.jupiter.api.Tests | |class A { | fun foo(){ | println("hello") | } |} """.trimMargin() assertThrows { KotlinParser().createNode(code) } } @Test @Suppress( "UnsafeCallOnNullableType", "TOO_LONG_FUNCTION", "AVOID_NULL_CHECKS" ) fun `test multiline class code compare with applyToCode`() { val emptyClass = """ |package com.saveourtool.diktat.ruleset.utils | |import org.junit.jupiter.api.Test | |class A { |} """.trimMargin() var nodeToApply: ASTNode? = null applyToCode(emptyClass, 0) { newNode, _ -> if (nodeToApply == null) { nodeToApply = newNode } } val resultClass = """ |package com.saveourtool.diktat.ruleset.utils | |import org.junit.jupiter.api.Test | |class A { |fun foo() = "Hello" |} """.trimMargin() var resultNode: ASTNode? = null applyToCode(resultClass, 0) { newNode, _ -> if (resultNode == null) { resultNode = newNode } } Assertions.assertTrue(nodeToApply!!.prettyPrint() == KotlinParser().createNode(emptyClass, true).prettyPrint()) val classNode = nodeToApply!!.findChildByType(CLASS)!!.findChildByType(CLASS_BODY)!! val function = """ |fun foo() = "Hello" """.trimMargin() classNode.addChild(KotlinParser().createNode(function), classNode.findChildByType(RBRACE)) classNode.addChild(PsiWhiteSpaceImpl("\n"), classNode.findChildByType(RBRACE)) Assertions.assertTrue(nodeToApply!!.prettyPrint() == resultNode!!.prettyPrint()) } @Test fun `check package`() { val packageCode = """ |package com.saveourtool.diktat.ruleset.utils """.trimMargin() val node = KotlinParser().createNode(packageCode, true) Assertions.assertEquals(KtFileElementType.INSTANCE, node.elementType) Assertions.assertEquals(packageCode, node.text) Assertions.assertEquals(PACKAGE_DIRECTIVE, node.firstChildNode.elementType) } @Test fun `check import`() { val importCode = """ |import org.junit.jupiter.api.Test """.trimMargin() val node = KotlinParser().createNode(importCode) Assertions.assertEquals(IMPORT_DIRECTIVE, node.elementType) Assertions.assertEquals(importCode, node.text) Assertions.assertEquals(IMPORT_KEYWORD, node.firstChildNode.elementType) } @Test fun `check imports`() { val importCode = """ |import org.junit.jupiter.api.Test |import org.junit.jupiter.api.Tests |import org.junit.jupiter.api """.trimMargin() val node = KotlinParser().createNode(importCode) Assertions.assertEquals(IMPORT_LIST, node.elementType) Assertions.assertEquals(importCode, node.text) Assertions.assertEquals(IMPORT_DIRECTIVE, node.firstChildNode.elementType) } @Test fun `check package and import`() { val code = """ |package com.saveourtool.diktat.ruleset.utils | |import org.junit.jupiter.api.Test |import org.junit.jupiter.api.Tests """.trimMargin() val node = KotlinParser().createNode(code, true) Assertions.assertEquals(KtFileElementType.INSTANCE, node.elementType) Assertions.assertEquals(code, node.text) Assertions.assertEquals(PACKAGE_DIRECTIVE, node.firstChildNode.elementType) } @Test @Suppress("UnsafeCallOnNullableType") fun `check KDoc`() { val code = """ |/** |* [link]haha |*/ |fun foo() """.trimMargin() val node = KotlinParser().createNode(code) Assertions.assertEquals(KDOC, node.findChildByType(FUN)!!.firstChildNode.elementType) val kdocText = """ /** * [link]haha */ """.trimIndent() Assertions.assertEquals(kdocText, node.findChildByType(FUN)!!.firstChildNode.text) } @Test fun `test createNodeForInit`() { val code = """ |init { | println("A") | // import is a weak keyword | println("B") |} """.trimMargin() val node = KotlinParser().createNodeForInit(code) Assertions.assertEquals(CALL_EXPRESSION, node.elementType) Assertions.assertEquals(code, node.text) } @Test fun `test createNodeForSecondaryConstructor`() { val code = """ |constructor(a: Int) { | // import is a weak keyword | b = a.toString() |} """.trimMargin() val node = KotlinParser().createNodeForSecondaryConstructor(code) Assertions.assertEquals(SECONDARY_CONSTRUCTOR, node.elementType) Assertions.assertEquals(code, node.text) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/utils/RulesConfigYamlTest.kt ================================================ package com.saveourtool.diktat.ruleset.utils import com.saveourtool.diktat.common.config.rules.DIKTAT_COMMON import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.config.kotlinVersion import com.saveourtool.diktat.ruleset.config.DiktatRuleConfigYamlReader import com.saveourtool.diktat.common.config.rules.getRuleConfig import com.saveourtool.diktat.ruleset.constants.Warnings import com.charleskorn.kaml.Yaml import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import java.io.File import java.nio.file.Paths import kotlin.io.path.exists import kotlin.io.path.inputStream import kotlinx.serialization.encodeToString /** * Special test that checks that developer has not forgotten to add his warning to a diktat-analysis.yml * This file is needed to be in tact with latest changes in Warnings.kt */ @Suppress("UNUSED") class RulesConfigYamlTest { private val parentDiktatAnalysis = "${System.getProperty("user.dir")}${File.separator}..${File.separator}diktat-analysis.yml${File.separator}" private val pathMap: Map = mapOf("diktat-analysis.yml" to "diKTat/diktat-rules/src/main/resources/diktat-analysis.yml", "diktat-analysis-huawei.yml" to "diKTat/diktat-rules/src/main/resources/diktat-analysis-huawei.yml", parentDiktatAnalysis to "diKTat/diktat-analysis.yml") @Test fun `read rules config yml`() { compareRulesAndConfig("diktat-analysis.yml") compareRulesAndConfig("diktat-analysis-huawei.yml") compareRulesAndConfig(parentDiktatAnalysis, "diKTat/diktat-analysis.yml") } @Test fun `check comments before rules`() { checkComments("src/main/resources/diktat-analysis.yml") checkComments("src/main/resources/diktat-analysis-huawei.yml") checkComments("../diktat-analysis.yml") } @Test @Suppress("UNUSED") fun `check kotlin version`() { val currentKotlinVersion = "${KotlinVersion.CURRENT.major}.${KotlinVersion.CURRENT.minor}" pathMap.keys.forEach { path -> val config = readAllRulesFromConfig(path) val ktVersion = config.find { it.name == DIKTAT_COMMON } ?.configuration ?.get("kotlinVersion") ?.kotlinVersion() val ktVersionNew = "${ktVersion?.major}.${ktVersion?.minor}" Assertions.assertEquals(ktVersionNew, currentKotlinVersion) } } private fun checkComments(configName: String) { val lines = File(configName) .readLines() .filter { it.startsWith("-") || it.startsWith("#") } lines.forEachIndexed { index, str -> if (str.startsWith("-")) { Assertions.assertTrue(lines[if (index > 0) index - 1 else 0].trim().startsWith("#")) { """ There is no comment before $str in $configName """.trimIndent() } } } } private fun compareRulesAndConfig(nameConfig: String, nameConfigToText: String? = null) { val filePath = nameConfigToText?.let { pathMap[it] } ?: pathMap[nameConfig] val allRulesFromConfig = readAllRulesFromConfig(nameConfig) val allRulesFromCode = readAllRulesFromCode() allRulesFromCode.forEach { rule -> if (rule == Warnings.DUMMY_TEST_WARNING) { return@forEach } val foundRule = allRulesFromConfig.getRuleConfig(rule) val ymlCodeSnippet = RulesConfig(rule.ruleName(), true, emptyMap()) val ruleYaml = Yaml.default.encodeToString(ymlCodeSnippet) Assertions.assertTrue(foundRule != null) { """ Cannot find warning ${rule.ruleName()} in $filePath. You can fix it by adding the following code below to $filePath: $ruleYaml """.trimIndent() } } allRulesFromConfig.forEach { warning -> val warningName = warning.name val ruleFound = allRulesFromCode.any { it.ruleName() == warningName || warningName == "DIKTAT_COMMON" } Assertions.assertTrue(ruleFound) { """ Found rule (warning) in $filePath: <$warningName> that does not exist in the code. Misprint or configuration was renamed? """.trimIndent() } } } private fun readAllRulesFromConfig(nameConfig: String) = run { Paths.get(nameConfig).takeIf { it.exists() }?.inputStream() ?: javaClass.classLoader.getResourceAsStream(nameConfig) } ?.let { DiktatRuleConfigYamlReader().invoke(it) } ?: emptyList() private fun readAllRulesFromCode() = Warnings.values() } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/utils/StringCaseUtilsTest.kt ================================================ package com.saveourtool.diktat.ruleset.utils import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test class StringCaseUtilsTest { @Test fun `check conversion to upperSnakeCase`() { Assertions.assertEquals("PASCAL_CASE", "PascalCase".toUpperSnakeCase()) Assertions.assertEquals("LOWER_SNAKE", "lower_snake".toUpperSnakeCase()) Assertions.assertEquals("I_AM_CONSTANT", "iAmConstant".toUpperSnakeCase()) Assertions.assertEquals("PASCAL_N_CASE", "PascalN_Case".toUpperSnakeCase()) } @Test fun `check conversion to lowerCamelCase`() { Assertions.assertEquals("strangeName", "STRANGE_name".toLowerCamelCase()) Assertions.assertEquals("strangeName", "STRANGE_NAME".toLowerCamelCase()) Assertions.assertEquals("sTrangeName", "sTrange_NAME".toLowerCamelCase()) Assertions.assertEquals("sTrangeName", "sTRange_NAME".toLowerCamelCase()) Assertions.assertEquals("sTrangeName", "__sTRange_NAME".toLowerCamelCase()) Assertions.assertEquals("pascalNCase", "PascalN_Case".toLowerCamelCase()) } @Test fun `check conversion to PascalCase`() { Assertions.assertEquals("StrangeName", "STRANGE_name".toPascalCase()) Assertions.assertEquals("StrangeName", "STRANGE_NAME".toPascalCase()) Assertions.assertEquals("StrangeName", "sTrange_NAME".toPascalCase()) Assertions.assertEquals("KdocTest", "KDoc_test".toPascalCase()) Assertions.assertEquals("StrangeName", "sTRange_NAME".toPascalCase()) Assertions.assertEquals("StrangeName", "__sTRange_NAME".toPascalCase()) Assertions.assertEquals("PascalNCase", "PascalN_Case".toPascalCase()) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/utils/SuppressAnnotatedExpressionTest.kt ================================================ package com.saveourtool.diktat.ruleset.utils import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter3.CollapseIfStatementsRule import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import org.junit.jupiter.api.Test class SuppressAnnotatedExpressionTest : LintTestBase(::CollapseIfStatementsRule) { private val ruleId: String = "$DIKTAT_RULE_SET_ID:${CollapseIfStatementsRule.NAME_ID}" @Test fun `should lint errors without suppress`() { val code = """ |fun foo() { | if (true) { | if (true) { | if (true) { | | } | } | } |} """.trimMargin() lintMethod(code, DiktatError(3, 8, ruleId, "${Warnings.COLLAPSE_IF_STATEMENTS.warnText()} avoid using redundant nested if-statements", true), DiktatError(4, 12, ruleId, "${Warnings.COLLAPSE_IF_STATEMENTS.warnText()} avoid using redundant nested if-statements", true) ) } @Test fun `should suppress annotated expressions`() { val code = """ |fun foo() { | if (true) { | @Suppress("COLLAPSE_IF_STATEMENTS") | if (true) { | if (true) { | | } | } | } |} """.trimMargin() lintMethod(code) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/utils/SuppressTest.kt ================================================ package com.saveourtool.diktat.ruleset.utils import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ruleset.constants.Warnings import com.saveourtool.diktat.ruleset.rules.chapter1.IdentifierNaming import com.saveourtool.diktat.util.LintTestBase import com.saveourtool.diktat.api.DiktatError import org.junit.jupiter.api.Test class SuppressTest : LintTestBase(::IdentifierNaming) { private val ruleId: String = "$DIKTAT_RULE_SET_ID:${IdentifierNaming.NAME_ID}" @Test fun `test suppress on class`() { val code = """ @Suppress("FUNCTION_NAME_INCORRECT_CASE", "BACKTICKS_PROHIBITED") class SomeClass { fun /* */ methODTREE(): String { } fun `some`() {} } """.trimIndent() lintMethod(code) } @Test fun `check suppress on method`() { lintMethod( """ |class SomeClass { | | @Suppress("FUNCTION_NAME_INCORRECT_CASE") | fun /* */ methODTREE(): String { | | fun soMEMETHOD() { | | } | | } | | fun /* */ methODTREEASA(): String { | | } |} """.trimMargin(), DiktatError(12, 14, ruleId, "${Warnings.FUNCTION_NAME_INCORRECT_CASE.warnText()} methODTREEASA", true) ) } @Test fun `check suppress on variable`() { lintMethod( """ |class SomeClass { | | @Suppress("FUNCTION_NAME_INCORRECT_CASE") | fun /* */ methODTREE(): String { | @Suppress( "VARIABLE_NAME_INCORRECT_FORMAT" ) | var SOMEvar = 5 | } |} """.trimMargin() ) } @Test fun `test suppress on file`() { val code = """ @file:Suppress("FUNCTION_NAME_INCORRECT_CASE") class SomeClass { fun /* */ methODTREE(): String { } } """.trimIndent() lintMethod(code) } @Test fun `test suppress field`() { val code = """ class SomeClass(@field:Suppress("IDENTIFIER_LENGTH") val a:String) { fun /* */ method(): String { } } """.trimIndent() lintMethod(code) } @Test fun `test suppress field with set`() { val code = """ class SomeClass() { @set:[Suppress("IDENTIFIER_LENGTH") Inject] val a = 5 fun /* */ method(): String { } } """.trimIndent() lintMethod(code) } @Test fun `check simple wrong enum`() { lintMethod( """ |@set:[Suppress("WRONG_DECLARATION_ORDER") Suppress("IDENTIFIER_LENGTH") Suppress("CONFUSING_IDENTIFIER_NAMING")] |enum class Alph { | D, | C, | A, | B, | ; |} """.trimMargin() ) } @Test fun `test suppress on class bad`() { val code = """ @Suppress() class SomeClass { fun /* */ methODTREE(): String { } } """.trimIndent() lintMethod(code, DiktatError(3, 15, "$DIKTAT_RULE_SET_ID:${IdentifierNaming.NAME_ID}", "${Warnings.FUNCTION_NAME_INCORRECT_CASE.warnText()} methODTREE", true)) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/utils/VariablesSearchTest.kt ================================================ package com.saveourtool.diktat.ruleset.utils import com.saveourtool.diktat.ruleset.utils.search.VariablesSearch import com.saveourtool.diktat.ruleset.utils.search.default import com.saveourtool.diktat.util.applyToCode import org.jetbrains.kotlin.psi.KtElement import org.jetbrains.kotlin.psi.KtNameReferenceExpression import org.jetbrains.kotlin.psi.KtProperty import org.jetbrains.kotlin.psi.stubs.elements.KtFileElementType import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test @Suppress("UnsafeCallOnNullableType") class VariablesSearchTest { @Test fun `testing requirement for collecting variables`() { applyToCode(""" fun foo(a: Int) { fun foo1() { var o = 1 b = o c = o o = 15 o = 17 } } """.trimIndent(), 0) { node, _ -> if (node.elementType != KtFileElementType.INSTANCE) { val variablesSearchAbstract: VariablesSearch = object : VariablesSearch(node, ::default) { override fun KtElement.getAllSearchResults(property: KtProperty): List = TODO("Not required for test") } val thrown = Assertions.assertThrows(IllegalArgumentException::class.java) { variablesSearchAbstract.collectVariables() } assertTrue(thrown.message!!.contains("To collect all variables in a file you need to provide file root node")) } } } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/utils/VariablesWithAssignmentsSearchTest.kt ================================================ package com.saveourtool.diktat.ruleset.utils import com.saveourtool.diktat.ruleset.utils.search.findAllVariablesWithAssignments import com.saveourtool.diktat.util.applyToCode import org.jetbrains.kotlin.psi.stubs.elements.KtFileElementType import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test @Suppress("UnsafeCallOnNullableType") class VariablesWithAssignmentsSearchTest { @Test fun `testing proper variables search in function`() { applyToCode(""" fun foo(a: Int) { fun foo1() { var o = 1 b = o c = o o = 15 o = 17 } } """.trimIndent(), 0) { node, _ -> if (node.elementType == KtFileElementType.INSTANCE) { val vars = node.findAllVariablesWithAssignments().mapKeys { it.key.text } val keys = vars.keys val var1 = keys.elementAt(0) Assertions.assertEquals("var o = 1", var1) Assertions.assertEquals(2, vars[var1]?.size) } } } @Test fun `testing proper variables search in class`() { applyToCode(""" class A { var o = 1 fun foo(a: Int) { fun foo1() { b = o c = o d = o o = 15 o = 17 } } } """.trimIndent(), 0) { node, _ -> if (node.elementType == KtFileElementType.INSTANCE) { val vars = node.findAllVariablesWithAssignments().mapKeys { it.key.text } val keys = vars.keys val var1 = keys.elementAt(0) Assertions.assertEquals("var o = 1", var1) Assertions.assertEquals(2, vars[var1]?.size) } } } @Test @Disabled fun `testing proper variables search with lambda`() { applyToCode(""" fun foo(a: Int) { var a = 1 a++ } """.trimIndent(), 0) { node, _ -> if (node.elementType == KtFileElementType.INSTANCE) { val vars = node.findAllVariablesWithAssignments().mapKeys { it.key.text } val keys = vars.keys val var1 = keys.elementAt(0) Assertions.assertEquals("var a = 1", var1) Assertions.assertEquals(1, vars[var1]?.size) } } } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/utils/VariablesWithUsagesSearchTest.kt ================================================ package com.saveourtool.diktat.ruleset.utils import com.saveourtool.diktat.ruleset.utils.search.findAllVariablesWithUsages import com.saveourtool.diktat.util.applyToCode import org.jetbrains.kotlin.psi.stubs.elements.KtFileElementType import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test @Suppress("UnsafeCallOnNullableType") class VariablesWithUsagesSearchTest { @Test fun `testing proper variables search in function`() { applyToCode(""" fun foo(a: Int) { fun foo1() { val o = 1 val a = 2 println(a.o) } } """.trimIndent(), 0) { node, _ -> if (node.elementType == KtFileElementType.INSTANCE) { val vars = node.findAllVariablesWithUsages().mapKeys { it.key.text } val keys = vars.keys val var1 = keys.elementAt(0) val var2 = keys.elementAt(1) assertEquals("val o = 1", var1) assertEquals(0, vars[var1]?.size) assertEquals("val a = 2", var2) assertEquals(1, vars[var2]?.size) } } } @Test fun `testing proper variables search in function with false positive shadowing`() { applyToCode(""" fun foo() { var v = 1 if (true) { v++ var v = 0 } } """.trimIndent(), 0) { node, _ -> if (node.elementType == KtFileElementType.INSTANCE) { val vars = node.findAllVariablesWithUsages().mapKeys { it.key.text } val keys = vars.keys val var1 = keys.elementAt(0) val var2 = keys.elementAt(1) assertEquals("var v = 1", var1) assertEquals(1, vars[var1]?.size) assertEquals("var v = 0", var2) assertEquals(0, vars[var2]?.size) } } } @Test fun `testing proper variables search in function with false positive shadowing and nesting`() { applyToCode(""" fun foo() { var v = 1 if (true) { if (true) { v++ } var v = 0 } } """.trimIndent(), 0) { node, _ -> if (node.elementType == KtFileElementType.INSTANCE) { val vars = node.findAllVariablesWithUsages().mapKeys { it.key.text } val keys = vars.keys val var1 = keys.elementAt(0) val var2 = keys.elementAt(1) assertEquals("var v = 1", var1) assertEquals(1, vars[var1]?.size) assertEquals("var v = 0", var2) assertEquals(0, vars[var2]?.size) } } } @Test fun `testing proper variables search in simple class with property`() { applyToCode(""" class A { val v = 0 fun foo() { ++v } } """.trimIndent(), 0) { node, _ -> if (node.elementType == KtFileElementType.INSTANCE) { val vars = node.findAllVariablesWithUsages().mapKeys { it.key.text } val keys = vars.keys val var1 = keys.elementAt(0) assertEquals("val v = 0", var1) assertEquals(1, vars[var1]?.size) } } } @Test @Disabled // FixMe: very strange behavior of Kotlin fun `testing proper variables search in function with a class nested in a function`() { applyToCode(""" fun foo() { var a = 0 class A { var a = 1 fun foo() { a++ } } } """.trimIndent(), 0) { node, _ -> if (node.elementType == KtFileElementType.INSTANCE) { val vars = node.findAllVariablesWithUsages().mapKeys { it.key.text } val keys = vars.keys val var1 = keys.elementAt(0) val var2 = keys.elementAt(1) assertEquals("var a = 0", var1) assertEquals(1, vars[var1]?.size) assertEquals("var a = 1", var2) assertEquals(0, vars[var2]?.size) } } } @Test fun `testing proper variables search in class`() { applyToCode(""" class SomeClass { val someVal = 0 fun foo(a: Int) { someVal++ } } """.trimIndent(), 0) { node, _ -> if (node.elementType == KtFileElementType.INSTANCE) { val vars = node.findAllVariablesWithUsages().mapKeys { it.key.text } val keys = vars.keys val var1 = keys.elementAt(0) assertEquals("val someVal = 0", var1) assertEquals(1, vars[var1]?.size) } } } @Test fun `testing proper variables search in class and global context`() { applyToCode(""" val someVal = 1 class SomeClass { val someVal = 0 fun foo(a: Int) { someVal++ } } """.trimIndent(), 0) { node, _ -> if (node.elementType == KtFileElementType.INSTANCE) { val vars = node.findAllVariablesWithUsages().mapKeys { it.key.text } val keys = vars.keys val var1 = keys.elementAt(0) val var2 = keys.elementAt(1) assertEquals("val someVal = 1", var1) assertEquals(0, vars[var1]?.size) assertEquals("val someVal = 0", var2) assertEquals(1, vars[var2]?.size) } } } @Test fun `testing proper variables search in a nested class that is inside of a function`() { applyToCode(""" fun foo(a: Int) { class A { var a = 5 fun foo() { println(a) } } } """.trimIndent(), 0) { node, _ -> if (node.elementType == KtFileElementType.INSTANCE) { val vars = node.findAllVariablesWithUsages().mapKeys { it.key.text } val keys = vars.keys val var1 = keys.elementAt(0) assertEquals("var a = 5", var1) assertEquals(0, vars[var1]?.size) } } } @Test fun `testing proper variables search in a nested functions`() { applyToCode(""" fun foo(a: Int) { fun foo() { var a = 5 println(a) } } """.trimIndent(), 0) { node, _ -> if (node.elementType == KtFileElementType.INSTANCE) { val vars = node.findAllVariablesWithUsages().mapKeys { it.key.text } val keys = vars.keys val var1 = keys.elementAt(0) assertEquals("var a = 5", var1) assertEquals(1, vars[var1]?.size) } } } @Test fun `testing proper variables search in class with shadowing`() { applyToCode(""" class A { var v = 0 fun foo() { v++ var v = 1 v++ } } """.trimIndent(), 0) { node, _ -> if (node.elementType == KtFileElementType.INSTANCE) { val vars = node.findAllVariablesWithUsages().mapKeys { it.key.text } val keys = vars.keys val var1 = keys.elementAt(0) val var2 = keys.elementAt(1) assertEquals("var v = 0", var1) assertEquals(1, vars[var1]?.size) assertEquals("var v = 1", var2) assertEquals(1, vars[var2]?.size) } } } @Test fun `testing proper variables search on global level`() { applyToCode(""" var v = 0 fun foo() { v++ var v = 1 v++ } class A { fun foo() { v++ var v = 2 v++ } } """.trimIndent(), 0) { node, _ -> if (node.elementType == KtFileElementType.INSTANCE) { val vars = node.findAllVariablesWithUsages().mapKeys { it.key.text } val keys = vars.keys val var1 = keys.elementAt(0) val var2 = keys.elementAt(1) val var3 = keys.elementAt(2) assertEquals("var v = 0", var1) assertEquals(2, vars[var1]?.size) assertEquals("var v = 1", var2) assertEquals(1, vars[var2]?.size) assertEquals("var v = 2", var3) assertEquals(1, vars[var3]?.size) } } } @Test @Disabled fun `testing proper variables search in companion object`() { applyToCode(""" var v = 0 class A { companion object { var v = 1 } fun foo() { v++ var v = 2 v++ } } """.trimIndent(), 0) { node, _ -> if (node.elementType == KtFileElementType.INSTANCE) { val vars = node.findAllVariablesWithUsages().mapKeys { it.key.text } val keys = vars.keys val var1 = keys.elementAt(0) val var2 = keys.elementAt(1) val var3 = keys.elementAt(1) assertEquals("var v = 0", var1) assertEquals(0, vars[var1]?.size) assertEquals("var v = 1", var2) assertEquals(1, vars[var2]?.size) assertEquals("var v = 2", var3) assertEquals(1, vars[var3]?.size) } } } @Test @Disabled fun `testing proper variables search in companion object with less priority then property`() { applyToCode(""" var v = 0 class A { companion object { var v = 1 } fun foo() { v++ var v = 2 v++ } var v = 3 } """.trimIndent(), 0) { node, _ -> if (node.elementType == KtFileElementType.INSTANCE) { val vars = node.findAllVariablesWithUsages().mapKeys { it.key.text } val keys = vars.keys val var1 = keys.elementAt(0) val var2 = keys.elementAt(1) val var3 = keys.elementAt(2) assertEquals("var v = 0", var1) assertEquals(0, vars[var1]?.size) assertEquals("var v = 1", var2) assertEquals(0, vars[var2]?.size) assertEquals("var v = 2", var3) assertEquals(1, vars[var3]?.size) assertEquals("var v = 3", var3) assertEquals(1, vars[var3]?.size) } } } @Test fun `testing proper variables search with while statement`() { applyToCode(""" class A { fun foo() { var v = 1 while(true) { v++ } v++ } } """.trimIndent(), 0) { node, _ -> if (node.elementType == KtFileElementType.INSTANCE) { val vars = node.findAllVariablesWithUsages().mapKeys { it.key.text } val keys = vars.keys val var1 = keys.elementAt(0) assertEquals("var v = 1", var1) assertEquals(2, vars[var1]?.size) } } } @Test @Disabled fun `testing proper variables search in class with the property in the end`() { applyToCode(""" class A { fun foo() { v++ } var v = 1 } """.trimIndent(), 0) { node, _ -> if (node.elementType == KtFileElementType.INSTANCE) { val vars = node.findAllVariablesWithUsages().mapKeys { it.key.text } val keys = vars.keys val var1 = keys.elementAt(0) assertEquals("var v = 1", var1) assertEquals(1, vars[var1]?.size) } } } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/ruleset/utils/WarningsGenerationTest.kt ================================================ package com.saveourtool.diktat.ruleset.utils import com.saveourtool.diktat.ruleset.constants.Warnings import org.junit.jupiter.api.Test class WarningsGenerationTest { @Test fun `checking that warnings has all proper fields filled`() { Warnings.values().forEach { warn -> assert(warn.ruleId.split(".").size == 3) } } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/util/DiktatRuleSetFactoryImplTest.kt ================================================ /** * Stub for diktat ruleset provide to be used in tests and other related utilities */ package com.saveourtool.diktat.util import com.saveourtool.diktat.api.DiktatRuleSet import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.config.DiktatRuleConfigYamlReader import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.ruleset.rules.DiktatRuleSetFactoryImpl import com.saveourtool.diktat.test.framework.util.filterContentMatches import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import java.nio.file.Path import kotlin.io.path.ExperimentalPathApi import kotlin.io.path.Path import kotlin.io.path.isRegularFile import kotlin.io.path.nameWithoutExtension import kotlin.io.path.walk class DiktatRuleSetFactoryImplTest { @OptIn(ExperimentalPathApi::class) @Suppress("UnsafeCallOnNullableType") @Test fun `check DiktatRuleSetFactoryImpl contain all rules`() { val path = "${System.getProperty("user.dir")}/src/main/kotlin/com/saveourtool/diktat/ruleset/rules" val fileNames = Path(path) .walk() .filter(Path::isRegularFile) .filterContentMatches(linesToRead = 150, Regex(""":\s*(?:Diktat)?Rule\s*\(""")) .map(Path::nameWithoutExtension) .filterNot { it in ignoredFileNames } .toList() val ruleNames = DiktatRuleSetFactoryImpl() .invoke(emptyList()) .rules .asSequence() .map { it::class.simpleName } .filterNotNull() .filterNot { it in ignoredRuleNames } .toList() assertThat(fileNames).isNotEmpty assertThat(ruleNames).isNotEmpty assertThat(ruleNames.sorted()).containsExactlyElementsOf(fileNames.sorted()) } companion object { private val ignoredFileNames = listOf( "DiktatRule", "OrderedRuleSet", ) private val ignoredRuleNames = listOf( "DummyWarning", ) /** * Simple method to emulate [DiktatRuleSet] to inject `.yml` rule configuration and mock this part of code. */ internal fun diktatRuleSetForTest( ruleSupplier: (rulesConfigList: List) -> DiktatRule, rulesConfigList: List?, ): DiktatRuleSet = run { rulesConfigList ?: Companion::class.java.classLoader.getResourceAsStream("diktat-analysis.yml") ?.let { DiktatRuleConfigYamlReader().invoke(it) } .orEmpty() } .let(ruleSupplier) .let { DiktatRuleSet(listOf(it)) } } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/util/DiktatRuleTest.kt ================================================ package com.saveourtool.diktat.util import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.BLANK_LINE_BETWEEN_PROPERTIES import com.saveourtool.diktat.ruleset.constants.Warnings.WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES import com.saveourtool.diktat.ruleset.rules.chapter3.ClassLikeStructuresOrderRule import com.saveourtool.diktat.api.DiktatError import org.junit.jupiter.api.Test class DiktatRuleTest : LintTestBase(::ClassLikeStructuresOrderRule) { private val ruleId = "$DIKTAT_RULE_SET_ID:${ClassLikeStructuresOrderRule.NAME_ID}" private val codeTemplate = """ |class Example { | private val FOO = 42 | private val log = LoggerFactory.getLogger(Example.javaClass) | // blank line between property | private val some = 2 |} """.trimMargin() private val rulesConfigAllDisabled = listOf( RulesConfig(BLANK_LINE_BETWEEN_PROPERTIES.name, enabled = false), RulesConfig(WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES.name, enabled = false) ) private val rulesConfigOneRuleIsEnabled = listOf( RulesConfig(BLANK_LINE_BETWEEN_PROPERTIES.name, enabled = true), RulesConfig(WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES.name, enabled = false) ) @Test fun `check that if all inspections are disabled then rule won't run`() { lintMethod(codeTemplate, rulesConfigList = rulesConfigAllDisabled) } @Test fun `check that if one inspection is enabled then rule will run`() { lintMethod(codeTemplate, DiktatError(4, 4, ruleId, "${BLANK_LINE_BETWEEN_PROPERTIES.warnText()} some", true), rulesConfigList = rulesConfigOneRuleIsEnabled ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/util/FixTestBase.kt ================================================ package com.saveourtool.diktat.util import com.saveourtool.diktat.api.DiktatCallback import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ktlint.format import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.test.framework.processing.ResourceReader import com.saveourtool.diktat.test.framework.processing.TestComparatorUnit import com.saveourtool.diktat.test.framework.processing.TestFileContent import com.saveourtool.diktat.util.DiktatRuleSetFactoryImplTest.Companion.diktatRuleSetForTest import io.github.oshai.kotlinlogging.KotlinLogging import org.intellij.lang.annotations.Language import java.nio.file.Path import kotlin.io.path.bufferedWriter import kotlin.io.path.createDirectories import kotlin.io.path.div /** * Base class for FixTest */ open class FixTestBase( resourceFilePath: String, ruleSupplier: (rulesConfigList: List) -> DiktatRule, defaultRulesConfigList: List? = null, cb: DiktatCallback = defaultCallback, ) { /** * testComparatorUnit */ private val testComparatorUnitSupplier = { overrideRulesConfigList: List? -> TestComparatorUnit( resourceFilePath = resourceFilePath, function = { testFile -> format( ruleSetSupplier = { diktatRuleSetForTest(ruleSupplier, overrideRulesConfigList ?: defaultRulesConfigList) }, file = testFile, cb = cb, ) }, ) } /** * @param expectedPath path to file with expected result, relative to [resourceFilePath] * @param testPath path to file with code that will be transformed by formatter, relative to [resourceFilePath] * @param overrideRulesConfigList optional override to [defaultRulesConfigList] * @param overrideResourceReader function to override [ResourceReader] to read resource content. * @see fixAndCompareContent */ protected fun fixAndCompare( expectedPath: String, testPath: String, overrideRulesConfigList: List? = null, overrideResourceReader: (ResourceReader) -> ResourceReader = { it }, ) { val testComparatorUnit = testComparatorUnitSupplier(overrideRulesConfigList) val result = testComparatorUnit .compareFilesFromResources(expectedPath, testPath, overrideResourceReader) result.assertSuccessful() } /** * Unlike [fixAndCompare], this method doesn't perform any assertions. * * @param actualContent the original file content (may well be modified as * fixes are applied). * @param expectedContent the content the file is expected to have after the * fixes are applied. * @param tempDir the temporary directory (usually injected by _JUnit_). * @param overrideRulesConfigList an optional override for [rulesConfigList] * (the class-wide configuration). * @return the result of file content comparison. * @see fixAndCompare */ @Suppress("FUNCTION_BOOLEAN_PREFIX") protected fun fixAndCompareContent( @Language("kotlin") actualContent: String, @Language("kotlin") expectedContent: String = actualContent, tempDir: Path, subFolder: String? = null, overrideRulesConfigList: List? = null ): TestFileContent { val folder = subFolder?.let { tempDir / it }?.also { it.createDirectories() } ?: tempDir val actual = folder / "actual.kt" actual.bufferedWriter().use { out -> out.write(actualContent) } val expected = folder / "expected.kt" expected.bufferedWriter().use { out -> out.write(expectedContent) } val testComparatorUnit = testComparatorUnitSupplier(overrideRulesConfigList) return testComparatorUnit .compareFilesFromFileSystem(expected, actual) } companion object { private val log = KotlinLogging.logger { } private val defaultCallback = DiktatCallback { error, _ -> log.warn { "Received linting error: $error" } } } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/util/LintTestBase.kt ================================================ package com.saveourtool.diktat.util import com.saveourtool.diktat.api.DiktatCallback import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ktlint.check import com.saveourtool.diktat.ruleset.rules.DiktatRule import com.saveourtool.diktat.util.DiktatRuleSetFactoryImplTest.Companion.diktatRuleSetForTest import com.saveourtool.diktat.api.DiktatError import org.assertj.core.api.Assertions.assertThat import org.intellij.lang.annotations.Language import java.nio.file.Path import kotlin.io.path.createDirectories import kotlin.io.path.readText import kotlin.io.path.writeText /** * Base class for testing rules without fixing code. * @property ruleSupplier mapping of list of [RulesConfig] into a [DiktatRule] * @property rulesConfigList optional custom rules config */ open class LintTestBase(private val ruleSupplier: (rulesConfigList: List) -> DiktatRule, private val rulesConfigList: List? = null) { /** * Perform linting of [code], collect errors and compare with [expectedLintErrors] * * @param code code to check * @param expectedLintErrors expected errors * @param rulesConfigList optional override for `this.rulesConfigList` * @see lintResult */ fun lintMethod(@Language("kotlin") code: String, vararg expectedLintErrors: DiktatError, rulesConfigList: List? = null, ) = doAssert( actualLintErrors = lintResult(code, rulesConfigList), description = "lint result for \"$code\"", expectedLintErrors = expectedLintErrors ) /** * Perform linting of [code] by creating a file in [tempDir] with [fileName], collect errors and compare with [expectedLintErrors] * * @param code code to check * @param tempDir a path to temporary folder * @param fileName relative path to file which needs to be checked * @param expectedLintErrors expected errors * @param rulesConfigList optional override for `this.rulesConfigList` * @see lintResult */ fun lintMethodWithFile( @Language("kotlin") code: String, tempDir: Path, fileName: String, vararg expectedLintErrors: DiktatError, rulesConfigList: List? = null, ) { val file = tempDir.resolve(fileName).also { it.parent.createDirectories() it.writeText(code) } lintMethodWithFile( file = file, expectedLintErrors = expectedLintErrors, rulesConfigList = rulesConfigList, ) } /** * Perform linting of [file], collect errors and compare with [expectedLintErrors] * * @param file a path to file to check * @param expectedLintErrors expected errors * @param rulesConfigList optional override for `this.rulesConfigList` * @see lintResult */ fun lintMethodWithFile( file: Path, vararg expectedLintErrors: DiktatError, rulesConfigList: List? = null, ) = doAssert( actualLintErrors = lintResult(file, rulesConfigList), description = "lint result for \"${file.readText()}\"", expectedLintErrors = expectedLintErrors ) private fun doAssert( actualLintErrors: List, description: String, vararg expectedLintErrors: DiktatError, ) { when { expectedLintErrors.size == 1 && actualLintErrors.size == 1 -> { val actual = actualLintErrors[0] val expected = expectedLintErrors[0] assertThat(actual) .describedAs(description) .isEqualTo(expected) assertThat(actual.canBeAutoCorrected) .describedAs("canBeAutoCorrected") .isEqualTo(expected.canBeAutoCorrected) } else -> assertThat(actualLintErrors) .describedAs(description) .apply { when { expectedLintErrors.isEmpty() -> isEmpty() else -> containsExactly(*expectedLintErrors) } } } } /** * Lints the [file] and returns the errors collected, but (unlike * [lintMethodWithFile]) doesn't make any assertions. * * @param file the file to check. * @param rulesConfigList an optional override for `this.rulesConfigList`. * @return the list of lint errors. * @see lintMethodWithFile */ private fun lintResult( file: Path, rulesConfigList: List? = null, ): List { val lintErrors: MutableList = mutableListOf() check( ruleSetSupplier = { rulesConfigList.toDiktatRuleSet() }, file = file, cb = lintErrors.collector(), ) return lintErrors } /** * Lints the [code] and returns the errors collected, but (unlike * [lintMethodWithFile]) doesn't make any assertions. * * @param code the code to check. * @param rulesConfigList an optional override for `this.rulesConfigList`. * @return the list of lint errors. * @see lintMethodWithFile */ protected fun lintResult( @Language("kotlin") code: String, rulesConfigList: List? = null, ): List { val lintErrors: MutableList = mutableListOf() check( ruleSetSupplier = { rulesConfigList.toDiktatRuleSet() }, text = code, cb = lintErrors.collector(), ) return lintErrors } private fun List?.toDiktatRuleSet() = diktatRuleSetForTest(ruleSupplier, this ?: rulesConfigList) companion object { private fun MutableList.collector(): DiktatCallback = DiktatCallback { error, _ -> this += error } } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/util/SuppressingTest.kt ================================================ package com.saveourtool.diktat.util import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.common.config.rules.RulesConfig import com.saveourtool.diktat.ruleset.constants.Warnings.IDENTIFIER_LENGTH import com.saveourtool.diktat.ruleset.rules.chapter1.IdentifierNaming import com.saveourtool.diktat.api.DiktatError import org.junit.jupiter.api.Test class SuppressingTest : LintTestBase(::IdentifierNaming) { private val ruleId: String = "$DIKTAT_RULE_SET_ID:${IdentifierNaming.NAME_ID}" private val rulesConfigBooleanFunctions: List = listOf( RulesConfig(IDENTIFIER_LENGTH.name, true, emptyMap(), setOf("MySuperSuppress")) ) @Test fun `checking that suppression with ignoredAnnotation works`() { val code = """ @MySuperSuppress() fun foo() { val a = 1 } """.trimIndent() lintMethod(code, rulesConfigList = rulesConfigBooleanFunctions) } @Test fun `checking that suppression with ignore everything works`() { val code = """ @Suppress("diktat") fun foo() { val a = 1 } """.trimIndent() lintMethod(code) } @Test fun `checking that suppression with a targeted inspection name works`() { val code = """ @Suppress("IDENTIFIER_LENGTH") fun foo() { val a = 1 } """.trimIndent() lintMethod(code) } @Test fun `negative scenario for other annotation`() { val code = """ @MySuperSuppress111() fun foo() { val a = 1 } """.trimIndent() lintMethod( code, DiktatError(3, 9, ruleId, "[IDENTIFIER_LENGTH] identifier's length is incorrect, it" + " should be in range of [2, 64] symbols: a", false), rulesConfigList = rulesConfigBooleanFunctions, ) } } ================================================ FILE: diktat-rules/src/test/kotlin/com/saveourtool/diktat/util/TestUtils.kt ================================================ /** * Utility classes and methods for tests */ package com.saveourtool.diktat.util import com.saveourtool.diktat.api.DiktatErrorEmitter import com.saveourtool.diktat.api.DiktatRule import com.saveourtool.diktat.api.DiktatRuleSet import com.saveourtool.diktat.ktlint.check import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.fail import org.intellij.lang.annotations.Language import org.jetbrains.kotlin.com.intellij.lang.ASTNode import java.io.Reader import java.util.concurrent.atomic.AtomicInteger import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind.EXACTLY_ONCE import kotlin.contracts.contract internal const val TEST_FILE_NAME = "TestFileName.kt" /** * Casts a nullable value to a non-`null` one, similarly to the `!!` * operator. * * @param lazyFailureMessage the message to evaluate in case of a failure. * @return a non-`null` value. */ @OptIn(ExperimentalContracts::class) internal fun T?.assertNotNull(lazyFailureMessage: () -> String = { "Expecting actual not to be null" }): T { contract { returns() implies (this@assertNotNull != null) } return this ?: fail(lazyFailureMessage()) } /** * This utility function lets you run arbitrary code on every node of given [code]. * It also provides you with counter which can be incremented inside [applyToNode] and then will be compared to [expectedAsserts]. * This allows you to keep track of how many assertions have actually been run on your code during tests. * * @param code * @param expectedAsserts Number of expected times of assert invocation * @param applyToNode Function to be called on each AST node, should increment counter if assert is called */ @Suppress("TYPE_ALIAS") internal fun applyToCode(@Language("kotlin") code: String, expectedAsserts: Int, applyToNode: (node: ASTNode, counter: AtomicInteger) -> Unit ) { val counter = AtomicInteger(0) check( ruleSetSupplier = { DiktatRuleSet(listOf(object : DiktatRule { override val id: String get() = "astnode-utils-test" override fun invoke(node: ASTNode, autoCorrect: Boolean, emitter: DiktatErrorEmitter) { applyToNode(node, counter) } })) }, text = code, ) assertThat(counter.get()) .`as`("Number of expected asserts") .isEqualTo(expectedAsserts) } ================================================ FILE: diktat-rules/src/test/resources/log4j2.properties ================================================ rootLogger.level = info rootLogger.appenderRef.stdout.ref = STDOUT appender.stdout.type = Console appender.stdout.name = STDOUT appender.stdout.target = SYSTEM_OUT appender.stdout.layout.type = PatternLayout appender.stdout.layout.pattern = [%-5p] %d{yyyy-MM-dd HH:mm:ss} %m%n ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/abstract_classes/ShouldReplaceAbstractKeywordExpected.kt ================================================ package test.paragraph6.abstract_classes actual open class CoroutineTest actual constructor() { actual fun runTest(block: suspend CoroutineScope.() -> T) { runBlocking { block() } } } open class Some() { fun some(){} fun another(){} @SomeAnnotation @Another open inner class Any { fun func(){} } inner open class Second { fun someFunc(){} } } abstract class Another { abstract fun absFunc() fun someFunc(){} } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/abstract_classes/ShouldReplaceAbstractKeywordTest.kt ================================================ package test.paragraph6.abstract_classes actual abstract class CoroutineTest actual constructor() { actual fun runTest(block: suspend CoroutineScope.() -> T) { runBlocking { block() } } } abstract class Some() { fun some(){} fun another(){} @SomeAnnotation @Another abstract inner class Any { fun func(){} } inner abstract class Second { fun someFunc(){} } } abstract class Another { abstract fun absFunc() fun someFunc(){} } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/classes/AssignmentWithLocalPropertyExpected.kt ================================================ package test.chapter6.classes class Foo (a: Int){ val a: Int init { val f = F(a) this.a = f.foo() } } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/classes/AssignmentWithLocalPropertyTest.kt ================================================ package test.chapter6.classes class Foo { val a: Int constructor(a: Int) { val f = F(a) this.a = f.foo() } } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/classes/ConstructorShouldKeepExpressionsOrderExpected.kt ================================================ package test.chapter6.classes class Test (){ init { str = "ABC" println(str) str = str.reversed() println(str) } var str: String } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/classes/ConstructorShouldKeepExpressionsOrderTest.kt ================================================ package test.chapter6.classes class Test { constructor() { str = "ABC" println(str) str = str.reversed() println(str) } var str: String } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/classes/ConstructorWithCommentsExpected.kt ================================================ package test.chapter6.classes class Test (){ init { println("A") // import is a weak keyword imported println("B") /* import is a weak keyword */ println("C") /** * import is a weak keyword */ println("D") } } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/classes/ConstructorWithCommentsTest.kt ================================================ package test.chapter6.classes class Test { constructor() { println("A") // import is a weak keyword imported println("B") /* import is a weak keyword */ println("C") /** * import is a weak keyword */ println("D") } } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/classes/ConstructorWithComplexAssignmentsExpected.kt ================================================ package test.chapter6.classes class A { var b: String = "" constructor(a: Int) { // help b = a.toString() } fun foo1() { } // ssss fun foo() { } } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/classes/ConstructorWithComplexAssignmentsTest.kt ================================================ package test.chapter6.classes class A { var b: String = "" constructor(a: Int) { // help b = a.toString() } fun foo1() { } // ssss fun foo() { } } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/classes/ConstructorWithCustomAssignmentsExpected.kt ================================================ package test.chapter6.classes class Test (a: Int){ var a: Int init { this.a = a + 42 } } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/classes/ConstructorWithCustomAssignmentsTest.kt ================================================ package test.chapter6.classes class Test { var a: Int constructor(a: Int) { this.a = a + 42 } } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/classes/ConstructorWithInitExpected.kt ================================================ package test.chapter6.classes class Test (var a: Int){ init { println("Lorem ipsum") foo() } } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/classes/ConstructorWithInitTest.kt ================================================ package test.chapter6.classes class Test { var a: Int constructor(a: Int) { println("Lorem ipsum") foo() this.a = a } } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/classes/ConstructorWithModifiersExpected.kt ================================================ package test.chapter6.classes class Test @Annotation private constructor (var a: Int){ } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/classes/ConstructorWithModifiersTest.kt ================================================ package test.chapter6.classes class Test { var a: Int @Annotation private constructor(a: Int) { this.a = a } } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/classes/SimpleConstructorExpected.kt ================================================ package test.chapter6.classes class Test (var a: Int){ } class Test (var a: Int){ } class Test (var a: Int){ init { var a = 14 } } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/classes/SimpleConstructorTest.kt ================================================ package test.chapter6.classes class Test { var a: Int constructor(a: Int) { this.a = a } } class Test { var a: Int constructor(_a: Int) { a = _a } } class Test { var a: Int constructor(_a: Int) { var a = 14 a = _a this.a = _a } } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/compact_initialization/ApplyOnStatementsWithThisKeywordExpected.kt ================================================ fun String.createPluginConfig() { val pluginConfig = TomlDecoder.decode( serializer(), fakeFileNode, DecoderConf() ).apply { prop1 = property1 // comment2 prop2 = property2} pluginConfig.configLocation = this.toPath() // comment1 pluginConfig.configLocation2 = this.toPath() } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/compact_initialization/ApplyOnStatementsWithThisKeywordTest.kt ================================================ fun String.createPluginConfig() { val pluginConfig = TomlDecoder.decode( serializer(), fakeFileNode, DecoderConf() ) pluginConfig.configLocation = this.toPath() pluginConfig.prop1 = property1 // comment1 pluginConfig.configLocation2 = this.toPath() // comment2 pluginConfig.prop2 = property2 } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/compact_initialization/ApplyWithValueArgumentExpected.kt ================================================ fun main() { val httpClient = HttpClient("myConnection").apply { setDefaultUrl(this) port = "8080" timeout = 100 } httpClient.doRequest() } fun setDefaultUrl(httpClient: HttpClient) { httpClient.url = "http://example.com" } fun foo() { val diktatExtension = project.extensions.create(DIKTAT_EXTENSION, DiktatExtension::class.java).apply { inputs = project.fileTree("src").apply { include("**/*.kt") } reporter = PlainReporter(System.out)} } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/compact_initialization/ApplyWithValueArgumentTest.kt ================================================ fun main() { val httpClient = HttpClient("myConnection").apply(::setDefaultUrl) httpClient.port = "8080" httpClient.timeout = 100 httpClient.doRequest() } fun setDefaultUrl(httpClient: HttpClient) { httpClient.url = "http://example.com" } fun foo() { val diktatExtension = project.extensions.create(DIKTAT_EXTENSION, DiktatExtension::class.java) diktatExtension.inputs = project.fileTree("src").apply { include("**/*.kt") } diktatExtension.reporter = PlainReporter(System.out) } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/compact_initialization/ExampleWithCommentsExpected.kt ================================================ fun main() { val httpClient = HttpClient("myConnection").apply { // assigning URL url = "http://example.com" // setting port to 8080 port = "8080" /* we set timeout in case it times out */ timeout = 100} // finally, we can make request httpClient.doRequest() } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/compact_initialization/ExampleWithCommentsTest.kt ================================================ fun main() { val httpClient = HttpClient("myConnection") // assigning URL httpClient.url = "http://example.com" // setting port to 8080 httpClient.port = "8080" /* we set timeout in case it times out */ httpClient.timeout = 100 // finally, we can make request httpClient.doRequest() } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/compact_initialization/ParenthesizedReceiverExpected.kt ================================================ fun `translate text`() { val res = translateText(text = "dummy") (res is TranslationsSuccess) shouldBe true val translationsSuccess = (res as TranslationsSuccess).apply { translations = 1} } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/compact_initialization/ParenthesizedReceiverTest.kt ================================================ fun `translate text`() { val res = translateText(text = "dummy") (res is TranslationsSuccess) shouldBe true val translationsSuccess = res as TranslationsSuccess translationsSuccess.translations = 1 } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/compact_initialization/SimpleExampleExpected.kt ================================================ fun main() { val httpClient = HttpClient("myConnection").apply { url = "http://example.com" port = "8080" timeout = 100} httpClient.doRequest() } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/compact_initialization/SimpleExampleTest.kt ================================================ fun main() { val httpClient = HttpClient("myConnection") httpClient.url = "http://example.com" httpClient.port = "8080" httpClient.timeout = 100 httpClient.doRequest() } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/compact_initialization/StatementUseFieldMultipleTimesExpected.kt ================================================ fun foo() { val execution = Execution().apply { id = executionService.saveExecution(this)} return execution.id!! } fun foo() { val execution = Execution().apply { id = executionService.saveExecution(this) sdk = this.defaultSdk(this) id2 = this.id + shift name = this.execution(this)} return execution.id!! } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/compact_initialization/StatementUseFieldMultipleTimesTest.kt ================================================ fun foo() { val execution = Execution() execution.id = executionService.saveExecution(execution) return execution.id!! } fun foo() { val execution = Execution() execution.id = executionService.saveExecution(execution) execution.sdk = execution.defaultSdk(execution) execution.id2 = execution.id + shift execution.name = execution.execution(execution) return execution.id!! } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/init_blocks/InitBlockWithAssignmentsExpected.kt ================================================ package test.chapter6.init_blocks class A(baseUrl: String) { private val customUrl: String = "$baseUrl/myUrl" } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/init_blocks/InitBlockWithAssignmentsTest.kt ================================================ package test.chapter6.init_blocks class A(baseUrl: String) { private val customUrl: String init { customUrl = "$baseUrl/myUrl" } } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/init_blocks/InitBlocksExpected.kt ================================================ package test.chapter6.init_blocks class Example { init { println("Lorem ipsum") println("Dolor sit amet") } val foo = 0 } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/init_blocks/InitBlocksTest.kt ================================================ package test.chapter6.init_blocks class Example { init { println("Lorem ipsum") } val foo = 0 init { println("Dolor sit amet") } } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/init_blocks/InitBlocksWithAssignmentsExpected.kt ================================================ package test.chapter6.init_blocks class A(baseUrl: String) { private val customUrl: String = "$baseUrl/myUrl" init { println("Lorem ipsum") println("Dolor sit amet") } } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/init_blocks/InitBlocksWithAssignmentsTest.kt ================================================ package test.chapter6.init_blocks class A(baseUrl: String) { private val customUrl: String init { customUrl = "$baseUrl/myUrl" println("Lorem ipsum") } init { println("Dolor sit amet") } } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/lastIndex_change/IncorrectUseLengthMinusOneExpected.kt ================================================ package test.chapter6.lastIndex_change fun main(args: Array) { val str = "ASDFG" val A = str.lastIndex val B = str.lastIndex } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/lastIndex_change/IncorrectUseLengthMinusOneTest.kt ================================================ package test.chapter6.lastIndex_change fun main(args: Array) { val str = "ASDFG" val A = str.length - 1 val B = str.length - 1 } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/lastIndex_change/UseAnyWhiteSpacesExpected.kt ================================================ package test.chapter6.lastIndex_change fun main(args: Array) { val str = "ASDFG" val A = str.lastIndex val B = str.lastIndex val C = str.lastIndex val D = str.lastIndex val F = str.lastIndex val E = str[str.lastIndex] val G = str[str.lastIndex ] } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/lastIndex_change/UseAnyWhiteSpacesTest.kt ================================================ package test.chapter6.lastIndex_change fun main(args: Array) { val str = "ASDFG" val A = str.length - 1 val B = str.length-1 val C = str.length -1 val D = str.length- 1 val F = str.length - 1 val E = str[str.length - 1] val G = str[str.length - 1 ] } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/primary_constructor/EmptyPCExpected.kt ================================================ package test.chapter6.primary_constructor class Test { var a: Int = 0 var b: Int = 0 } class Test { var a = "Property" init { println("some init") } constructor(a: String): this() { this.a = a } } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/primary_constructor/EmptyPCTest.kt ================================================ package test.chapter6.primary_constructor class Test() { var a: Int = 0 var b: Int = 0 } class Test() { var a = "Property" init { println("some init") } constructor(a: String): this() { this.a = a } } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/properties/TrivialPropertyAccessorsExpected.kt ================================================ package test.chapter6.properties class Some { var prop: Int = 0 val prop2: Int = 0 var propNotChange: Int = 7 get() { return someCoolLogic(field) } set(value) { anotherCoolLogic(value) } var testName: String? = null private set val x = 0 } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/properties/TrivialPropertyAccessorsTest.kt ================================================ package test.chapter6.properties class Some { var prop: Int = 0 get() = field set(value) { field = value } val prop2: Int = 0 get() { return field } var propNotChange: Int = 7 get() { return someCoolLogic(field) } set(value) { anotherCoolLogic(value) } var testName: String? = null private set val x = 0 get } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/script/SimpleRunInScriptExpected.kts ================================================ run { println("hello world!") } fun foo() { println() } val q = Config() run { println("a") } also { println("a") } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/script/SimpleRunInScriptTest.kts ================================================ println("hello world!") fun foo() { println() } val q = Config() run { println("a") } also { println("a") } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/stateless_classes/StatelessClassExpected.kt ================================================ package test.chapter6.stateless_classes interface I { fun foo() } object O: I { override fun foo() {} } /** * Some KDOC */ object A: I { override fun foo() {} } ================================================ FILE: diktat-rules/src/test/resources/test/chapter6/stateless_classes/StatelessClassTest.kt ================================================ package test.chapter6.stateless_classes interface I { fun foo() } class O: I { override fun foo() {} } /** * Some KDOC */ class A: I { override fun foo() {} } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/class_/IncorrectClassNameExpected.kt ================================================ package com.saveourtool.diktat.test.paragraph1.naming.class_ class PaScalCase1 {} class PascalCase2 {} class PascalCase3 {} class PascalCase4 {} class Pascalcase5 {} class Pascalcase6 {} class PascAlCase7 {} class PascaLcase8 {} class PascAlCase9 {} class PascAlCase10 {} class PascAlCase11 {} ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/class_/IncorrectClassNameTest.kt ================================================ package com.saveourtool.diktat.test.paragraph1.naming.class_ class PaScalCase1 {} class Pascal_Case2 {} class Pascal_case3 {} class Pascal_Case4 {} class Pascalcase5 {} class Pascalcase6 {} class PascAl_Case7 {} class PascaL_Case8 {} class PascAL_Case9 {} class _PascAL_Case10 {} class PascAL_Case11_ {} ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/enum_/EnumValuePascalCaseExpected.kt ================================================ package com.saveourtool.diktat.test.resources.test.paragraph1.naming.enum_ enum class EnumValuePascalCaseTest { PaScSalL, PascAslF, StartPsaaaDfe, NameMyaSayR, NameMyaSayR } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/enum_/EnumValuePascalCaseTest.kt ================================================ package com.saveourtool.diktat.test.resources.test.paragraph1.naming.enum_ enum class EnumValuePascalCaseTest { paSC_SAl_l, PascAsl_f, START_PSaaa_DFE, _NAme_MYa_sayR, NAme_MYa_sayR_ } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/enum_/EnumValueSnakeCaseExpected.kt ================================================ package com.saveourtool.diktat.test.resources.test.paragraph1.naming.enum_ enum class EnumValueSnakeCaseTest { PA_SC_SAL_L, PASC_ASL_F, START_PSAAA_DFE, NAME_MYA_SAY_R, NAME_MYA_SAY_R_ } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/enum_/EnumValueSnakeCaseTest.kt ================================================ package com.saveourtool.diktat.test.resources.test.paragraph1.naming.enum_ enum class EnumValueSnakeCaseTest { paSC_SAl_l, PascAsl_f, START_PSaaa_DFE, _NAme_MYa_sayR, NAme_MYa_sayR_ } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/file/fileNameTest.kt ================================================ package com.saveourtool.diktat.resources.test.paragraph1.naming.file.resources.test.paragraph1.naming.file.naming.file fun foo () { } class FileNameTest2 { } class FileNameTest3 { } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/file/file_nameTest.kt ================================================ package com.saveourtool.diktat.resources.test.paragraph1.naming.file.resources.test.paragraph1.naming.file.resources.test.paragraph1.naming.file.naming.file class FileNameTest1 { } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/function/FunctionNameExpected.kt ================================================ package com.saveourtool.diktat.ktlint.ruleset.standarddd import com.saveourtool.diktat.ktlint.CORE.Rule /** * Alphabetical with capital letters before lower case letters (e.g. Z before a). * No blank lines between major groups (android, com, junit, net, org, java, javax). * Single group regardless of import type. * * https://developer.android.com/kotlin/style-guide#import_statements */ class TestPackageName { fun METHOD1() { } fun method_two() { } fun methODTREE() { } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/function/FunctionNameTest.kt ================================================ package com.saveourtool.diktat.ktlint.ruleset class TestPackageName { fun /* */ METHOD1(someName: String): Unit { } fun /* */ method_two(someName: String): Unit { fun other_method_inside(): Boolean { return false } } fun /* */ methODTREE(someName: String) { } } // incorrect case fun /* */ String.STRMETHOD1(someName: String): Unit { } // incorrect case fun /* */ String.str_method_two(someName: String): Unit { } // incorrect case fun /* */ String.strMethODTREE(): String { return "" } // should be corrected to isValidIdentifier fun /* */ String.validIdentifier(): Boolean { return false } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/generic/GenericFunctionExpected.kt ================================================ package com.saveourtool.diktat.test.paragraph1.naming.generic private class ClassName { private fun lock(body: ((Template?) -> T?)?, value: Template?): T? { try { val variableName: Template? = null val variableT: T? = null println(variableT) return body!!(variableName) } finally { println() } } fun foo(var1: T, var2: ((T?) -> T?)?) { lock(var2, var1) } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/generic/GenericFunctionTest.kt ================================================ package com.saveourtool.diktat.test.paragraph1.naming.generic private class ClassName { private fun lock(body: ((Template?) -> T?)?, value: Template?): T? { try { val variableName: Template? = null val variableT: T? = null println(variableT) return body!!(variableName) } finally { println() } } fun foo(var1: T, var2: ((T?) -> T?)?) { lock(var2, var1) } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/identifiers/ConstantValNameExpected.kt ================================================ package test.paragraph1.naming.identifiers private const val PASCAL_CASE = "" private const val LOWER = "" private const val LOWER_CAMEL = "" private const val LOWER_SNAKE = "" private const val AM_CONSTANT = "" // be careful - we have also replaced a prefix here private const val CODE = "" // prefix "x" - was removed private const val AM_CONSTANT1 = "" private const val STRANGE_NAME = "" private const val MY_STR_ANGE_NAME = "" ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/identifiers/ConstantValNameTest.kt ================================================ package test.paragraph1.naming.identifiers private const val PascalCase = "" private const val lower = "" private const val lowerCamel = "" private const val lower_snake = "" private const val iAmConstant = "" // be careful - we have also replaced a prefix here private const val xCode = "" // prefix "x" - was removed private const val I_AM_CONSTANT1 = "" private const val STRANGE_name = "" private const val My_strAngeName = "" ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/identifiers/IdentifierNameRegressionExpected.kt ================================================ package io.reflekt.plugin.utils object Util { private val getUses: String } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/identifiers/IdentifierNameRegressionTest.kt ================================================ package io.reflekt.plugin.utils object Util { private val GET_USES: String } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/identifiers/LambdaArgExpected.kt ================================================ package test.paragraph1.naming.identifiers private fun checkCommentedCode(node: ASTNode) { val eolCommentsOffsetToText = "" val blockCommentsOffsetToText = "" (eolCommentsOffsetToText + blockCommentsOffsetToText) .map { (strangecase, text) -> "" } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/identifiers/LambdaArgTest.kt ================================================ package test.paragraph1.naming.identifiers private fun checkCommentedCode(node: ASTNode) { val eolCommentsOffsetToText = "" val blockCommentsOffsetToText = "" (eolCommentsOffsetToText + blockCommentsOffsetToText) .map { (STRANGECASE, text) -> "" } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/identifiers/PrefixInNameExpected.kt ================================================ package test.paragraph1.naming.identifiers const val GLOB = "" val prefix = "" ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/identifiers/PrefixInNameTest.kt ================================================ package test.paragraph1.naming.identifiers const val M_GLOB = "" val aPrefix = "" ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/identifiers/PropertyInKdocExpected.kt ================================================ package test.paragraph1.naming.identifiers /** * @property anAbcMember * @property another_abc_member * @property anDefMember * @property anotherDefMember */ data class Abc( private val anAbcMember: String, val another_abc_member: String, ) { private val anDefMember: String = "" private val anotherDefMember: String = "" } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/identifiers/PropertyInKdocTest.kt ================================================ package test.paragraph1.naming.identifiers /** * @property an_abc_member * @property another_abc_member * @property an_def_member * @property another_def_member */ data class abc( private val an_abc_member: String, val another_abc_member: String, ) { private val an_def_member: String = "" private val another_def_member: String = "" } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/identifiers/TypeAliasNameExpected.kt ================================================ package test.paragraph1.naming.identifiers class TypeAliasName { typealias RelatedClasses = List> } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/identifiers/TypeAliasNameTest.kt ================================================ package test.paragraph1.naming.identifiers class TypeAliasName { typealias relatedClasses = List> } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/identifiers/VariableNamingExpected.kt ================================================ package test.paragraph1.naming.identifiers private var pascalCase = "" private var upper = "" private var upperSnake = "" private var lowerSnake = "" private val amConstant1 = "" private val strangeName = "" private val loWerValue = "" private val lower = "" private val valNx256 = "" private val voiceIpPort = "" class A { private val voiceIpPort = "" public val valN_x2567 = "" fun foo() { val voiceIpPorts = "" val upperSnakers = voiceIpPort qww(voiceIpPorts) } fun goo() { val qwe = lowerSnake val pre = valNx256 } } class B { companion object { val QQ = 1 private val qwe = 11 } fun foo() { var qq = 20 while (qq < 20) { qq = 10 } } class Ba { fun goo() { val qwe = QQ } } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/identifiers/VariableNamingTest.kt ================================================ package test.paragraph1.naming.identifiers private var PascalCase = "" private var UPPER = "" private var UPPER_SNAKE = "" private var lower_snake = "" private val I_AM_CONSTANT1 = "" private val STRANGE_name = "" private val loWer_VAlue = "" private val lower = "" private val ValN_x256 = "" private val VoiceIP_port = "" class A { private val VoiceIP_port = "" public val valN_x2567 = "" fun foo() { val VoiceIP_ports = "" val UPPER_SNAKERS = VoiceIP_port qww(VoiceIP_ports) } fun goo() { val qwe = lower_snake val pre = ValN_x256 } } class B { companion object { val QQ = 1 private val QWE = 11 } fun foo() { var QQ = 20 while (QQ < 20) { QQ = 10 } } class BA { fun goo() { val qwe = QQ } } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/object_/IncorrectObjectNameExpected.kt ================================================ package com.saveourtool.diktat.test.resources.test.paragraph1.naming.object_ object PaScalCase1 {} object PascalCase2 {} object PascalCase3 {} object PascalCase4 {} object Pascalcase5 {} object Pascalcase6 {} object PascAlCase7 {} object PascaLcase8 {} object PascAlCase9 {} ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/object_/IncorrectObjectNameTest.kt ================================================ package com.saveourtool.diktat.test.resources.test.paragraph1.naming.object_ object PaScalCase1 {} object Pascal_Case2 {} object Pascal_case3 {} object Pascal_Case4 {} object Pascalcase5 {} object Pascalcase6 {} object PascAl_Case7 {} object PascaL_Case8 {} object PascAL_Case9 {} ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/package/FixUnderscoreExpected.kt ================================================ package /* AAAAAA */ com.saveourtool.diktat.ktlint.ruleset.standarddd import com.saveourtool.diktat.ktlint.CORE.Rule /** * some comments * */ class TestPackageName { var dfsGGGG ="" } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/package/FixUnderscoreTest.kt ================================================ package /* AAAAAA */ com.saveourtool.diktat.ktlint.rule_set.standarddd import com.saveourtool.diktat.ktlint.CORE.Rule /** * some comments * */ class TestPackageName { var dfsGGGG ="" } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/package/FixUpperExpected.kt ================================================ package /* AAAAAA */ com.saveourtool.diktat.ktlint.ruleset.standarddd import com.saveourtool.diktat.ktlint.CORE.Rule /** * some comments * */ class TestPackageName { var dfsGGGG ="" } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/package/FixUpperTest.kt ================================================ package /* AAAAAA */ com.saveourtool.diktat.ktlint.ruleset.standarDDD import com.saveourtool.diktat.ktlint.CORE.Rule /** * some comments * */ class TestPackageName { var dfsGGGG ="" } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/package/MissingDomainNameExpected.kt ================================================ package /* AAAAAA */ com.saveourtool.diktat.ktlint.ruleset.standarddd import com.saveourtool.diktat.ktlint.CORE.Rule /** * some comments * */ class TestPackageName { var dfsGGGG ="" } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/package/MissingDomainNameTest.kt ================================================ package /* AAAAAA */ ktlint.ruleset.standarddd import com.saveourtool.diktat.ktlint.CORE.Rule /** * some comments * */ class TestPackageName { var dfsGGGG ="" } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/com/saveourtool/diktat/some/name/FixIncorrectExpected.kt ================================================ package /* AAAAAA */ com.saveourtool.diktat.some.name import com.saveourtool.diktat.ktlint.core.Rule class TestPackageName { } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/com/saveourtool/diktat/some/name/FixIncorrectTest.kt ================================================ package /* AAAAAA */ some.buggy.way.to import com.saveourtool.diktat.ktlint.core.Rule class TestPackageName { } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/com/saveourtool/diktat/some/name/FixMissingExpected.kt ================================================ package com.saveourtool.diktat.some.name import com.saveourtool.diktat.ktlint.core.Rule class TestPackageName { } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/com/saveourtool/diktat/some/name/FixMissingTest.kt ================================================ import com.saveourtool.diktat.ktlint.core.Rule class TestPackageName { } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/com/saveourtool/diktat/some/name/FixMissingWithAnnotationExpected.kt ================================================ @file:Suppress("CONSTANT_UPPERCASE") package com.saveourtool.diktat.some.name import com.saveourtool.diktat.ktlint.core.Rule class TestPackageName { } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/com/saveourtool/diktat/some/name/FixMissingWithAnnotationExpected2.kt ================================================ @file:Suppress("CONSTANT_UPPERCASE") // comment package com.saveourtool.diktat.some.name import com.saveourtool.diktat.ktlint.core.Rule class TestPackageName { } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/com/saveourtool/diktat/some/name/FixMissingWithAnnotationExpected3.kt ================================================ /** * comment */ @file:Suppress("CONSTANT_UPPERCASE") package com.saveourtool.diktat.some.name import com.saveourtool.diktat.ktlint.core.Rule class TestPackageName { } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/com/saveourtool/diktat/some/name/FixMissingWithAnnotationTest.kt ================================================ @file:Suppress("CONSTANT_UPPERCASE") import com.saveourtool.diktat.ktlint.core.Rule class TestPackageName { } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/com/saveourtool/diktat/some/name/FixMissingWithAnnotationTest2.kt ================================================ @file:Suppress("CONSTANT_UPPERCASE") // comment import com.saveourtool.diktat.ktlint.core.Rule class TestPackageName { } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/com/saveourtool/diktat/some/name/FixMissingWithAnnotationTest3.kt ================================================ /** * comment */ @file:Suppress("CONSTANT_UPPERCASE") import com.saveourtool.diktat.ktlint.core.Rule class TestPackageName { } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/com/saveourtool/diktat/some/name/FixMissingWithoutImportExpected.kt ================================================ @file:Suppress("CONSTANT_UPPERCASE") package com.saveourtool.diktat.some.name val a = 5 class TestPackageName { } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/com/saveourtool/diktat/some/name/FixMissingWithoutImportTest.kt ================================================ @file:Suppress("CONSTANT_UPPERCASE") val a = 5 class TestPackageName { } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/com/saveourtool/diktat/some/name/FixPackageRegressionExpected.kt ================================================ package com.saveourtool.diktat.some.name import com.saveourtool.diktat.ktlint.core.Rule class TestPackageName { } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/com/saveourtool/diktat/some/name/FixPackageRegressionTest.kt ================================================ package app import com.saveourtool.diktat.ktlint.core.Rule class TestPackageName { } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/some/FixIncorrectExpected.kt ================================================ package /* AAAAAA */ com.saveourtool.diktat.some import com.saveourtool.diktat.ktlint.core.Rule class TestPackageName { } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/some/FixIncorrectTest.kt ================================================ package /* AAAAAA */ buggy.path import com.saveourtool.diktat.ktlint.core.Rule class TestPackageName { } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/some/FixMissingExpected.kt ================================================ package com.saveourtool.diktat.some import com.saveourtool.diktat.ktlint.core.Rule class TestPackageName { } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/some/FixMissingTest.kt ================================================ import com.saveourtool.diktat.ktlint.core.Rule class TestPackageName { } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/header/AutoCopyrightApplyPatternExpected.kt ================================================ /* Copyright (c) Huawei Technologies Co., Ltd. 2020-%%YEAR%%. All rights reserved. */ package test.paragraph2.header class Example ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/header/AutoCopyrightApplyPatternTest.kt ================================================ package test.paragraph2.header class Example ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/header/AutoCopyrightExpected.kt ================================================ /* Copyright (c) Huawei Technologies Co., Ltd. 2020-%%YEAR%%. All rights reserved. */ package test.paragraph2.header class Example ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/header/AutoCopyrightTest.kt ================================================ package test.paragraph2.header class Example ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/header/CopyrightAbsentInvalidPatternExpected.kt ================================================ /* Copyright (c) My Company., Ltd. 2012-%%YEAR%%. All rights reserved. */ /** * Lorem ipsum * dolor sit amet */ package test.paragraph2.header import com.saveourtool.diktat.example.A import com.saveourtool.diktat.example.B /** * Example class */ class Example { lateinit var map: Map } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/header/CopyrightAbsentInvalidPatternTest.kt ================================================ /** * Lorem ipsum * dolor sit amet */ package test.paragraph2.header import com.saveourtool.diktat.example.A import com.saveourtool.diktat.example.B /** * Example class */ class Example { lateinit var map: Map } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/header/CopyrightDifferentYearExpected.kt ================================================ /* Copyright (c) My Company., Ltd. 2012-%%YEAR%%. All rights reserved. */ /** * Lorem ipsum * dolor sit amet */ package test.paragraph2.header import com.saveourtool.diktat.example.A import com.saveourtool.diktat.example.B /** * Example class */ class Example { lateinit var map: Map } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/header/CopyrightDifferentYearExpected2.kt ================================================ /* Copyright (c) My Company., Ltd. 2021. All rights reserved. */ /** * Lorem ipsum * dolor sit amet */ package test.paragraph2.header import com.saveourtool.diktat.example.A import com.saveourtool.diktat.example.B /** * Example class */ class Example { lateinit var map: Map } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/header/CopyrightDifferentYearTest.kt ================================================ /* Copyright (c) My Company., Ltd. 2012-2019. All rights reserved. */ /** * Lorem ipsum * dolor sit amet */ package test.paragraph2.header import com.saveourtool.diktat.example.A import com.saveourtool.diktat.example.B /** * Example class */ class Example { lateinit var map: Map } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/header/CopyrightDifferentYearTest2.kt ================================================ /* Copyright (c) My Company., Ltd. 2003. All rights reserved. */ /** * Lorem ipsum * dolor sit amet */ package test.paragraph2.header import com.saveourtool.diktat.example.A import com.saveourtool.diktat.example.B /** * Example class */ class Example { lateinit var map: Map } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/header/CopyrightInvalidPatternValidCodeExpected.kt ================================================ /* Copyright (c) My Company., Ltd. 2021-%%YEAR%%. All rights reserved. */ /** * Lorem ipsum * dolor sit amet */ package test.paragraph2.header import com.saveourtool.diktat.example.A import com.saveourtool.diktat.example.B /** * Example class */ class Example { lateinit var map: Map } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/header/CopyrightInvalidPatternValidCodeTest.kt ================================================ /* Copyright (c) My Company., Ltd. 2021-%%YEAR%%. All rights reserved. */ /** * Lorem ipsum * dolor sit amet */ package test.paragraph2.header import com.saveourtool.diktat.example.A import com.saveourtool.diktat.example.B /** * Example class */ class Example { lateinit var map: Map } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/header/CopyrightShouldNotTriggerNPEExpected.kt ================================================ /* Copyright (c) My Company., Ltd. 2012-%%YEAR%%. All rights reserved. */ /** * Lorem ipsum * dolor sit amet */ package test.paragraph2.header import com.saveourtool.diktat.example.A import com.saveourtool.diktat.example.B /** * Example class */ class Example { lateinit var map: Map } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/header/CopyrightShouldNotTriggerNPETest.kt ================================================ /** * Lorem ipsum * dolor sit amet */ package test.paragraph2.header import com.saveourtool.diktat.example.A import com.saveourtool.diktat.example.B /** * Example class */ class Example { lateinit var map: Map } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/header/MisplacedHeaderKdocAppendedCopyrightExpected.kt ================================================ /* Copyright (c) Huawei Technologies Co., Ltd. 2020-%%YEAR%%. All rights reserved. */ /** * Lorem ipsum * dolor sit amet */ package test.paragraph2.header import com.saveourtool.diktat.example.A import com.saveourtool.diktat.example.B /** * Example class */ class Example { lateinit var map: Map } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/header/MisplacedHeaderKdocAppendedCopyrightTest.kt ================================================ package test.paragraph2.header import com.saveourtool.diktat.example.A import com.saveourtool.diktat.example.B /** * Lorem ipsum * dolor sit amet */ /** * Example class */ class Example { lateinit var map: Map } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/header/MisplacedHeaderKdocExpected.kt ================================================ /* Copyright (c) Huawei Technologies Co., Ltd. 2020-%%YEAR%%. All rights reserved. */ /** * Lorem ipsum * dolor sit amet */ package test.paragraph2.header import com.saveourtool.diktat.example.A import com.saveourtool.diktat.example.B /** * Example class */ class Example { lateinit var map: Map } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/header/MisplacedHeaderKdocNoCopyrightExpected.kt ================================================ /** * Lorem ipsum * dolor sit amet */ package test.paragraph2.header import com.saveourtool.diktat.example.A import com.saveourtool.diktat.example.B /** * Example class */ class Example { lateinit var map: Map } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/header/MisplacedHeaderKdocNoCopyrightTest.kt ================================================ package test.paragraph2.header import com.saveourtool.diktat.example.A import com.saveourtool.diktat.example.B /** * Lorem ipsum * dolor sit amet */ /** * Example class */ class Example { lateinit var map: Map } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/header/MisplacedHeaderKdocTest.kt ================================================ /* Copyright (c) Huawei Technologies Co., Ltd. 2020-%%YEAR%%. All rights reserved. */ package test.paragraph2.header import com.saveourtool.diktat.example.A import com.saveourtool.diktat.example.B /** * Lorem ipsum * dolor sit amet */ /** * Example class */ class Example { lateinit var map: Map } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/header/MultilineCopyrightExample.kt ================================================ /* Copyright 2018-%%YEAR%% John Doe. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ class SomeClass { fun coolFun() { val a = 5 } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/header/MultilineCopyrightNotTriggerExample.kt ================================================ /* Copyright 2018-%%YEAR%% John Doe. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at */ package test.paragraph2.header class SomeClass { fun function() { } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/header/MultilineCopyrightNotTriggerTest.kt ================================================ /* Copyright 2018-%%YEAR%% John Doe. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at */ package test.paragraph2.header class SomeClass { fun function() { } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/header/MultilineCopyrightTest.kt ================================================ class SomeClass { fun coolFun() { val a = 5 } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/header/NewlineAfterHeaderKdocExpected.kt ================================================ /* Copyright (c) Huawei Technologies Co., Ltd. 2020-%%YEAR%%. All rights reserved. */ /** * This is a file used in unit test */ package test.paragraph2.header class Example ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/header/NewlineAfterHeaderKdocTest.kt ================================================ /* Copyright (c) Huawei Technologies Co., Ltd. 2020-%%YEAR%%. All rights reserved. */ /** * This is a file used in unit test */ package test.paragraph2.header class Example ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/BasicTagsEmptyLineBeforeExpected.kt ================================================ package test.paragraph2.kdoc class Example { /** * @param a integer parameter */ fun test1(a: Int) = Unit /** * Description * * @param a integer parameter */ fun test2(a: Int) = Unit /** * Description * @see test2 * * @param a integer parameter */ fun test3(a: Int) = Unit /** * @param a integer parameter */ fun test4(a: Int) = Unit } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/BasicTagsEmptyLineBeforeTest.kt ================================================ package test.paragraph2.kdoc class Example { /** * @param a integer parameter */ fun test1(a: Int) = Unit /** * Description * @param a integer parameter */ fun test2(a: Int) = Unit /** * Description * @see test2 * @param a integer parameter */ fun test3(a: Int) = Unit /** * * @param a integer parameter */ fun test4(a: Int) = Unit } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/BasicTagsEmptyLinesExpected.kt ================================================ package test.paragraph2.kdoc /** * @param a * @return * @throws IllegalStateException */ fun test(a: Int) = 2 * a ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/BasicTagsEmptyLinesTest.kt ================================================ package test.paragraph2.kdoc /** * @param a * * @return * @throws IllegalStateException */ fun test(a: Int) = 2 * a ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/ConstructorCommentExpected.kt ================================================ package test.paragraph2.kdoc /** * kdoc * class * comment * * @param name */ class A constructor( name: String ) {} /** * kdoc * class * comment * * @param name single-line comment */ class A constructor( name: String ) {} /** * kdoc * class * comment * * @param name * block * comment */ class A constructor( name: String ) {} /** * kdoc * class * comment * * @param name * kdoc property * comment */ class A constructor( name: String ) {} /** * kdoc * class * comment */ class A constructor( /** * @property name property * comment */ name: String ) {} /** * kdoc * class * comment * * @property name */ class A constructor( val name: String ) {} /** * kdoc * class * comment * * @property name single-line comment */ class A constructor( val name: String ) {} /** * kdoc * class * comment * * @property name * block * comment */ class A constructor( val name: String ) {} /** * kdoc * class * comment * * @property name * kdoc property * comment */ class A constructor( val name: String ) {} /** * kdoc * class * comment */ class A constructor( /** * @property name property * comment */ val name: String ) {} /** * kdoc * class * comment * * @param name */ class A constructor( private val name: String ) {} /** * kdoc * class * comment * * @param name single-line comment */ class A constructor( private val name: String ) {} /** * kdoc * class * comment * * @param name * block * comment */ class A constructor( private val name: String ) {} /** * kdoc * class * comment * * @param name * kdoc property * comment */ class A constructor( private val name: String ) {} /** * kdoc * class * comment */ class A constructor( /** * @property name property * comment */ private val name: String ) {} /** * kdoc * class * comment * * @param K * @property openName single-line comment * @property openLastName * block * comment * @property openBirthDate * kdoc property * comment */ open class B constructor( open val openName: String, open val openLastName: String, open val openBirthDate: String, /** * @property openAddr property * comment */ open val openAddr: String ) {} /** * kdoc * class * comment * * @param K * @param P * @param G * @param privateName single-line comment * @property protectedName single-line comment * @property internalName single-line comment * @property openName single-line comment * @property name single-line comment * @param paramName single-line comment * @param privateLastName * block * comment * @property protectedLastName * block * comment * @property internalLastName * block * comment * @property openLastName * block * comment * @property lastName * block * comment * @param paramLastName * block * comment * @param privateBirthDate * kdoc property * comment * @property protectedBirthDate * kdoc property * comment * @property internalBirthDate * kdoc property * comment * @property openBirthDate * kdoc property * comment * @property birthDate * kdoc property * comment * @param paramBirthDate * kdoc property * comment */ class A constructor( private val privateName: String, protected val protectedName: String, internal val internalName: String, override val openName: String, val name: String, paramName: String, private val privateLastName: String, protected val protectedLastName: String, internal val internalLastName: String, override val openLastName: String, val lastName: String, paramLastName: String, private val privateBirthDate: String, protected val protectedBirthDate: String, internal val internalBirthDate: String, override val openBirthDate: String, val birthDate: String, paramBirthDate: String, /** * @property privateAddr property * comment */ private val privateAddr: String, /** * @property protectedAddr property * comment */ protected val protectedAddr: String, /** * @property internalAddr property * comment */ internal val internalAddr: String, /** * @property openAddr property * comment */ override val openAddr: String, /** * @property addr property * comment */ val addr: String, /** * @property paramAddr property * comment */ paramAddr: String, ) : B(), C

, D {} /** * kdoc * class * comment * * @property as * @property keyAs */ actual annotation class JsonSerialize( actual val `as`: KClass<*>, actual val keyAs: KClass<*>, ) ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/ConstructorCommentNewlineExpected.kt ================================================ package test.paragraph2.kdoc /** * @property param1 * @property param2 first comment */ class Example( val param1: String, val param2: String, // second comment ) /** * @property param1 * @property param2 first comment */ class Example( val param1: String, val param2: String, /* second comment */ ) /** * @property param1 * @property param2 first comment */ class Example( val param1: String, val param2: String, /** second comment */ ) ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/ConstructorCommentNewlineTest.kt ================================================ package test.paragraph2.kdoc class Example( val param1: String, // first comment val param2: String, // second comment ) class Example( val param1: String, /* first comment */ val param2: String, /* second comment */ ) class Example( val param1: String, /** first comment */ val param2: String, /** second comment */ ) ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/ConstructorCommentNoKDocExpected.kt ================================================ package test.paragraph2.kdoc /** * @param name */ class A constructor( name: String ) {} /** * @param name single-line comment */ class A constructor( name: String ) {} /** * @param name * block * comment */ class A constructor( name: String ) {} /** * @param name * kdoc property * comment */ class A constructor( name: String ) {} class A constructor( /** * @property name property * comment */ name: String ) {} /** * @property name */ class A constructor( val name: String ) {} /** * @property name single-line comment */ class A constructor( val name: String ) {} /** * @property name * block * comment */ class A constructor( val name: String ) {} /** * @property name * kdoc property * comment */ class A constructor( val name: String ) {} class A constructor( /** * @property name property * comment */ val name: String ) {} /** * @param name */ class A constructor( private val name: String ) {} /** * @param name single-line comment */ class A constructor( private val name: String ) {} /** * @param name * block * comment */ class A constructor( private val name: String ) {} /** * @param name * kdoc property * comment */ class A constructor( private val name: String ) {} class A constructor( /** * @property name property * comment */ private val name: String ) {} /** * @param K * @property openName single-line comment * @property openLastName * block * comment * @property openBirthDate * kdoc property * comment */ open class B constructor( open val openName: String, open val openLastName: String, open val openBirthDate: String, /** * @property openAddr property * comment */ open val openAddr: String ) {} /** * @param K * @param P * @param G * @param privateName single-line comment * @property protectedName single-line comment * @property internalName single-line comment * @property openName single-line comment * @property name single-line comment * @param paramName single-line comment * @param privateLastName * block * comment * @property protectedLastName * block * comment * @property internalLastName * block * comment * @property openLastName * block * comment * @property lastName * block * comment * @param paramLastName * block * comment * @param privateBirthDate * kdoc property * comment * @property protectedBirthDate * kdoc property * comment * @property internalBirthDate * kdoc property * comment * @property openBirthDate * kdoc property * comment * @property birthDate * kdoc property * comment * @param paramBirthDate * kdoc property * comment */ class A constructor( private val privateName: String, protected val protectedName: String, internal val internalName: String, override val openName: String, val name: String, paramName: String, private val privateLastName: String, protected val protectedLastName: String, internal val internalLastName: String, override val openLastName: String, val lastName: String, paramLastName: String, private val privateBirthDate: String, protected val protectedBirthDate: String, internal val internalBirthDate: String, override val openBirthDate: String, val birthDate: String, paramBirthDate: String, /** * @property privateAddr property * comment */ private val privateAddr: String, /** * @property protectedAddr property * comment */ protected val protectedAddr: String, /** * @property internalAddr property * comment */ internal val internalAddr: String, /** * @property openAddr property * comment */ override val openAddr: String, /** * @property addr property * comment */ val addr: String, /** * @property paramAddr property * comment */ paramAddr: String, ) : B(), C

, D {} /** * @property as * @property keyAs */ actual annotation class JsonSerialize( actual val `as`: KClass<*>, actual val keyAs: KClass<*>, ) ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/ConstructorCommentNoKDocTest.kt ================================================ package test.paragraph2.kdoc class A constructor( name: String ) {} class A constructor( //single-line comment name: String ) {} class A constructor( /* * block * comment */ name: String ) {} class A constructor( /** * kdoc property * comment */ name: String ) {} class A constructor( /** * @property name property * comment */ name: String ) {} class A constructor( val name: String ) {} class A constructor( //single-line comment val name: String ) {} class A constructor( /* * block * comment */ val name: String ) {} class A constructor( /** * kdoc property * comment */ val name: String ) {} class A constructor( /** * @property name property * comment */ val name: String ) {} class A constructor( private val name: String ) {} class A constructor( //single-line comment private val name: String ) {} class A constructor( /* * block * comment */ private val name: String ) {} class A constructor( /** * kdoc property * comment */ private val name: String ) {} class A constructor( /** * @property name property * comment */ private val name: String ) {} open class B constructor( //single-line comment open val openName: String, /* * block * comment */ open val openLastName: String, /** * kdoc property * comment */ open val openBirthDate: String, /** * @property openAddr property * comment */ open val openAddr: String ) {} class A constructor( //single-line comment private val privateName: String, //single-line comment protected val protectedName: String, //single-line comment internal val internalName: String, //single-line comment override val openName: String, //single-line comment val name: String, //single-line comment paramName: String, /* * block * comment */ private val privateLastName: String, /* * block * comment */ protected val protectedLastName: String, /* * block * comment */ internal val internalLastName: String, /* * block * comment */ override val openLastName: String, /* * block * comment */ val lastName: String, /* * block * comment */ paramLastName: String, /** * kdoc property * comment */ private val privateBirthDate: String, /** * kdoc property * comment */ protected val protectedBirthDate: String, /** * kdoc property * comment */ internal val internalBirthDate: String, /** * kdoc property * comment */ override val openBirthDate: String, /** * kdoc property * comment */ val birthDate: String, /** * kdoc property * comment */ paramBirthDate: String, /** * @property privateAddr property * comment */ private val privateAddr: String, /** * @property protectedAddr property * comment */ protected val protectedAddr: String, /** * @property internalAddr property * comment */ internal val internalAddr: String, /** * @property openAddr property * comment */ override val openAddr: String, /** * @property addr property * comment */ val addr: String, /** * @property paramAddr property * comment */ paramAddr: String, ) : B(), C

, D {} actual annotation class JsonSerialize( actual val `as`: KClass<*>, actual val keyAs: KClass<*>, ) ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/ConstructorCommentPropertiesExpected.kt ================================================ package test.paragraph2.kdoc /** * @param name property info */ class A constructor( name: String ) {} /** * @param name property info * single-line comment */ class A constructor( name: String ) {} /** * @param name property info * block * comment */ class A constructor( name: String ) {} /** * @param name property info * kdoc property * comment */ class A constructor( name: String ) {} /** * @param name property info */ class A constructor( /** * @property name property * comment */ name: String ) {} /** * @property name property info */ class A constructor( val name: String ) {} /** * @property name property info * single-line comment */ class A constructor( val name: String ) {} /** * @property name property info * block * comment */ class A constructor( val name: String ) {} /** * @property name property info * kdoc property * comment */ class A constructor( val name: String ) {} /** * @property name property info */ class A constructor( /** * @property name property * comment */ val name: String ) {} /** * @param name property info */ class A constructor( private val name: String ) {} /** * @param name property info * single-line comment */ class A constructor( private val name: String ) {} /** * @param name property info * block * comment */ class A constructor( private val name: String ) {} /** * @param name property info * kdoc property * comment */ class A constructor( private val name: String ) {} /** * @property name property info */ class A constructor( /** * @property name property * comment */ private val name: String ) {} /** * @property openName open property info * single-line comment * @property openLastName * open last property * info * block * comment * @property openAddr * property * info * @param K * @property openBirthDate * kdoc property * comment */ open class B constructor( open val openName: String, open val openLastName: String, open val openBirthDate: String, /** * @property openAddr property * comment */ open val openAddr: String ) {} /** * @param P generic type * @param K generic type * @property internalName internal * property info * single-line comment * @property openName override * property info * single-line comment * @param privateLastName private * property info * block * comment * @property openAddr override * property info * @param G * @param privateName single-line comment * @property protectedName single-line comment * @property name single-line comment * @param paramName single-line comment * @property protectedLastName * block * comment * @property internalLastName * block * comment * @property openLastName * block * comment * @property lastName * block * comment * @param paramLastName * block * comment * @param privateBirthDate * kdoc property * comment * @property protectedBirthDate * kdoc property * comment * @property internalBirthDate * kdoc property * comment * @property openBirthDate * kdoc property * comment * @property birthDate * kdoc property * comment * @param paramBirthDate * kdoc property * comment */ class A constructor( private val privateName: String, protected val protectedName: String, internal val internalName: String, override val openName: String, val name: String, paramName: String, private val privateLastName: String, protected val protectedLastName: String, internal val internalLastName: String, override val openLastName: String, val lastName: String, paramLastName: String, private val privateBirthDate: String, protected val protectedBirthDate: String, internal val internalBirthDate: String, override val openBirthDate: String, val birthDate: String, paramBirthDate: String, /** * @property privateAddr property * comment */ private val privateAddr: String, /** * @property protectedAddr property * comment */ protected val protectedAddr: String, /** * @property internalAddr property * comment */ internal val internalAddr: String, /** * @property openAddr property * comment */ override val openAddr: String, /** * @property addr property * comment */ val addr: String, /** * @property paramAddr property * comment */ paramAddr: String, ) : B(), C

, D {} /** * @property keyAs * @property as */ actual annotation class JsonSerialize( actual val `as`: KClass<*>, actual val keyAs: KClass<*>, ) ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/ConstructorCommentPropertiesTest.kt ================================================ package test.paragraph2.kdoc /** * @param name property info */ class A constructor( name: String ) {} /** * @param name property info */ class A constructor( //single-line comment name: String ) {} /** * @param name property info */ class A constructor( /* * block * comment */ name: String ) {} /** * @param name property info */ class A constructor( /** * kdoc property * comment */ name: String ) {} /** * @param name property info */ class A constructor( /** * @property name property * comment */ name: String ) {} /** * @property name property info */ class A constructor( val name: String ) {} /** * @property name property info */ class A constructor( //single-line comment val name: String ) {} /** * @property name property info */ class A constructor( /* * block * comment */ val name: String ) {} /** * @property name property info */ class A constructor( /** * kdoc property * comment */ val name: String ) {} /** * @property name property info */ class A constructor( /** * @property name property * comment */ val name: String ) {} /** * @property name property info */ class A constructor( private val name: String ) {} /** * @property name property info */ class A constructor( //single-line comment private val name: String ) {} /** * @property name property info */ class A constructor( /* * block * comment */ private val name: String ) {} /** * @property name property info */ class A constructor( /** * kdoc property * comment */ private val name: String ) {} /** * @property name property info */ class A constructor( /** * @property name property * comment */ private val name: String ) {} /** * @property openName open property info * @param openLastName * open last property * info * @property openAddr * property * info */ open class B constructor( //single-line comment open val openName: String, /* * block * comment */ open val openLastName: String, /** * kdoc property * comment */ open val openBirthDate: String, /** * @property openAddr property * comment */ open val openAddr: String ) {} /** * @property P generic type * @param K generic type * @property internalName internal * property info * @param openName override * property info * @property privateLastName private * property info * @property openAddr override * property info */ class A constructor( //single-line comment private val privateName: String, //single-line comment protected val protectedName: String, //single-line comment internal val internalName: String, //single-line comment override val openName: String, //single-line comment val name: String, //single-line comment paramName: String, /* * block * comment */ private val privateLastName: String, /* * block * comment */ protected val protectedLastName: String, /* * block * comment */ internal val internalLastName: String, /* * block * comment */ override val openLastName: String, /* * block * comment */ val lastName: String, /* * block * comment */ paramLastName: String, /** * kdoc property * comment */ private val privateBirthDate: String, /** * kdoc property * comment */ protected val protectedBirthDate: String, /** * kdoc property * comment */ internal val internalBirthDate: String, /** * kdoc property * comment */ override val openBirthDate: String, /** * kdoc property * comment */ val birthDate: String, /** * kdoc property * comment */ paramBirthDate: String, /** * @property privateAddr property * comment */ private val privateAddr: String, /** * @property protectedAddr property * comment */ protected val protectedAddr: String, /** * @property internalAddr property * comment */ internal val internalAddr: String, /** * @property openAddr property * comment */ override val openAddr: String, /** * @property addr property * comment */ val addr: String, /** * @property paramAddr property * comment */ paramAddr: String, ) : B(), C

, D {} /** * @property keyAs */ actual annotation class JsonSerialize( actual val `as`: KClass<*>, actual val keyAs: KClass<*>, ) ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/ConstructorCommentTest.kt ================================================ package test.paragraph2.kdoc /** * kdoc * class * comment */ class A constructor( name: String ) {} /** * kdoc * class * comment */ class A constructor( //single-line comment name: String ) {} /** * kdoc * class * comment */ class A constructor( /* * block * comment */ name: String ) {} /** * kdoc * class * comment */ class A constructor( /** * kdoc property * comment */ name: String ) {} /** * kdoc * class * comment */ class A constructor( /** * @property name property * comment */ name: String ) {} /** * kdoc * class * comment */ class A constructor( val name: String ) {} /** * kdoc * class * comment */ class A constructor( //single-line comment val name: String ) {} /** * kdoc * class * comment */ class A constructor( /* * block * comment */ val name: String ) {} /** * kdoc * class * comment */ class A constructor( /** * kdoc property * comment */ val name: String ) {} /** * kdoc * class * comment */ class A constructor( /** * @property name property * comment */ val name: String ) {} /** * kdoc * class * comment */ class A constructor( private val name: String ) {} /** * kdoc * class * comment */ class A constructor( //single-line comment private val name: String ) {} /** * kdoc * class * comment */ class A constructor( /* * block * comment */ private val name: String ) {} /** * kdoc * class * comment */ class A constructor( /** * kdoc property * comment */ private val name: String ) {} /** * kdoc * class * comment */ class A constructor( /** * @property name property * comment */ private val name: String ) {} /** * kdoc * class * comment */ open class B constructor( //single-line comment open val openName: String, /* * block * comment */ open val openLastName: String, /** * kdoc property * comment */ open val openBirthDate: String, /** * @property openAddr property * comment */ open val openAddr: String ) {} /** * kdoc * class * comment */ class A constructor( //single-line comment private val privateName: String, //single-line comment protected val protectedName: String, //single-line comment internal val internalName: String, //single-line comment override val openName: String, //single-line comment val name: String, //single-line comment paramName: String, /* * block * comment */ private val privateLastName: String, /* * block * comment */ protected val protectedLastName: String, /* * block * comment */ internal val internalLastName: String, /* * block * comment */ override val openLastName: String, /* * block * comment */ val lastName: String, /* * block * comment */ paramLastName: String, /** * kdoc property * comment */ private val privateBirthDate: String, /** * kdoc property * comment */ protected val protectedBirthDate: String, /** * kdoc property * comment */ internal val internalBirthDate: String, /** * kdoc property * comment */ override val openBirthDate: String, /** * kdoc property * comment */ val birthDate: String, /** * kdoc property * comment */ paramBirthDate: String, /** * @property privateAddr property * comment */ private val privateAddr: String, /** * @property protectedAddr property * comment */ protected val protectedAddr: String, /** * @property internalAddr property * comment */ internal val internalAddr: String, /** * @property openAddr property * comment */ override val openAddr: String, /** * @property addr property * comment */ val addr: String, /** * @property paramAddr property * comment */ paramAddr: String, ) : B(), C

, D {} /** * kdoc * class * comment */ actual annotation class JsonSerialize( actual val `as`: KClass<*>, actual val keyAs: KClass<*>, ) ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/DeprecatedTagExpected.kt ================================================ package test.paragraph2.kdoc /** * Example class */ @Deprecated(message = "Use class B instead") class A ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/DeprecatedTagTest.kt ================================================ package test.paragraph2.kdoc /** * Example class * @deprecated Use class B instead */ class A ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/KdocBlockCommentExpected.kt ================================================ package test.paragraph2.kdoc /** * Converts this AST node and all its children to pretty string representation */ @Suppress("AVOID_NESTED_FUNCTIONS") fun Example.prettyPrint(level: Int = 0, maxLevel: Int = -1): String { /** * AST operates with \n only, so we need to build the whole string representation and then change line separator */ fun Example.doPrettyPrint(level: Int, maxLevel: Int): String { return "test" + level + maxLevel } return doPrettyPrint(level, maxLevel).replace("\n", System.lineSeparator()) } /** * right place for kdoc */ class Example { /** * right place for kdoc */ fun doGood() { /* * wrong place for kdoc */ /* * right place for block comment */ // right place for eol comment 1 + 2 /** * Converts this AST node and all its children to pretty string representation */ fun Example.prettyPrint(level: Int = 0, maxLevel: Int = -1): String { return "test" } } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/KdocBlockCommentTest.kt ================================================ package test.paragraph2.kdoc /** * Converts this AST node and all its children to pretty string representation */ @Suppress("AVOID_NESTED_FUNCTIONS") fun Example.prettyPrint(level: Int = 0, maxLevel: Int = -1): String { /** * AST operates with \n only, so we need to build the whole string representation and then change line separator */ fun Example.doPrettyPrint(level: Int, maxLevel: Int): String { return "test" + level + maxLevel } return doPrettyPrint(level, maxLevel).replace("\n", System.lineSeparator()) } /** * right place for kdoc */ class Example { /** * right place for kdoc */ fun doGood() { /** * wrong place for kdoc */ /* * right place for block comment */ // right place for eol comment 1 + 2 /** * Converts this AST node and all its children to pretty string representation */ fun Example.prettyPrint(level: Int = 0, maxLevel: Int = -1): String { return "test" } } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/KdocCodeBlockFormattingExampleExpected.kt ================================================ package test.paragraph2.kdoc /** * This is the short overview comment for the example interface. * /* Add a blank line between the general comment text and each KDoc tag */ * @since 1.6 */ public interface Example { // Some comments /* Since it is the first member definition in this code block, there is no need to add a blank line here */ val aField: String // Some comments val bField: String /** * This is a long comment with whitespace that should be split in * multiple line comments in case the line comment formatting is enabled. * /* blank line between description and Kdoc tag */ * @return the rounds of battle of fox and dog */ fun foo() /** * These possibilities include: Formatting of header comments * @return the rounds of battle of fox and dog */ fun bar() { // Some comments /* Since it is the first member definition in this range, there is no need to add a blank line here */ var aVar = 5 // Some comments /* Add a blank line above the comment */ fun doSome() { } } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/KdocCodeBlockFormattingExampleTest.kt ================================================ package test.paragraph2.kdoc /** * This is the short overview comment for the example interface. * /* Add a blank line between the general comment text and each KDoc tag */ * @since 1.6 */ public interface Example { // Some comments /* Since it is the first member definition in this code block, there is no need to add a blank line here */ val aField: String // Some comments val bField: String /** * This is a long comment with whitespace that should be split in * multiple line comments in case the line comment formatting is enabled. * /* blank line between description and Kdoc tag */ * @return the rounds of battle of fox and dog */ fun foo() /** * These possibilities include: Formatting of header comments * @return the rounds of battle of fox and dog */ fun bar() { // Some comments /* Since it is the first member definition in this range, there is no need to add a blank line here */ var aVar = 5 // Some comments /* Add a blank line above the comment */ fun doSome() { } } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/KdocCodeBlocksFormattingExpected.kt ================================================ package test.paragraph2.kdoc /** * This is a test kDoc Comment */ class SomeClass { /* block comment to func */ fun testFunc() { val a = 5 // Right side comment good val c = 6 // Side comment when (this) { is AnotherClass -> println() // Comment is Any -> println() } /* General if comment */ if (a == 5) { } else { // Some Comment } if (a == 5) { } else // Some Comment print(a) /* Block Comment */ val some = 4 /* This is a block comment */ /* Don't fix this comment */ } /** * Useless kdoc comment * Line */ /** * This is a useless function */ fun someUselessFunction() { // This is a useless value val uselessValue = 1 } // Class comment val b = 6 /* Comment to this useless func*/ fun anotherUselessFunc() { } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/KdocCodeBlocksFormattingTest.kt ================================================ package test.paragraph2.kdoc /** * This is a test kDoc Comment */ class SomeClass { /* block comment to func */ fun testFunc() { val a = 5//Right side comment good val c = 6 // Side comment when (this) { is AnotherClass -> println() // Comment is Any -> println() } /* General if comment */ if (a == 5) { } // Some Comment else { } if (a == 5) { } // Some Comment else print(a) /* Block Comment */ val some = 4 /*This is a block comment */ /* Don't fix this comment */ } /** *Useless kdoc comment *Line */ /** * This is a useless function */ fun someUselessFunction() { //This is a useless value val uselessValue = 1 } // Class comment val b = 6 /* Comment to this useless func*/ fun anotherUselessFunc() { } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/KdocEmptyLineExpected.kt ================================================ package com.saveourtool.diktat.test.resources.test.paragraph2.kdoc /** * declaration for some constant */ const val SUPER_CONSTANT = 46 /** * Kdoc docummentation */ class SomeName { /** * another Kdoc */ val a = "string" /** * another Kdoc */ fun somePublicFunction() {} } /** * another Kdoc */ fun someFunction() {} ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/KdocEmptyLineTest.kt ================================================ package com.saveourtool.diktat.test.resources.test.paragraph2.kdoc /** * declaration for some constant */ const val SUPER_CONSTANT = 46 /** * Kdoc docummentation */ class SomeName { /** * another Kdoc */ val a = "string" /** * another Kdoc */ fun somePublicFunction() {} } /** * another Kdoc */ fun someFunction() {} ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/KdocFormattingFullExpected.kt ================================================ package test.paragraph2.kdoc class Example { /** * Empty function to test KDocs * @apiNote stuff * * @implSpec spam * * Another line of description * * @param a useless integer * @return doubled value * @throws RuntimeException never */ @Deprecated(message = "Use testNew") fun test(a: Int): Int = 2 * a } class Foo { /** * @implNote lorem ipsum */ private fun foo() {} } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/KdocFormattingFullTest.kt ================================================ package test.paragraph2.kdoc class Example { /** * Empty function to test KDocs * @deprecated Use testNew * @apiNote stuff * @implSpec spam * * Another line of description * @param a useless integer * @throws RuntimeException never * * @return doubled value */ fun test(a: Int): Int = 2 * a } class Foo { /** * @implNote lorem ipsum */ private fun foo() {} } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/KdocFormattingOrderExpected.kt ================================================ package test.paragraph2.kdoc /** * Creates a docker container with [file], prepared to execute it * * @param runConfiguration a [RunConfiguration] for the supplied binary * @param file a file that will be included as an executable * @param resources additional resources * @return id of created container or null if it wasn't created * @throws DockerException if docker daemon has returned an error * @throws DockerException if docker daemon has returned an error * @throws RuntimeException if an exception not specific to docker has occurred */ internal fun createWithFile(runConfiguration: RunConfiguration, containerName: String, file: File, resources: Collection = emptySet()): String {} ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/KdocFormattingOrderTest.kt ================================================ package test.paragraph2.kdoc /** * Creates a docker container with [file], prepared to execute it * * @param runConfiguration a [RunConfiguration] for the supplied binary * @param file a file that will be included as an executable * @param resources additional resources * @throws DockerException if docker daemon has returned an error * @throws DockerException if docker daemon has returned an error * @throws RuntimeException if an exception not specific to docker has occurred * @return id of created container or null if it wasn't created */ internal fun createWithFile(runConfiguration: RunConfiguration, containerName: String, file: File, resources: Collection = emptySet()): String {} ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/NoPackageNoImportExpected.kt ================================================ /** * Dolor sit amet */ class Example ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/NoPackageNoImportTest.kt ================================================ /** * Dolor sit amet */ class Example ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/OrderedTagsAssertionExpected.kt ================================================ package com.saveourtool.save.reporter.json /** * Reporter that produces a JSON report as a [Report] * * @param builder additional configuration lambda for serializers module * @property out a sink for output */ class JsonReporter( override val out: BufferedSink, builder: PolymorphicModuleBuilder.() -> Unit = {} ) : Reporter ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/OrderedTagsAssertionTest.kt ================================================ package com.saveourtool.save.reporter.json /** * Reporter that produces a JSON report as a [Report] * * @property out a sink for output * * @param builder additional configuration lambda for serializers module */ class JsonReporter( override val out: BufferedSink, builder: PolymorphicModuleBuilder.() -> Unit = {} ) : Reporter ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/OrderedTagsExpected.kt ================================================ package test.paragraph2.kdoc class OrderedTags { /** * Empty function to test KDocs * * @param a useless integer * @return Unit * @throws RuntimeException never */ fun test(a: Int) = Unit } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/OrderedTagsTest.kt ================================================ package test.paragraph2.kdoc class OrderedTags { /** * Empty function to test KDocs * * @return Unit * @throws RuntimeException never * @param a useless integer */ fun test(a: Int) = Unit } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/SpacesAfterTagExpected.kt ================================================ package test.paragraph2.kdoc class SpacesAfterTag { /** * Empty function to test KDocs * * @param a useless integer * @return * @throws RuntimeException never */ fun test(a: Int) = Unit } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/SpacesAfterTagTest.kt ================================================ package test.paragraph2.kdoc class SpacesAfterTag { /** * Empty function to test KDocs * * @param a useless integer * @return * @throws RuntimeException never */ fun test(a: Int) = Unit } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/SpecialTagsInKdocExpected.kt ================================================ package test.paragraph2.kdoc class SpecialTagsInKdoc { /** * Empty function to test KDocs * @apiNote foo * * @implSpec bar * * @implNote baz */ fun test() = Unit } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/SpecialTagsInKdocTest.kt ================================================ package test.paragraph2.kdoc class SpecialTagsInKdoc { /** * Empty function to test KDocs * @apiNote foo * @implSpec bar * * * @implNote baz */ fun test() = Unit } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/package/src/main/kotlin/com/saveourtool/diktat/kdoc/methods/EmptyKdocExpected.kt ================================================ package com.saveourtool.diktat.kdoc.methods val foo = 42 fun doNothing() { return } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/package/src/main/kotlin/com/saveourtool/diktat/kdoc/methods/EmptyKdocTested.kt ================================================ package com.saveourtool.diktat.kdoc.methods val foo = 42 fun doNothing() { return } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/package/src/main/kotlin/com/saveourtool/diktat/kdoc/methods/KdocMethodsFullExpected.kt ================================================ package com.saveourtool.diktat.kdoc.methods class KdocMethodsFull { fun test1() { // this function does nothing } /** * This function is described * partially. * @param a * @return */ fun test2(a: Int): Int { return 2 * a } companion object { /** * @param a * @throws IllegalStateException */ fun test3(a: Int) { throw IllegalStateException("Lorem ipsum") } } private class Nested { /** * @param a * @param b * @return */ fun test4(a: Int, b: Int): Int = 42 } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/package/src/main/kotlin/com/saveourtool/diktat/kdoc/methods/KdocMethodsFullTested.kt ================================================ package com.saveourtool.diktat.kdoc.methods class KdocMethodsFull { fun test1() { // this function does nothing } /** * This function is described * partially. */ fun test2(a: Int): Int { return 2 * a } companion object { fun test3(a: Int) { throw IllegalStateException("Lorem ipsum") } } private class Nested { fun test4(a: Int, b: Int): Int = 42 } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/package/src/main/kotlin/com/saveourtool/diktat/kdoc/methods/KdocWithoutThrowsTagExpected.kt ================================================ package test.paragraph2.kdoc.`package`.src.main.kotlin.com.saveourtool.diktat.kdoc.methods /** * @param onSuccess * @param onFailure * @throws ArrayIndexOutOfBounds */ fun parseInputNumber(onSuccess: (number: Int) -> Unit, onFailure: () -> Unit) { try { val input: Int = binding.inputEditText.text.toString().toInt() if (input < 0) throw NumberFormatException() throw ArrayIndexOutOfBounds() throw NullPointerException() onSuccess(input) } catch (e: IllegalArgumentException) { onFailure() } catch (e: NullPointerException) { onFailure() } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/package/src/main/kotlin/com/saveourtool/diktat/kdoc/methods/KdocWithoutThrowsTagTested.kt ================================================ package test.paragraph2.kdoc.`package`.src.main.kotlin.com.saveourtool.diktat.kdoc.methods /** * @param onSuccess * @param onFailure */ fun parseInputNumber(onSuccess: (number: Int) -> Unit, onFailure: () -> Unit) { try { val input: Int = binding.inputEditText.text.toString().toInt() if (input < 0) throw NumberFormatException() throw ArrayIndexOutOfBounds() throw NullPointerException() onSuccess(input) } catch (e: IllegalArgumentException) { onFailure() } catch (e: NullPointerException) { onFailure() } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/package/src/main/kotlin/com/saveourtool/diktat/kdoc/methods/MissingKdocExpected.kt ================================================ package com.saveourtool.diktat.kdoc.methods class Example { /** * @param a * @param b * @return * @throws IllegalStateException */ fun addInts(a: Int, b: Int): Int { if (false) throw IllegalStateException() return a + b } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/package/src/main/kotlin/com/saveourtool/diktat/kdoc/methods/MissingKdocOnFunctionExpected.kt ================================================ package test.paragraph2.kdoc class Example { /** * @return */ fun hasNoChildren() = children.size == 0 /** * @return */ fun getFirstChild() = children.elementAtOrNull(0) @GetMapping("/projects") fun getProjects() = projectService.getProjects() } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/package/src/main/kotlin/com/saveourtool/diktat/kdoc/methods/MissingKdocOnFunctionTest.kt ================================================ package test.paragraph2.kdoc class Example { fun hasNoChildren() = children.size == 0 fun getFirstChild() = children.elementAtOrNull(0) @GetMapping("/projects") fun getProjects() = projectService.getProjects() } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/package/src/main/kotlin/com/saveourtool/diktat/kdoc/methods/MissingKdocTested.kt ================================================ package com.saveourtool.diktat.kdoc.methods class Example { fun addInts(a: Int, b: Int): Int { if (false) throw IllegalStateException() return a + b } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/package/src/main/kotlin/com/saveourtool/diktat/kdoc/methods/MissingKdocWithModifiersExpected.kt ================================================ package test.paragraph2.kdoc class Example { /** * @param a * @param b * @return * @throws IllegalStateException */ @Deprecated("Use something else") internal fun addInts(a: Int, b: Int): Int { if (false) throw IllegalStateException() return a + b } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/package/src/main/kotlin/com/saveourtool/diktat/kdoc/methods/MissingKdocWithModifiersTest.kt ================================================ package test.paragraph2.kdoc class Example { @Deprecated("Use something else") internal fun addInts(a: Int, b: Int): Int { if (false) throw IllegalStateException() return a + b } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/package/src/main/kotlin/com/saveourtool/diktat/kdoc/methods/ParamTagInsertionExpected.kt ================================================ package com.saveourtool.diktat.kdoc.methods class Example { /** * Lorem ipsum * @param a */ fun test1(a: Int): Unit { } /** * Lorem ipsum * @param a * @return integer value */ fun test2(a: Int): Int = 0 /** * Lorem ipsum * @param a * @throws IllegalStateException */ fun test3(a: Int) { throw IllegalStateException("dolor sit amet") } /** * Lorem ipsum * @param a * @return integer value * @throws IllegalStateException */ fun test4(a: Int): Int { throw IllegalStateException("dolor sit amet") return 0 } /** * Lorem ipsum * @param a * @param b * @return integer value * @throws IllegalStateException */ fun test5(a: Int, b: String): Int { throw IllegalStateException("dolor sit amet") return 0 } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/package/src/main/kotlin/com/saveourtool/diktat/kdoc/methods/ParamTagInsertionTested.kt ================================================ package com.saveourtool.diktat.kdoc.methods class Example { /** * Lorem ipsum */ fun test1(a: Int): Unit { } /** * Lorem ipsum * @return integer value */ fun test2(a: Int): Int = 0 /** * Lorem ipsum * @throws IllegalStateException */ fun test3(a: Int) { throw IllegalStateException("dolor sit amet") } /** * Lorem ipsum * @return integer value * @throws IllegalStateException */ fun test4(a: Int): Int { throw IllegalStateException("dolor sit amet") return 0 } /** * Lorem ipsum * @return integer value * @throws IllegalStateException */ fun test5(a: Int, b: String): Int { throw IllegalStateException("dolor sit amet") return 0 } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/package/src/main/kotlin/com/saveourtool/diktat/kdoc/methods/ReturnTagInsertionExpected.kt ================================================ package com.saveourtool.diktat.kdoc.methods class Example { /** * Lorem ipsum * @return */ fun test1(): Int = 0 /** * Lorem ipsum * @param a integer parameter * @return */ fun test2(a: Int): Int = 0 /** * Lorem ipsum * @return * @throws IllegalStateException never */ fun test3(): Int = 0 /** * Lorem ipsum * @param a integer parameter * @return * @throws IllegalStateException never */ fun test4(a: Int): Int = 0 /** * Lorem ipsum * @throws IllegalStateException never */ fun test5() = 0 /** * Lorem ipsum * @throws IllegalStateException never */ fun A.test6() = 0 } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/package/src/main/kotlin/com/saveourtool/diktat/kdoc/methods/ReturnTagInsertionTested.kt ================================================ package com.saveourtool.diktat.kdoc.methods class Example { /** * Lorem ipsum */ fun test1(): Int = 0 /** * Lorem ipsum * @param a integer parameter */ fun test2(a: Int): Int = 0 /** * Lorem ipsum * @throws IllegalStateException never */ fun test3(): Int = 0 /** * Lorem ipsum * @param a integer parameter * @throws IllegalStateException never */ fun test4(a: Int): Int = 0 /** * Lorem ipsum * @throws IllegalStateException never */ fun test5() = 0 /** * Lorem ipsum * @throws IllegalStateException never */ fun A.test6() = 0 } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/package/src/main/kotlin/com/saveourtool/diktat/kdoc/methods/ThrowsTagInsertionExpected.kt ================================================ package com.saveourtool.diktat.kdoc.methods class Example { /** * Lorem ipsum * @throws IllegalStateException */ fun test1(): Unit { throw IllegalStateException("Lorem ipsum") } /** * Lorem ipsum * @param a integer parameter * @throws IllegalStateException */ fun test2(a: Int): Unit { throw IllegalStateException("Lorem ipsum") } /** * Lorem ipsum * @param a integer parameter * @return integer value * @throws IllegalStateException */ fun test3(a: Int): Int { throw IllegalStateException("Lorem ipsum") } /** * Lorem ipsum * @return integer value * @throws IllegalStateException */ fun test4(): Int { throw IllegalStateException("Lorem ipsum") } /** * Lorem ipsum * @throws IllegalStateException * @throws IllegalAccessException */ fun test5() { if (true) throw IllegalStateException("Lorem ipsum") else throw IllegalAccessException("Dolor sit amet") } /** * @throws RuntimeException */ fun test6() { try { foo() } catch (_: NullPointerException) { println("NPE!") } catch (e: RuntimeException) { println("Whoops...") throw e } catch (e: Error) { val x = IllegalStateException() println("Nothing to do here") throw x } } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph2/kdoc/package/src/main/kotlin/com/saveourtool/diktat/kdoc/methods/ThrowsTagInsertionTested.kt ================================================ package com.saveourtool.diktat.kdoc.methods class Example { /** * Lorem ipsum */ fun test1(): Unit { throw IllegalStateException("Lorem ipsum") } /** * Lorem ipsum * @param a integer parameter */ fun test2(a: Int): Unit { throw IllegalStateException("Lorem ipsum") } /** * Lorem ipsum * @param a integer parameter * @return integer value */ fun test3(a: Int): Int { throw IllegalStateException("Lorem ipsum") } /** * Lorem ipsum * @return integer value */ fun test4(): Int { throw IllegalStateException("Lorem ipsum") } /** * Lorem ipsum */ fun test5() { if (true) throw IllegalStateException("Lorem ipsum") else throw IllegalAccessException("Dolor sit amet") } fun test6() { try { foo() } catch (_: NullPointerException) { println("NPE!") } catch (e: RuntimeException) { println("Whoops...") throw e } catch (e: Error) { val x = IllegalStateException() println("Nothing to do here") throw x } } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/annotations/AnnotationCommentExpected.kt ================================================ @Hello() // hello @Hi class A { } @Foo /* */ @Goo /* */ @Qwe class AA {} /** */ @Foo /** */ @Gg class Bb {} ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/annotations/AnnotationCommentTest.kt ================================================ @Hello() // hello @Hi class A { } @Foo /* */ @Goo /* */ @Qwe class AA {} /** */ @Foo /** */ @Gg class Bb {} ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/annotations/AnnotationConstructorSingleLineExpected.kt ================================================ package test.paragraph3.annotations class SomeClass @Inject @SomeAnnotation constructor() { @ThirdAnnotation @FourthAnnotation fun someFunc(@Annotation var1: Int, @AnotherAnnotation var2: String) { } } class AnotherClass @Inject @SomeAnnotation constructor() { } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/annotations/AnnotationConstructorSingleLineTest.kt ================================================ package test.paragraph3.annotations class SomeClass @Inject @SomeAnnotation constructor() { @ThirdAnnotation @FourthAnnotation fun someFunc(@Annotation var1: Int, @AnotherAnnotation var2: String) { } } class AnotherClass @Inject @SomeAnnotation constructor() { } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/annotations/AnnotationSingleLineExpected.kt ================================================ package test.paragraph3.annotations @SomeAnnotation @SecondAnnotation class SomeClass @Inject constructor() { @ThirdAnnotation @FourthAnnotation fun someFunc(@Annotation var1: Int, @AnotherAnnotation var2: String) { } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/annotations/AnnotationSingleLineTest.kt ================================================ package test.paragraph3.annotations @SomeAnnotation @SecondAnnotation class SomeClass @Inject constructor() { @ThirdAnnotation @FourthAnnotation fun someFunc(@Annotation var1: Int, @AnotherAnnotation var2: String) { } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/blank_lines/CodeBlockWithBlankLinesExpected.kt ================================================ package test.paragraph3.blank_lines class Example { fun foo() { bar() } fun bar() { println() println() } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/blank_lines/CodeBlockWithBlankLinesTest.kt ================================================ package test.paragraph3.blank_lines class Example { fun foo() { bar() } fun bar() { println() println() } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/blank_lines/RedundantBlankLinesAtTheEndOfBlockExpected.kt ================================================ package test.paragraph3.blank_lines class Example { fun foo() { bar() } fun bar() { println() println() } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/blank_lines/RedundantBlankLinesAtTheEndOfBlockTest.kt ================================================ package test.paragraph3.blank_lines class Example { fun foo() { bar() } fun bar() { println() println() } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/blank_lines/RedundantBlankLinesExpected.kt ================================================ package test.paragraph3.blank_lines class Example { val foo = 0 fun bar() { } fun foo() { list.map { bar() } } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/blank_lines/RedundantBlankLinesTest.kt ================================================ package test.paragraph3.blank_lines class Example { val foo = 0 fun bar() { } fun foo() { list.map { bar() } } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/block_brace/ClassBracesExpected.kt ================================================ package test.paragraph3.block_brace class A{ private val id = 10 } class B{ private val id = 10 } class C{ companion object{ private val id = 11 } } class D { val x = 0 } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/block_brace/ClassBracesTest.kt ================================================ package test.paragraph3.block_brace class A{ private val id = 10 } class B{ private val id = 10} class C{ companion object{ private val id = 11} } class D {val x = 0} ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/block_brace/DoWhileBracesExpected.kt ================================================ package test.paragraph3.block_brace fun foo1() { do { println("hello") } while (x > 0) } fun foo2() { do{ println("hello") } while (y > 0) } fun foo3() { do { println("hellp") }while (z > 0) } fun foo4() { do { println("hellp") } while (l > 0) } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/block_brace/DoWhileBracesTest.kt ================================================ package test.paragraph3.block_brace fun foo1() { do { println("hello") } while (x > 0) } fun foo2() { do{ println("hello") } while (y > 0) } fun foo3() { do { println("hellp") }while (z > 0) } fun foo4() { do { println("hellp") } while (l > 0) } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/block_brace/IfElseBracesExpected.kt ================================================ package test.paragraph3.block_brace fun foo1() { if (x > 0) { foo() } else { zoo() } } fun foo1(){ if (x < 0){ println("Helloo") } } fun foo2(){ if (x<0) { println("Hello") } } fun foo3(){ if (x == 5){ println(5) } else if (x == 6) println(6) else println(7) } fun foo4(){ if (x > 4){ println(4) }else if (x < 4){ println(5) } } fun foo() { //comment1 // comment if(x) { //comment2 foo() } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/block_brace/IfElseBracesTest.kt ================================================ package test.paragraph3.block_brace fun foo1() { if (x > 0) { foo() } else { zoo() } } fun foo1(){ if (x < 0){ println("Helloo") } } fun foo2(){ if (x<0) { println("Hello") } } fun foo3(){ if (x == 5){ println(5) } else if (x == 6) println(6) else println(7) } fun foo4(){ if (x > 4){ println(4) }else if (x < 4){ println(5) } } fun foo() { //comment1 if(x) // comment { //comment2 foo() } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/block_brace/LoopsBracesExpected.kt ================================================ package test.paragraph3.block_brace fun foo(){ for (i in 1..100){ println(i) } for (i in 1..100) { println(i) } for (i in 1..100) { println(i) } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/block_brace/LoopsBracesTest.kt ================================================ package test.paragraph3.block_brace fun foo(){ for (i in 1..100){ println(i) } for (i in 1..100) { println(i)} for (i in 1..100) { println(i) } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/block_brace/TryBraceExpected.kt ================================================ package test.paragraph3.block_brace fun divideOrZero(numerator: Int, denominator: Int): Int { try { return numerator / denominator } catch (e: ArithmeticException) { return 0 } catch (e: Exception) { return 1 } finally { println("Hello") } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/block_brace/TryBraceTest.kt ================================================ package test.paragraph3.block_brace fun divideOrZero(numerator: Int, denominator: Int): Int { try { return numerator / denominator } catch (e: ArithmeticException) { return 0 } catch (e: Exception) { return 1 } finally { println("Hello") } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/block_brace/WhenBranchesExpected.kt ================================================ package test.paragraph3.block_brace fun foo(){ when(x){ 1-> println(1) else -> println(2) } when(x) { 1 -> println(1) else -> println(2) } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/block_brace/WhenBranchesTest.kt ================================================ package test.paragraph3.block_brace fun foo(){ when(x){ 1-> println(1) else -> println(2)} when(x) {1 -> println(1) else -> println(2)} } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/boolean_expressions/BooleanExpressionsExpected.kt ================================================ package test.paragraph3.boolean_expressions fun foo() { if (some != null && some == 6) { } if ((some != null || c > 2) && (a > 4 || b > 3)) { } if (a > 3 && b > 3) { } if (!a && !b) {} if (c && !a && !b) {} } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/boolean_expressions/BooleanExpressionsTest.kt ================================================ package test.paragraph3.boolean_expressions fun foo() { if (some != null && some != null && some == 6) { } if ((some != null || c > 2) && (a > 4 || b > 3)) { } if (a > 3 && b > 3 && a > 3) { } if (!(a || b)) {} if (!(a || b) && c) {} } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/boolean_expressions/DistributiveLawExpected.kt ================================================ package test.paragraph3.boolean_expressions fun some() { if (a > 5 && (b > 6 || c > 7)) { } if (a > 5 || (b > 6 && c > 7)) { } if (a > 5 || (b > 6 && c > 7 && d > 8)) { } if (a > 5 && (b > 6 || c > 7 || d > 8)) { } // Special case if (a > 5 && (b > 6 || c > 7 || d > 8)) { } // long case if (a > 5 && ((b > 6 && z > 3) || (c > 7 && y > 4) || (d > 8 && w > 5))) { } // long case #2.1 if (b > 6 && a > 5 && (z > 3 || c > 7 || w > 5)) { } // long case #2.2 if (b > 6 || a > 5 || (z > 3 && c > 7 && w > 5)) { } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/boolean_expressions/DistributiveLawTest.kt ================================================ package test.paragraph3.boolean_expressions fun some() { if (a > 5 && b > 6 || c > 7 && a > 5) { } if ((a > 5 || b > 6) && (c > 7 || a > 5)) { } if ((a > 5 || b > 6) && (c > 7 || a > 5) && (a > 5 || d > 8)) { } if ((a > 5 && b > 6) || (c > 7 && a > 5) || (a > 5 && d > 8)) { } // Special case if ((b > 6 && a > 5) || (c > 7 && a > 5) || (a > 5 && d > 8)) { } // long case if ((b > 6 && a > 5 && z > 3) || (c > 7 && a > 5 && y > 4) || (a > 5 && d > 8 && w > 5)) { } // long case #2.1 if ((b > 6 && a > 5 && z > 3) || (c > 7 && a > 5 && b > 6) || (a > 5 && b > 6 && w > 5)) { } // long case #2.2 if ((b > 6 || a > 5 || z > 3) && (c > 7 || a > 5 || b > 6) && (a > 5 || b > 6 || w > 5)) { } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/boolean_expressions/ExpressionSimplificationExpected.kt ================================================ package test.paragraph3.boolean_expressions fun F.foo1() { if (this.valueParameters[i].getFunctionName() != other.valueParameters[i].getFunctionName() || this.valueParameters[i].getFunctionType() == other.valueParameters[i].getFunctionType() ) { return false } } fun F.foo2() { if (this.valueParameters[i].getFunctionName() <= other.valueParameters[i].getFunctionName() || this.valueParameters[i].getFunctionType() >= other.valueParameters[i].getFunctionType() ) { return false } } fun F.foo3() { if (!(this.valueParameters[i].getFunctionName() xor other.valueParameters[i].getFunctionName()) || !(this.valueParameters[i].getFunctionType() xor other.valueParameters[i].getFunctionType()) ) { return false } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/boolean_expressions/ExpressionSimplificationTest.kt ================================================ package test.paragraph3.boolean_expressions fun F.foo1() { if (!(this.valueParameters[i].getFunctionName() == other.valueParameters[i].getFunctionName() && this.valueParameters[i].getFunctionType() != other.valueParameters[i].getFunctionType()) ) { return false } } fun F.foo2() { if (!(this.valueParameters[i].getFunctionName() > other.valueParameters[i].getFunctionName() && this.valueParameters[i].getFunctionType() < other.valueParameters[i].getFunctionType()) ) { return false } } fun F.foo3() { if (!(this.valueParameters[i].getFunctionName() xor other.valueParameters[i].getFunctionName() && this.valueParameters[i].getFunctionType() xor other.valueParameters[i].getFunctionType()) ) { return false } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/boolean_expressions/NegativeExpressionExpected.kt ================================================ package test.paragraph3.boolean_expressions fun foo() { if (bar) { } if (bar) { } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/boolean_expressions/NegativeExpressionTest.kt ================================================ package test.paragraph3.boolean_expressions fun foo() { if (bar && (!isEmpty() || isEmpty())) { } if (bar && (isEmpty() || !isEmpty())) { } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/boolean_expressions/OrderIssueExpected.kt ================================================ package test.paragraph3.boolean_expressions fun foo() { if (isB && isA && isC) { } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/boolean_expressions/OrderIssueTest.kt ================================================ package test.paragraph3.boolean_expressions fun foo() { if (isB && isA && isC && (isEmpty() || !isEmpty())) { } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/boolean_expressions/SameExpressionsInConditionExpected.kt ================================================ fun foo() { if (a) {} if (a) {} if (a) {} return if (node is TomlKeyValueSimple) { decodeValue().toString().toLowerCase() != "null" } else { true } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/boolean_expressions/SameExpressionsInConditionTest.kt ================================================ fun foo() { if (a || a) {} if (a && a) {} if ((((a && a)))) {} return if ((node is TomlKeyValueSimple) || (node is TomlKeyValueSimple)) { decodeValue().toString().toLowerCase() != "null" } else { true } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/boolean_expressions/SubstitutionIssueExpected.kt ================================================ package test.paragraph3.boolean_expressions fun foo() { if (isABC_A && isABC_B && isABC_C) { } if (isAdd && isAdded()) { } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/boolean_expressions/SubstitutionIssueTest.kt ================================================ package test.paragraph3.boolean_expressions fun foo() { if (isABC_A && isABC_B && isABC_C && (isEmpty() || !isEmpty())) { } if (isAdd && isAdded() && (isEmpty() || !isEmpty())) { } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/braces/DoWhileBracesExpected.kt ================================================ package test.paragraph3.braces fun foo() { do { } while (condition) do { } while (condition) } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/braces/DoWhileBracesTest.kt ================================================ package test.paragraph3.braces fun foo() { do while (condition) do while (condition) } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/braces/IfElseBraces1Expected.kt ================================================ package test.paragraph3.braces fun foo1() { if (x > 0) { foo() } else { bar() } } fun foo2() { if (x > 0) { foo() } else { println("else") bar() } } fun foo3() { if (x > 0) { } else { bar() } } fun foo4() { if (x > 0) { } else { }; } fun foo5() { if (x > 0) { foo() } else { bar() } } fun foo6() { if (x > 0) { foo() } else if (y > 0) { abc() } else { bar() } } fun foo7() { if (x > 0) { foo() } else if (y > 0) { abc() } else { bar() } } fun foo8() { if (x > 0) { if (y > 0) foo() else abc() } else { bar() } } fun foo9() { if (x > 0) { foo() } else if (y > 0) { abc() } else { bar() } } fun foo10() { if (x > 0) { foo() } else if (z > 0) { if (y > 0) abc() else qwe() } else { bar() } } fun foo11() { if (x > 0) else bar() } fun foo12() { if (x > 0) { foo() } else { }; } fun foo13() { if (x > 0) { } else { }; } fun foo() { if (a) { bar() } else b?.let { baz() } ?: run { qux() } } fun foo() { if (a) { bar() } else b.apply { baz() } } fun foo() { if (a) { bar() } else { baz(b.apply { id = 5 }) } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/braces/IfElseBraces1Test.kt ================================================ package test.paragraph3.braces fun foo1() { if (x > 0) foo() else bar() } fun foo2() { if (x > 0) foo() else { println("else") bar() } } fun foo3() { if (x > 0) else { bar() } } fun foo4() { if (x > 0) else ; } fun foo5() { if (x > 0) foo() else bar() } fun foo6() { if (x > 0) foo() else if (y > 0) abc() else bar() } fun foo7() { if (x > 0) foo() else if (y > 0) abc() else bar() } fun foo8() { if (x > 0) if (y > 0) foo() else abc() else bar() } fun foo9() { if (x > 0) foo() else if (y > 0) abc() else bar() } fun foo10() { if (x > 0) foo() else if (z > 0) if (y > 0) abc() else qwe() else bar() } fun foo11() { if (x > 0) else bar() } fun foo12() { if (x > 0) foo() else ; } fun foo13() { if (x > 0) else ; } fun foo() { if (a) { bar() } else b?.let { baz() } ?: run { qux() } } fun foo() { if (a) { bar() } else b.apply { baz() } } fun foo() { if (a) { bar() } else baz(b.apply { id = 5 }) } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/braces/IfElseBracesInsideScopeFunctionsExpected.kt ================================================ package test.paragraph3.braces fun foo1() { str.apply { if (x > 0) { foo() } else { bar() } } } fun foo2() { str.let { if (x > 0) { foo() } else { bar() } } } fun foo3() { str.run { while (x > 0) { if (x > 0) { foo() } else { bar() } } } } fun foo4() { str.with { while (x > 0) { if (x > 0) { foo() } else { bar() } } } } fun foo5() { str.also { while (x > 0) { if (x > 0) { foo() } else { bar() } } } } fun foo6() { str.apply { if (x > 0) { foo() } else if (y > 0) { abc() } else { bar() } } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/braces/IfElseBracesInsideScopeFunctionsTest.kt ================================================ package test.paragraph3.braces fun foo1() { str.apply { if (x > 0) foo() else bar() } } fun foo2() { str.let { if (x > 0) { foo() } else bar() } } fun foo3() { str.run { while (x > 0) { if (x > 0) foo() else bar() } } } fun foo4() { str.with { while (x > 0) { if (x > 0) foo() else { bar() } } } } fun foo5() { str.also { while (x > 0) { if (x > 0) foo() else bar() } } } fun foo6() { str.apply { if (x > 0) foo() else if (y > 0) abc() else bar() } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/braces/LoopsBracesExpected.kt ================================================ package test.paragraph3.braces fun foo() { for (i in 1..100) { println(i) } while (condition) { println("while") } do { println("do-while") } while (condition) } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/braces/LoopsBracesInsideScopeFunctionsExpected.kt ================================================ package test.paragraph3.braces fun foo1() { str.apply { for (i in 1..100) { println(i) } } } fun foo2() { str.let { while (x > 0) { println(i) } } } fun foo3() { str.run { do { println(i) } while (x > 0) } } fun foo4() { str.with { do { println(i) } while (x > 0) } } fun foo5() { str.also { for (i in 1..100) { while (x > 0) { println(i) } } } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/braces/LoopsBracesInsideScopeFunctionsTest.kt ================================================ package test.paragraph3.braces fun foo1() { str.apply { for (i in 1..100) println(i) } } fun foo2() { str.let { while (x > 0) println(i) } } fun foo3() { str.run { do println(i) while (x > 0) } } fun foo4() { str.with { do println(i) while (x > 0) } } fun foo5() { str.also { for (i in 1..100) { while (x > 0) println(i) } } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/braces/LoopsBracesTest.kt ================================================ package test.paragraph3.braces fun foo() { for (i in 1..100) println(i) while (condition) println("while") do println("do-while") while (condition) } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/braces/WhenBranchesExpected.kt ================================================ package test.paragraph3.braces fun foo(x: Options) { when (x) { OPTION_1 -> println(1) OPTION_2 -> println(2) OPTION_3 -> println(3) else -> { println("error") throw IllegalStateException() } } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/braces/WhenBranchesTest.kt ================================================ package test.paragraph3.braces fun foo(x: Options) { when (x) { OPTION_1 -> { println(1) } OPTION_2 -> { println(2) } OPTION_3 -> println(3) else -> { println("error") throw IllegalStateException() } } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/collapse_if/CollapseIfStatementsExpected.kt ================================================ package test.paragraph3.collapse_if fun foo() { if (true && true && true) { val a = 6 } } fun foo() { if (true) { val someConstant = 5 if (true) { doSomething() } } } fun foo() { if (true && true) { doSomething() } } fun foo() { if (true && true && true && true && true && true) { doSomething() } } fun foo() { } fun foo() { fun1() if (cond1) { fun2() } else if (cond2) { fun3() } else { fun4() } } fun foo() { fun1() if (cond1) { fun2() } else if (cond2) { if (true) { fun3() } } else { fun4() } } fun foo() { fun1() if (cond1) { fun2() } else if (cond2) { if (true && true) { fun3() } } else { fun4() } } fun foo() { fun1() if (cond1) { fun2() } else if (cond2) { if (true && true) { fun3() } } else { fun4() if (true && true) { fun5() } } } fun foo() { if (cond1 && (cond2 && cond3 || cond4)) { firstAction() secondAction() } } fun foo() { if (cond1 && cond2 && (cond3 || cond4)) { someAction() } } fun foo() { if (cond1) { if (cond2) { firstAction() secondAction() } else { firstAction() } } else { secondAction() } } fun foo () { if (cond1) { if (cond2) { firstAction() secondAction() } else if (cond3) { firstAction() } else { val a = 5 } } else { secondAction() } } fun foo() { if (cond1 && cond2) { firstAction() secondAction() } if (cond3) { secondAction() } } fun foo() { if (cond1 && (cond2 || cond3)) { firstAction() secondAction() } if (cond4) { secondAction() } } fun foo () { if (cond1) { if (cond2 || cond3) { firstAction() secondAction() } if (cond4) { secondAction() } } } fun foo() { if (cond1) { if (cond2) { doSomething() } val a = 5 } } fun foo() { if (true && /* Some Comments */ // More comments true) { // comment 1 val a = 5 // comment 2 doSomething() // comment 3 } } fun foo() { if (true && // Some // comments true) { doSomething() } } fun foo() { // comment if (cond1 && /* Some comments */ // More comments (cond2 || cond3)) { doSomething() } } fun foo() { if (cond1 && // comment cond2 && // comment 2 cond3) { doSomething() } } fun foo () { if (true && true) { doSomething() } } fun foo () { if (/*comment*/ true && true) { doSomething() } } fun foo () { if (true /*comment*/ && true) { doSomething() } } fun foo () { if (true && true /*comment*/) { doSomething() } } fun foo () { if (true && (true || false) /*comment*/ && true /*comment*/) { doSomething() } } fun foo () { if (true /*comment * more comments */ && true /*comment 2*/) { doSomething() } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/collapse_if/CollapseIfStatementsTest.kt ================================================ package test.paragraph3.collapse_if fun foo() { if (true) { if (true) { if (true) { val a = 6 } } } } fun foo() { if (true) { val someConstant = 5 if (true) { doSomething() } } } fun foo() { if (true) { if (true) { doSomething() } } } fun foo() { if (true) { if (true) { if (true) { if (true) { if (true) { if (true) { doSomething() } } } } } } } fun foo() { } fun foo() { fun1() if (cond1) { fun2() } else if (cond2) { fun3() } else { fun4() } } fun foo() { fun1() if (cond1) { fun2() } else if (cond2) { if (true) { fun3() } } else { fun4() } } fun foo() { fun1() if (cond1) { fun2() } else if (cond2) { if (true) { if (true) { fun3() } } } else { fun4() } } fun foo() { fun1() if (cond1) { fun2() } else if (cond2) { if (true) { if (true) { fun3() } } } else { fun4() if (true) { if (true) { fun5() } } } } fun foo() { if (cond1) { if (cond2 && cond3 || cond4) { firstAction() secondAction() } } } fun foo() { if (cond1) { if (cond2) { if (cond3 || cond4) { someAction() } } } } fun foo() { if (cond1) { if (cond2) { firstAction() secondAction() } else { firstAction() } } else { secondAction() } } fun foo () { if (cond1) { if (cond2) { firstAction() secondAction() } else if (cond3) { firstAction() } else { val a = 5 } } else { secondAction() } } fun foo() { if (cond1) { if (cond2) { firstAction() secondAction() } } if (cond3) { secondAction() } } fun foo() { if (cond1) { if (cond2 || cond3) { firstAction() secondAction() } } if (cond4) { secondAction() } } fun foo () { if (cond1) { if (cond2 || cond3) { firstAction() secondAction() } if (cond4) { secondAction() } } } fun foo() { if (cond1) { if (cond2) { doSomething() } val a = 5 } } fun foo() { if (true) { /* Some Comments */ // More comments if (true) { // comment 1 val a = 5 // comment 2 doSomething() } // comment 3 } } fun foo() { if (true) { // Some // comments if (true) { doSomething() } } } fun foo() { // comment if (cond1) { /* Some comments */ // More comments if (cond2 || cond3) { doSomething() } } } fun foo() { if (cond1) { // comment if (cond2) { // comment 2 if (cond3) { doSomething() } } } } fun foo () { if (true) { if (true) {doSomething()} } } fun foo () { if (/*comment*/ true) { if (true) { doSomething() } } } fun foo () { if (true /*comment*/) { if (true) { doSomething() } } } fun foo () { if (true) { if (true /*comment*/) { doSomething() } } } fun foo () { if (true && (true || false) /*comment*/) { if (true /*comment*/) { doSomething() } } } fun foo () { if (true /*comment * more comments */ ) { if (true /*comment 2*/) { doSomething() } } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/else_expected/ElseInWhenExpected.kt ================================================ package test.paragraph3.else_expected enum class TestEnum { ONE, TWO } fun testWhenExpression() { val directoryType = TestEnum.ONE when (directoryType) { TestEnum.ONE -> "d" TestEnum.TWO -> "-" } val noElse = when (directoryType) { TestEnum.ONE -> "d" TestEnum.TWO -> "a" } val v = 1 when (v) { f(TestEnum.ONE) -> print("1") f(TestEnum.TWO) -> print("2") else -> { // this is a generated else block }} val inLambda = {x: Int -> when(x) { 1 -> print(5) } } } sealed class Expr { class Num(val value: Int) : Expr() class Sum(val left: Expr, val right: Expr) : Expr() } fun eval(e: Expr): Int = when (e) { is Expr.Num -> e.value is Expr.Sum -> eval(e.right) + eval(e.left) } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/else_expected/ElseInWhenTest.kt ================================================ package test.paragraph3.else_expected enum class TestEnum { ONE, TWO } fun testWhenExpression() { val directoryType = TestEnum.ONE when (directoryType) { TestEnum.ONE -> "d" TestEnum.TWO -> "-" } val noElse = when (directoryType) { TestEnum.ONE -> "d" TestEnum.TWO -> "a" } val v = 1 when (v) { f(TestEnum.ONE) -> print("1") f(TestEnum.TWO) -> print("2") } val inLambda = {x: Int -> when(x) { 1 -> print(5) } } } sealed class Expr { class Num(val value: Int) : Expr() class Sum(val left: Expr, val right: Expr) : Expr() } fun eval(e: Expr): Int = when (e) { is Expr.Num -> e.value is Expr.Sum -> eval(e.right) + eval(e.left) } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/empty_block/EmptyBlockExpected.kt ================================================ package test.paragraph3.empty_block fun foo () { try { doSome() } catch (ex: Exception){ } } fun goo () { var x = 10 if (x == 10) return else println(10) val y = listOf().map { } for(x in 0..10) println(x) while (x > 0) --x } open class RuleConfiguration(protected val config: Map) object EmptyConfiguration: RuleConfiguration(mapOf()) ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/empty_block/EmptyBlockTest.kt ================================================ package test.paragraph3.empty_block fun foo () { try { doSome() } catch (ex: Exception){} } fun goo () { var x = 10 if (x == 10) return else println(10) val y = listOf().map { } for(x in 0..10) println(x) while (x > 0) --x } open class RuleConfiguration(protected val config: Map) object EmptyConfiguration: RuleConfiguration(mapOf()) ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/enum_separated/EnumSeparatedExpected.kt ================================================ package test.paragraph3.enum_separated enum class ENUM { A , B, C } enum class ENUM { RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF), /*sdcsc*/ ; } enum class ENUM { RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF), ; } enum class ProtocolState { WAITING { override fun signal() = TALKING }, TALKING { override fun signal() = WAITING }, ; abstract fun signal(): ProtocolState } enum class ProtocolState { WAITING { val a = 10; }, TALKING { val b = 123; }, ; abstract fun signal(): ProtocolState } enum class Simple { A, B, C } enum class SimpleWithFun { A, B, C, ; fun foo() {} } enum class SimpleWithNewLine { A, B, C, ; } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/enum_separated/EnumSeparatedTest.kt ================================================ package test.paragraph3.enum_separated enum class ENUM { A , B, C } enum class ENUM { RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF) /*sdcsc*/ } enum class ENUM { RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF),; } enum class ProtocolState { WAITING { override fun signal() = TALKING }, TALKING { override fun signal() = WAITING }; abstract fun signal(): ProtocolState } enum class ProtocolState { WAITING { val a = 10; }, TALKING { val b = 123; }; abstract fun signal(): ProtocolState } enum class Simple { A, B, C } enum class SimpleWithFun { A, B, C; fun foo() {} } enum class SimpleWithNewLine { A, B, C } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/enum_separated/LastElementCommentExpected.kt ================================================ package test.paragraph3.enum_separated enum class Foo0 { A, B, C, ; } enum class Foo11 { A, B, C, // some comment 11 ; } enum class Foo12 { A, B, C,// some comment 12 ; } enum class Foo13 { A, B, C, ;// some comment 13 } enum class Foo14 { A, B, C, ; // some comment 14 } enum class Foo21 { A, B, C, /* some comment 21 */ ; } enum class Foo22 { A, B, C,/* some comment 22 */ ; } enum class Foo23 { A, B, C, ;/* some comment 23 */ } enum class Foo24 { A, B, C, ; /* some comment 24 */ } enum class Foo31 { A, B, C, ; /** some comment 31 */ } enum class Foo32 { A, B, C, ; /** some comment 32 */ } enum class Foo33 { A, B, C, ; /** some comment 33 */ } enum class Foo34 { A, B, C, ; /** some comment 34 */ } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/enum_separated/LastElementCommentTest.kt ================================================ package test.paragraph3.enum_separated enum class Foo0 { A, B, C } enum class Foo11 { A, B, C // some comment 11 } enum class Foo12 { A, B, C// some comment 12 } enum class Foo13 { A, B, C;// some comment 13 } enum class Foo14 { A, B, C; // some comment 14 } enum class Foo21 { A, B, C /* some comment 21 */ } enum class Foo22 { A, B, C/* some comment 22 */ } enum class Foo23 { A, B, C;/* some comment 23 */ } enum class Foo24 { A, B, C; /* some comment 24 */ } enum class Foo31 { A, B, C /** some comment 31 */ } enum class Foo32 { A, B, C/** some comment 32 */ } enum class Foo33 { A, B, C;/** some comment 33 */ } enum class Foo34 { A, B, C; /** some comment 34 */ } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/file_structure/BlankLinesBetweenBlocksExpected.kt ================================================ /** * This is an example */ @file:JvmName("Foo") package com.saveourtool.diktat.example import com.saveourtool.diktat.Foo class Example { val y: Foo = Foo() } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/file_structure/CompanionObjectWithCommentExpected.kt ================================================ package com.saveourtool.diktat import io.micrometer.core.instrument.MeterRegistry import org.springframework.stereotype.Component /** * @param meterRegistry * @param binRepository */ @Component class ActiveBinsMetric(meterRegistry: MeterRegistry, private val binRepository: BinRepository) { companion object { private const val EGG_4_5_BUCKET_LABEL = "4-5" private const val EGG_3_BUCKET_LABEL = "3" private const val EGG_OVER_10_BUCKET_LABEL = "10+" private const val EGG_7_9_BUCKET_LABEL = "7-9" private const val DELAY = 15000L // 15s private const val ACTIVE_BINS_METRIC_NAME = "c.concurrent.bins" private const val NUMBER_OF_EGGS_LABEL = "numberOfEggs" private const val ALL_ACTIVE_BINS_LABEL = "total" private const val EGG_2_BUCKET_LABEL = "2" private const val EGG_1_BUCKET_LABEL = "1" private val numberOfEggsBuckets = setOf( EGG_4_5_BUCKET_LABEL, EGG_2_BUCKET_LABEL, EGG_3_BUCKET_LABEL, EGG_7_9_BUCKET_LABEL, EGG_1_BUCKET_LABEL, EGG_OVER_10_BUCKET_LABEL, ALL_ACTIVE_BINS_LABEL) } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/file_structure/CompanionObjectWithCommentTest.kt ================================================ package com.saveourtool.diktat import io.micrometer.core.instrument.MeterRegistry import org.springframework.stereotype.Component /** * @param meterRegistry * @param binRepository */ @Component class ActiveBinsMetric(meterRegistry: MeterRegistry, private val binRepository: BinRepository) { companion object { private const val EGG_4_5_BUCKET_LABEL = "4-5" private const val EGG_3_BUCKET_LABEL = "3" private const val EGG_OVER_10_BUCKET_LABEL = "10+" private const val EGG_7_9_BUCKET_LABEL = "7-9" private const val DELAY = 15000L // 15s private const val ACTIVE_BINS_METRIC_NAME = "c.concurrent.bins" private const val NUMBER_OF_EGGS_LABEL = "numberOfEggs" private const val ALL_ACTIVE_BINS_LABEL = "total" private const val EGG_2_BUCKET_LABEL = "2" private const val EGG_1_BUCKET_LABEL = "1" private val numberOfEggsBuckets = setOf( EGG_4_5_BUCKET_LABEL, EGG_2_BUCKET_LABEL, EGG_3_BUCKET_LABEL, EGG_7_9_BUCKET_LABEL, EGG_1_BUCKET_LABEL, EGG_OVER_10_BUCKET_LABEL, ALL_ACTIVE_BINS_LABEL) } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/file_structure/CopyrightCommentPositionExpected.kt ================================================ /* * Copyright. All rights reserved. */ @file:JvmName("Foo") package test.paragraph2.file_structure class Example ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/file_structure/CopyrightCommentPositionTest.kt ================================================ @file:JvmName("Foo") /* * Copyright. All rights reserved. */ package test.paragraph2.file_structure class Example ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/file_structure/DeclarationsInClassOrderExpected.kt ================================================ package test.paragraph3.file_structure class Example { private val log = LoggerFactory.getLogger(Example.javaClass) // lorem ipsum private val FOO = 42 private val BAR = 43 /** * Dolor sit amet */ private val BAZ = 44 private lateinit var lateFoo: Int init { bar() } constructor(baz: Int) fun foo() { val nested = Nested() } class Nested { val nestedFoo = 43 } companion object { private const val ZERO = 0 private var i = 0 } class UnusedNested { } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/file_structure/DeclarationsInClassOrderTest.kt ================================================ package test.paragraph3.file_structure class Example { init { bar() } companion object { private const val ZERO = 0 private var i = 0 } private lateinit var lateFoo: Int // lorem ipsum private val FOO = 42 private val log = LoggerFactory.getLogger(Example.javaClass) class UnusedNested { } constructor(baz: Int) fun foo() { val nested = Nested() } class Nested { val nestedFoo = 43 } private val BAR = 43 /** * Dolor sit amet */ private val BAZ = 44 } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/file_structure/DefaultPackageWithImportsExpected.kt ================================================ /* Copyright */ @file:Suppress("") import baz.Baz import foo.Bar /** * KDoc */ class Example{ val x: Bar = Bar() val y: Baz = Baz() } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/file_structure/DefaultPackageWithImportsTest.kt ================================================ @file:Suppress("") /* Copyright */ import foo.Bar import baz.Baz /** * KDoc */ class Example{ val x: Bar = Bar() val y: Baz = Baz() } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/file_structure/FileAnnotationExpected.kt ================================================ /** * This is an example */ @file:JvmName("Foo") package test.paragraph2.file_structure class Example ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/file_structure/FileAnnotationTest.kt ================================================ @file:JvmName("Foo") /** * This is an example */ package test.paragraph2.file_structure class Example ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/file_structure/HeaderKdocAfterPackageExpected.kt ================================================ /** * Header Kdoc */ package test.paragraph3.file_structure import com.saveourtool.diktat.Bar class A { val a = Bar() } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/file_structure/HeaderKdocAfterPackageTest.kt ================================================ package test.paragraph3.file_structure /** * Header Kdoc */ import com.saveourtool.diktat.Bar class A { val a = Bar() } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/file_structure/LoggerOrderExpected.kt ================================================ package test.paragraph3.file_structure import org.junit.platform.commons.logging.LoggerFactory class LoggerOrderExample { private val logger = getLogger("string template") private val logger = getLogger("""string template""") private val logger = getLogger(""" string template """) private val logger = getLogger(this.javaClass) private val logger = getLogger(this.javaClass.name) private val logger = getLogger(javaClass) private val logger = getLogger(javaClass.name) private val logger = getLogger(Foo::class.java) private val logger = getLogger(Foo::class.java.name) private val logger = getLogger(Foo::class) private val logger = getLogger(Foo::class.name) private val logger = getLogger() private val logger = getLogger({}.javaClass) private val a = "a" private val b = "b" private val c = "c" private val d = "d" private val e = "e" private val f = "f" private val g = "g" private val h = "h" private val i = "i" private val j = "j" private val k = "k" private val l = "l" private val m = "m" private val logger = LoggerFactory.getLogger(m) private val n = "n" private val logger = n.getLogger() private val o = "o" private val logger = o { get() } private val p = "p" private val logger = p::class.java.enclosingClass private val q = "q" private val logger = q::class.java private val r = "r" private val logger = r::javaClass private val s = "s" private val t = "t" private val logger = getLogger("$s$t") private val u = "u" private val v = "v" private val w = "w" private val x = "x" private val y = "y" private val z = "z" private val logger = getLogger("$s$t$u$v$w") { x + y + z } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/file_structure/LoggerOrderTest.kt ================================================ package test.paragraph3.file_structure import org.junit.platform.commons.logging.LoggerFactory class LoggerOrderExample { private val logger = getLogger("string template") private val a = "a" private val logger = getLogger("""string template""") private val b = "b" private val logger = getLogger(""" string template """) private val c = "c" private val logger = getLogger(this.javaClass) private val d = "d" private val logger = getLogger(this.javaClass.name) private val e = "e" private val logger = getLogger(javaClass) private val f = "f" private val logger = getLogger(javaClass.name) private val g = "g" private val logger = getLogger(Foo::class.java) private val h = "h" private val logger = getLogger(Foo::class.java.name) private val i = "i" private val logger = getLogger(Foo::class) private val j = "j" private val logger = getLogger(Foo::class.name) private val k = "k" private val logger = getLogger() private val l = "l" private val logger = getLogger({}.javaClass) private val m = "m" private val logger = LoggerFactory.getLogger(m) private val n = "n" private val logger = n.getLogger() private val o = "o" private val logger = o { get() } private val p = "p" private val logger = p::class.java.enclosingClass private val q = "q" private val logger = q::class.java private val r = "r" private val logger = r::javaClass private val s = "s" private val t = "t" private val logger = getLogger("$s$t") private val u = "u" private val v = "v" private val w = "w" private val x = "x" private val y = "y" private val z = "z" private val logger = getLogger("$s$t$u$v$w") { x + y + z } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/file_structure/MissingBlankLinesBetweenBlocksTest.kt ================================================ /** * This is an example */ @file:JvmName("Foo") package com.saveourtool.diktat.example import com.saveourtool.diktat.Foo class Example { val y: Foo = Foo() } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/file_structure/NoImportNoPackageExpected.kt ================================================ /* Copyright */ @file:Suppress("") /** * KDoc */ class Example ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/file_structure/NoImportNoPackageTest.kt ================================================ @file:Suppress("") /* Copyright */ /** * KDoc */ class Example ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/file_structure/OrderWithCommentExpected.kt ================================================ package test.paragraph3.file_structure class Example { companion object { private val log = LoggerFactory.getLogger(Example.javaClass) val q = 10 /* * Dolor sit amet */ /** * hi */ private val qq = 10 } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/file_structure/OrderWithCommentTest.kt ================================================ package test.paragraph3.file_structure class Example { companion object { val q = 10 private val log = LoggerFactory.getLogger(Example.javaClass) /* * Dolor sit amet */ /** * hi */ private val qq = 10 } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/file_structure/OrderWithEnumsExpected.kt ================================================ enum class Enum { FOO, BAR, ; fun f() {} companion object } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/file_structure/OrderWithEnumsTest.kt ================================================ enum class Enum { FOO, BAR, ; companion object fun f() {} } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/file_structure/OtherCommentsExpected.kt ================================================ /** * This is an example */ // some notes on this file // and some more package com.saveourtool.diktat.example import com.saveourtool.diktat.Foo class Example { val x: Foo = Foo() } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/file_structure/OtherCommentsTest.kt ================================================ // some notes on this file // and some more /** * This is an example */ package com.saveourtool.diktat.example import com.saveourtool.diktat.Foo class Example { val x: Foo = Foo() } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/file_structure/RedundantBlankLinesBetweenBlocksTest.kt ================================================ /** * This is an example */ @file:JvmName("Foo") package com.saveourtool.diktat.example import com.saveourtool.diktat.Foo class Example { val y: Foo = Foo() } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/file_structure/ReorderingImportsExpected.kt ================================================ package test.paragraph3.file_structure // lorem ipsum import com.saveourtool.diktat.example.Bar import com.saveourtool.diktat.example.Foo import org.junit.jupiter.api.Test class Example { val x: Test = Test() val y: Foo = Foo() val z: Bar = Bar() } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/file_structure/ReorderingImportsRecommendedExpected.kt ================================================ package test.paragraph3.file_structure // comment about java imports import android.* import androidx.* import com.android.* import com.saveourtool.* import com.saveourtool.diktat.* import com.fasterxml.jackson.databind.ObjectMapper import com.google.common.base.CaseFormat import io.gitlab.arturbosch.detekt.Detekt import org.junit.jupiter.api.Assertions import org.slf4j.Logger import org.springframework.context.annotation.Bean import java.io.IOException import java.net.URL import java.nio.charset.Charset import kotlin.system.exitProcess class Example { val x = setOf(CaseFormat(), Detekt(), Assertions(), Logger(), Bean(), IOException(), URL(), Charset(), ObjectMapper()) fun Foo() { while (true) { exitProcess(1) } } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/file_structure/ReorderingImportsRecommendedTest.kt ================================================ package test.paragraph3.file_structure import com.android.* import kotlin.system.exitProcess // comment about java imports import java.io.IOException import java.net.URL import com.saveourtool.* import com.fasterxml.jackson.databind.ObjectMapper import android.* import com.saveourtool.diktat.* import org.junit.jupiter.api.Assertions import androidx.* import org.springframework.context.annotation.Bean import com.google.common.base.CaseFormat import java.nio.charset.Charset import io.gitlab.arturbosch.detekt.Detekt import org.slf4j.Logger class Example { val x = setOf(CaseFormat(), Detekt(), Assertions(), Logger(), Bean(), IOException(), URL(), Charset(), ObjectMapper()) fun Foo() { while (true) { exitProcess(1) } } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/file_structure/ReorderingImportsTest.kt ================================================ package test.paragraph3.file_structure import org.junit.jupiter.api.Test import com.saveourtool.diktat.example.Foo // lorem ipsum import com.saveourtool.diktat.example.Bar class Example { val x: Test = Test() val y: Foo = Foo() val z: Bar = Bar() } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/file_structure/ScriptPackageDirectiveExpected.kts ================================================ @file:Suppress("DSL_SCOPE_VIOLATION")// Without suppressing these, version catalog usage in `plugins` is marked as an error in IntelliJ: // https://youtrack.jetbrains.com/issue/KTIJ-19369 plugins { id(libs.plugins.kotlinJvm.get().pluginId) } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/file_structure/ScriptPackageDirectiveTest.kts ================================================ // Without suppressing these, version catalog usage in `plugins` is marked as an error in IntelliJ: // https://youtrack.jetbrains.com/issue/KTIJ-19369 @file:Suppress("DSL_SCOPE_VIOLATION") plugins { id(libs.plugins.kotlinJvm.get().pluginId) } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/indentation/ConstructorExpected.kt ================================================ package test.paragraph3.indentation class Example(protected val property1: Type1, private val property2: Type2, property3: Type3) { constructor(property1: Type1, property2: Type2) : this(property1, property2, defaultValue) } sealed class SealedExample { class Subclass1(val property1: Type1, val property2: Type) : SealedExample() class Subclass(val property1: Type1, val property2: Type2, val property3: Type3, val property4: Type4) : SealedExample() } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/indentation/ConstructorTest.kt ================================================ package test.paragraph3.indentation class Example(protected val property1: Type1, private val property2: Type2, property3: Type3) { constructor(property1: Type1, property2: Type2) : this(property1, property2, defaultValue) } sealed class SealedExample { class Subclass1(val property1: Type1, val property2: Type) : SealedExample() class Subclass(val property1: Type1, val property2: Type2, val property3: Type3, val property4: Type4) : SealedExample() } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/indentation/IndentFullExpected.kt ================================================ package test.paragraph3.indentation @Deprecated("Use NewFoo instead") class Foo : Comparable, Appendable { val test = 12 @Deprecated("Foo") fun foo1( i1: Int, i2: Int, i3: Int ): Int { when (i1) { is Number -> 0 else -> 1 } if (i2 > 0 && i3 < 0 ) { return 2 } return 0 } private fun foo2(): Int { // todo: something try { return foo1( 12, 13, 14 ) } catch (e: Exception) { return 0 } finally { if (true) { return 1 } else { return 2 } } } fun foo3() { Integer .parseInt("32").let { println("parsed $it") } } private val f = { a: Int -> a * 2 } fun longMethod( @Named("param1") param1: Int, param2: String ) { @Deprecated val foo = 1 } fun multilineMethod( foo: String, bar: String?, x: Int? ) { foo.toUpperCase() .trim() .length val barLen = bar?.length ?: x ?: -1 if (foo.length > 0 && barLen > 0 ) { println("> 0") } } } @Deprecated val bar = 1 enum class Enumeration { A, B } fun veryLongExpressionBodyMethod() = "abc" ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/indentation/IndentFullTest.kt ================================================ package test.paragraph3.indentation @Deprecated("Use NewFoo instead") class Foo : Comparable, Appendable { val test = 12 @Deprecated("Foo") fun foo1( i1: Int, i2: Int, i3: Int ): Int { when (i1) { is Number -> 0 else -> 1 } if (i2 > 0 && i3 < 0 ) { return 2 } return 0 } private fun foo2(): Int { // todo: something try { return foo1( 12, 13, 14 ) } catch (e: Exception) { return 0 } finally { if (true) { return 1 } else { return 2 } } } fun foo3() { Integer .parseInt("32").let { println("parsed $it") } } private val f = { a: Int -> a * 2 } fun longMethod( @Named("param1") param1: Int, param2: String ) { @Deprecated val foo = 1 } fun multilineMethod( foo: String, bar: String?, x: Int? ) { foo.toUpperCase() .trim() .length val barLen = bar?.length ?: x ?: -1 if (foo.length > 0 && barLen > 0 ) { println("> 0") } } } @Deprecated val bar = 1 enum class Enumeration { A, B } fun veryLongExpressionBodyMethod() = "abc" ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/indentation/IndentationFull1Expected.kt ================================================ package test.paragraph3.indentation data class Example(val field1: Type1, val field2: Type2) { /** * Lorem ipsum * dolor sit amet */ fun foo( a: Int, b: Int ): Int { return a + b } fun bar() { for (i in 1..100) println(i) do println() while (condition) for (i in 1..100) { println(i) } } fun baz() { if (condition) foobar() else foobaz() } fun some() { val a = "${ foo().bar() }" val b = "${baz().foo()}" val c = "${ expression .foo() .bar() }" } val dockerFileAsText = """ FROM $baseImage COPY resources $resourcesPath RUN /bin/bash """.trimIndent() val some = """ some $foo test $start another value """.trimIndent() val teeest = """ some text $foo $bar another text """.trimIndent() } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/indentation/IndentationFull1Test.kt ================================================ package test.paragraph3.indentation data class Example(val field1: Type1, val field2: Type2) { /** * Lorem ipsum * dolor sit amet */ fun foo( a: Int, b: Int ): Int { return a + b } fun bar() { for (i in 1..100) println(i) do println() while (condition) for (i in 1..100) { println(i) } } fun baz() { if (condition) foobar() else foobaz() } fun some() { val a = "${ foo().bar() }" val b = "${baz().foo()}" val c = "${ expression .foo() .bar() }" } val dockerFileAsText = """ FROM $baseImage COPY resources $resourcesPath RUN /bin/bash """.trimIndent() val some = """ some $foo test $start another value """.trimIndent() val teeest = """ some text $foo $bar another text """.trimIndent() } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/indentation/IndentationParametersExpected.kt ================================================ package test.paragraph3.indentation class Test(val param1: Type, val param2: Type, val param3: Type) fun test( param1: Type, param2: Type, param3: Type ) { val test = Test(param1, param2, param3) } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/indentation/IndentationParametersTest.kt ================================================ package test.paragraph3.indentation class Test(val param1: Type, val param2: Type, val param3: Type) fun test( param1: Type, param2: Type, param3: Type ) { val test = Test(param1, param2, param3) } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/long_line/LongBinaryExpressionExpected.kt ================================================ package test.paragraph3.long_line fun foo() { val veryLongExpression = Methoooooooooooooooood() + 12345 val veryLongExpression = Methoooooooooooooooood() ?: null val veryLongExpression = a.Methooooood() + b.field val variable = someField.filter { it.elementType == KDOC } // limit at the left side val variable = a?.filter { it.elementType == KDOC } ?: null // limit at the right side val variable = bar?.filter { it.b == c } ?: null // limit at the operation reference val variable = field?.filter { bar == foo } ?: null val variable = field?.filter { bar == foo } ?: null val variable = Methooood() * 2 + 12 + field ?: 123 + Methood().linelength val variable = Methooood() * 2 + 12 + field ?: 123 + Methood().linelength val variable = Methoooooooooooooooooooooooooood() ?: "some loooooong string" val variable = Methooooood() ?: "some looong string" var headerKdoc = firstCodeNode.prevSibling { it.elementType == KDOC } ?: if (firstCodeNode == packageDirectiveNode) importsList?.prevSibling { it.elementType == KDOC } else null } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/long_line/LongBinaryExpressionLastWordExpected.kt ================================================ package test.paragraph3.long_line val elem1 = (hasExplicitNotUnitReturnType || isFunWithExpressionBody && !hasExplicitUnitReturnType && hasNotExpressionBodyTypes) && !hasReturnKdoc && !isReferenceExpressionWithSameName val elem2 = (hasExplicitNotUnitReturnType || isFunWithExpressionBody1 && !hasExplicitUnitReturnType && hasNotExpressionBodyTypes) && !hasReturnKdoc && isReferenceExpressionWithSameName val elem3 = "sdfghjkl;kjhgfdsdfghjkllkjhgfdsfghjkl;';lkiuytrdfghjklkjuhgfdsdfghnm,.lkjhgfdcvbnm,.lkjhgfdcvbnm,.lkjhgfdxcvbnm,lkjhgfdxcvbnm,lkgfdcvm" + "hgfjdgdsvfmg.k,gfdsgbh.gkhjhmhgdf" ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/long_line/LongBinaryExpressionLastWordTest.kt ================================================ package test.paragraph3.long_line val elem1 = (hasExplicitNotUnitReturnType || isFunWithExpressionBody && !hasExplicitUnitReturnType && hasNotExpressionBodyTypes) && !hasReturnKdoc && !isReferenceExpressionWithSameName val elem2 = (hasExplicitNotUnitReturnType || isFunWithExpressionBody1 && !hasExplicitUnitReturnType && hasNotExpressionBodyTypes) && !hasReturnKdoc && isReferenceExpressionWithSameName val elem3 = "sdfghjkl;kjhgfdsdfghjkllkjhgfdsfghjkl;';lkiuytrdfghjklkjuhgfdsdfghnm,.lkjhgfdcvbnm,.lkjhgfdcvbnm,.lkjhgfdxcvbnm,lkjhgfdxcvbnm,lkgfdcvm" + "hgfjdgdsvfmg.k,gfdsgbh.gkhjhmhgdf" ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/long_line/LongBinaryExpressionTest.kt ================================================ package test.paragraph3.long_line fun foo() { val veryLongExpression = Methoooooooooooooooood() + 12345 val veryLongExpression = Methoooooooooooooooood() ?: null val veryLongExpression = a.Methooooood() + b.field val variable = someField.filter { it.elementType == KDOC } // limit at the left side val variable = a?.filter { it.elementType == KDOC } ?: null // limit at the right side val variable = bar?.filter { it.b == c } ?: null // limit at the operation reference val variable = field?.filter { bar == foo } ?: null val variable = field?.filter { bar == foo }?: null val variable = Methooood() * 2 + 12 + field ?: 123 + Methood().linelength val variable = Methooood() * 2 + 12 + field?: 123 + Methood().linelength val variable = Methoooooooooooooooooooooooooood() ?: "some loooooong string" val variable = Methooooood() ?: "some looong string" var headerKdoc = firstCodeNode.prevSibling { it.elementType == KDOC } ?: if (firstCodeNode == packageDirectiveNode) importsList?.prevSibling { it.elementType == KDOC } else null } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/long_line/LongComplexExpressionExpected.kt ================================================ package test.paragraph3.long_line fun foo(){ val attrs.disabled = (selfRole == Role.OWNER && isSelfRecord(props.selfUserInfo, user)) || !(selfRole.isHigherOrEqualThan(Role.OWNER) || userRole.isLowerThan(selfRole)) } val attrs.disabled = (selfRole == Role.OWNER && isSelfRecord(props.selfUserInfo, user)) || !(selfRole.isHigherOrEqualThan(Role.OWNER) || userRole.isLowerThan(selfRole)) ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/long_line/LongComplexExpressionTest.kt ================================================ package test.paragraph3.long_line fun foo(){ val attrs.disabled = (selfRole == Role.OWNER && isSelfRecord(props.selfUserInfo, user)) || !(selfRole.isHigherOrEqualThan(Role.OWNER) || userRole.isLowerThan(selfRole)) } val attrs.disabled = (selfRole == Role.OWNER && isSelfRecord(props.selfUserInfo, user)) || !(selfRole.isHigherOrEqualThan(Role.OWNER) || userRole.isLowerThan(selfRole)) ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/long_line/LongConditionInSmallFunctionExpected.kt ================================================ package test.paragraph3.long_line private fun isContainingRequiredPartOfCode(text: String): Boolean = text.contains("val ", true) || text.contains("var ", true) || text.contains("=", true) || (text.contains("{", true) && text.substringAfter("{").contains("}", true)) ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/long_line/LongConditionInSmallFunctionTest.kt ================================================ package test.paragraph3.long_line private fun isContainingRequiredPartOfCode(text: String): Boolean = text.contains("val ", true) || text.contains("var ", true) || text.contains("=", true) || (text.contains("{", true) && text.substringAfter("{").contains("}", true)) ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/long_line/LongDotQualifiedExpressionExpected.kt ================================================ package test.paragraph3.long_line val G = ThisIsVeryyyyLooooonNameDooootQualifiedExpressioWithoutDot.lalalala.lalalal val A = This.Is.Veeeeryyyyyyy.Loooooong.Dot .Qualified.Expression val B = This?.Is?.Veeeeryyyyyyy?.Loooooong?.Dot ?.Qualified?.Expression val C = This!!.Is!!.Veeeeryyyyyyy!!.Loooooong!! .Dot!!.Qualified!!.Expression val D = This.Is.Veeeeryyyyyyy.Loooooong.Dot .Qualified.Expression val E = This?.Is?.Veeeeryyyyyyy?.Loooooong?.Dot ?.Qualified?.Expression val F = This!!.Is!!.Veeeeryyyyyyy!!.Loooooong!! .Dot!!.Qualified!!.Expression ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/long_line/LongDotQualifiedExpressionTest.kt ================================================ package test.paragraph3.long_line val G = ThisIsVeryyyyLooooonNameDooootQualifiedExpressioWithoutDot.lalalala.lalalal val A = This.Is.Veeeeryyyyyyy.Loooooong.Dot.Qualified.Expression val B = This?.Is?.Veeeeryyyyyyy?.Loooooong?.Dot?.Qualified?.Expression val C = This!!.Is!!.Veeeeryyyyyyy!!.Loooooong!!.Dot!!.Qualified!!.Expression val D = This.Is.Veeeeryyyyyyy.Loooooong.Dot .Qualified.Expression val E = This?.Is?.Veeeeryyyyyyy?.Loooooong?.Dot ?.Qualified?.Expression val F = This!!.Is!!.Veeeeryyyyyyy!!.Loooooong!! .Dot!!.Qualified!!.Expression ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/long_line/LongExpressionInConditionExpected.kt ================================================ package test.paragraph3.long_line fun foo() { val veryLooongStringName = "ASDFGHJKL" val veryLooooooongConstIntName1 = 12345 val veryLooooooongConstIntName2 = 54321 var carry = 1 if (veryLooooooongConstIntName1 > veryLooooooongConstIntName2) { carry++ } else if (veryLooooooongConstIntName2 > 123 * 12 && veryLooongStringName != "asd") { carry+=2 } else if (1234 + 1235 + 1236 + 1237 + 1238 > 124 * 12) { carry+=3 } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/long_line/LongExpressionInConditionTest.kt ================================================ package test.paragraph3.long_line fun foo() { val veryLooongStringName = "ASDFGHJKL" val veryLooooooongConstIntName1 = 12345 val veryLooooooongConstIntName2 = 54321 var carry = 1 if (veryLooooooongConstIntName1 > veryLooooooongConstIntName2) { carry++ } else if (veryLooooooongConstIntName2 > 123 * 12 && veryLooongStringName != "asd") { carry+=2 } else if (1234 + 1235 + 1236 + 1237 + 1238 > 124 * 12) { carry+=3 } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/long_line/LongExpressionNoFixExpected.kt ================================================ package com.saveourtool.diktat.resources.paragraph3.longline class veryLoooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong { // looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongggggggggggggggggggggggggg //looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongggggggggggggggggggggggggg val s = "d s d d d d ddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/long_line/LongExpressionNoFixTest.kt ================================================ package com.saveourtool.diktat.resources.paragraph3.longline class veryLoooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong { // looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongggggggggggggggggggggggggg //looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongggggggggggggggggggggggggg val s = "d s d d d d ddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/long_line/LongInlineCommentsExpected.kt ================================================ package test.paragraph3.long_line fun foobar() { foo( // ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff bar() ) } fun foobar() { foo( //ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff bar() ) } fun foobar() { foo( // ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff bar(), // uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu baz() ) } fun foobar() { // ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff bar() // uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu baz() } enum class SomeEnum { // ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff FOO, // ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff BAR, // ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff BAZ, }; fun foo() { when (node.elementType) { ElementType.CLASS, ElementType.FUN, ElementType.PROPERTY -> checkBlankLineAfterKdoc(node) // ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff ElementType.IF -> handleIfElse(node) ElementType.EOL_COMMENT, ElementType.BLOCK_COMMENT -> handleEolAndBlockComments(node, configuration) ElementType.KDOC -> handleKdocComments(node, configuration) // ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff else -> { // this is a generated else block } } } // aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa fun foo() { // bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb if (a) { // cccccccccccccccccccccccccccccccccccccccccc a() // dddddddddddddddddddddddddddddddddddddddddd } else { // eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee b() // ffffffffffffffffffffffffffffffffffffffffff } // gggggggggggggggggggggggggggggggggggggggggggggggggggg } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/long_line/LongInlineCommentsTest.kt ================================================ package test.paragraph3.long_line fun foobar() { foo( bar() // ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff ) } fun foobar() { foo( bar()//ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff ) } fun foobar() { foo( bar(), // ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff baz() // uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu ) } fun foobar() { bar() // ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff baz() // uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu } enum class SomeEnum { FOO, // ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff BAR, // ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff BAZ, // ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff }; fun foo() { when (node.elementType) { ElementType.CLASS, ElementType.FUN, ElementType.PROPERTY -> checkBlankLineAfterKdoc(node) ElementType.IF -> handleIfElse(node) // ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff ElementType.EOL_COMMENT, ElementType.BLOCK_COMMENT -> handleEolAndBlockComments(node, configuration) ElementType.KDOC -> handleKdocComments(node, configuration) else -> { // ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff // this is a generated else block } } } fun foo() { // aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa if (a) { // bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb a() // cccccccccccccccccccccccccccccccccccccccccc } else { // dddddddddddddddddddddddddddddddddddddddddd b() // eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee } // ffffffffffffffffffffffffffffffffffffffffff } // gggggggggggggggggggggggggggggggggggggggggggggggggggg ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/long_line/LongLineAnnotationExpected.kt ================================================ package test.paragraph3.long_line @Query( value = "select * from test inner join test_execution on test.id = test_execution.test_id and test_execution.st", nativeQuery = true ) fun retrieveBatches(limit: Int, offset: Int, executionId: Long): Some @Query( value = "select * from test inner join test_execution on test.id = test_execution.test_id and test_execution.status = 'READY' and test_execution.test_suite_execution_id = ?3 limit ?1 offset ?2", nativeQuery = true ) fun some(limit: Int, offset: Int, executionId: Long): List @Query(value = "select * from test inner joi", nativeQuery = true) fun test(limit: Int, offset: Int, executionId: Long): List @Query(value = "select * from test inner joibbb", nativeQuery = true) fun cornerCase(limit: Int, offset: Int, executionId: Long): List @Query( value = "select * from test inner join test_execution on test.id = test_execution.test_id and test_execution.status = 'READY' and test_execution.test_suite_execution_id = ?3 limit ?1 offset ?2", nativeQuery = true ) fun some(limit: Int, offset: Int, executionId: Long) = println( "testtesttesttesttesttesttesttesttesttesttesttest" ) ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/long_line/LongLineAnnotationTest.kt ================================================ package test.paragraph3.long_line @Query(value = "select * from test inner join test_execution on test.id = test_execution.test_id and test_execution.st", nativeQuery = true) fun retrieveBatches(limit: Int, offset: Int, executionId: Long): Some @Query(value = "select * from test inner join test_execution on test.id = test_execution.test_id and test_execution.status = 'READY' and test_execution.test_suite_execution_id = ?3 limit ?1 offset ?2", nativeQuery = true) fun some(limit: Int, offset: Int, executionId: Long): List @Query(value = "select * from test inner joi", nativeQuery = true) fun test(limit: Int, offset: Int, executionId: Long): List @Query(value = "select * from test inner joibbb", nativeQuery = true) fun cornerCase(limit: Int, offset: Int, executionId: Long): List @Query(value = "select * from test inner join test_execution on test.id = test_execution.test_id and test_execution.status = 'READY' and test_execution.test_suite_execution_id = ?3 limit ?1 offset ?2", nativeQuery = true) fun some(limit: Int, offset: Int, executionId: Long) = println("testtesttesttesttesttesttesttesttesttesttesttest") ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/long_line/LongLineCommentExpected.kt ================================================ package test.paragraph3.long_line // Hello World! This is a first part of comment. // This is a very long comment that cannot be // split fun foo() { val namesList = listOf("Jack", "Nick") namesList.forEach { name -> if (name == "Nick") { namesList.map { // This is another comment // inside map it.subSequence(0, 1) it.split("this is long regex") // this comment start to the right of max length } } } } fun goo() { val ok = true // short comment // h k comment looong comment val someLongFieldName = "some string" } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/long_line/LongLineCommentExpected2.kt ================================================ /** * This rule checks if there is a backing property for field with property accessors, in case they don't use field keyword */ class ImplicitBackingPropertyRuleTest(configRules: List) { private fun validateAccessors(node: ASTNode, propsWithBackSymbol: List) { // Comment, which should be moved val accessors = node.findAllDescendantsWithSpecificType(PROPERTY_ACCESSOR).filter { it.hasChildOfType(BLOCK) } // Comment, which should be moved val accessors2 = node.findAllDescendantsWithSpecificType(PROPERTY_ACCESSOR).filter { it.hasChildOfType(BLOCK) } var accessors3 = node.findAllDescendantsWithSpecificType(PROPERTY_ACCESSOR).filter { it.hasChildOfType(BLOCK) } .forEach { handleGetAccessors(it, node, propsWithBackSymbol) } // Comment, which shouldn't be moved // Comment, which should be moved var accessors4 = node.findAllDescendantsWithSpecificType(PROPERTY_ACCESSOR).filter { it.hasChildOfType(BLOCK) } .forEach { handleGetAccessors(it, node, propsWithBackSymbol) } var accessors5 = node.findAllDescendantsWithSpecificType(PROPERTY_ACCESSOR).filter { it.hasChildOfType(BLOCK) } .forEach { handleGetAccessors(it, node, propsWithBackSymbol) } // Comment, which shouldn't be moved accessors.filter { it.hasChildOfType(GET_KEYWORD) }.forEach { handleGetAccessors(it, node, propsWithBackSymbol) } accessors.filter { it.hasChildOfType(SET_KEYWORD) }.forEach { handleSetAccessors(it, node, propsWithBackSymbol) } } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/long_line/LongLineCommentTest.kt ================================================ package test.paragraph3.long_line // Hello World! This is a first part of comment. This is a very long comment that cannot be split fun foo() { val namesList = listOf("Jack", "Nick") namesList.forEach { name -> if (name == "Nick") { namesList.map { it.subSequence(0, 1) // This is another comment inside map it.split("this is long regex") // this comment start to the right of max length } } } } fun goo() { val ok = true // short comment val someLongFieldName = "some string" // h k comment looong comment } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/long_line/LongLineCommentTest2.kt ================================================ /** * This rule checks if there is a backing property for field with property accessors, in case they don't use field keyword */ class ImplicitBackingPropertyRuleTest(configRules: List) { private fun validateAccessors(node: ASTNode, propsWithBackSymbol: List) { val accessors = node.findAllDescendantsWithSpecificType(PROPERTY_ACCESSOR).filter { it.hasChildOfType(BLOCK) } // Comment, which should be moved val accessors2 = node.findAllDescendantsWithSpecificType(PROPERTY_ACCESSOR).filter { it.hasChildOfType(BLOCK) } // Comment, which should be moved var accessors3 = node.findAllDescendantsWithSpecificType(PROPERTY_ACCESSOR).filter { it.hasChildOfType(BLOCK) }.forEach { handleGetAccessors(it, node, propsWithBackSymbol) } // Comment, which shouldn't be moved var accessors4 = node.findAllDescendantsWithSpecificType(PROPERTY_ACCESSOR).filter { it.hasChildOfType(BLOCK) }.forEach { handleGetAccessors(it, node, propsWithBackSymbol) } // Comment, which should be moved var accessors5 = node.findAllDescendantsWithSpecificType(PROPERTY_ACCESSOR).filter { it.hasChildOfType(BLOCK) }.forEach { handleGetAccessors(it, node, propsWithBackSymbol) } // Comment, which shouldn't be moved accessors.filter { it.hasChildOfType(GET_KEYWORD) }.forEach { handleGetAccessors(it, node, propsWithBackSymbol) } accessors.filter { it.hasChildOfType(SET_KEYWORD) }.forEach { handleSetAccessors(it, node, propsWithBackSymbol) } } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/long_line/LongLineExpressionExpected.kt ================================================ package test.paragraph3.long_line fun foo() { if (( x > 4365873654863745683)|| y<238479283749238&&!isFoo()){} if (( x > 4365873654863745683) || y<238479283749238 && !isFoo()){} if (q.text == "dc" && !IsFoo() || x > 238479283749238 && y < 238479283749238 || g == "text"){} if (q.text == "dc" && !IsFoo() || x > 238479283749238 && y < 238479283749238 || g == "text"){} if (d == "very long text" && gh == "very long text" || x > 238479283749238 || y< 238479283749238){} if (x == 2384792837492387498728947289472987492){} if (x == 972938473924535278492792738497){} if (x == "veery long text to split" || x == "es" || y == 123 || b > 12 && jh==234 || h==54){} if ((x > 0 || y< 43658736548637456831231231223) || (y < 123123132 && x > 123123132)){} if ((x > 0 || ( y< 43658 )) && (y < 123123132 && x > 123123132)){} if (x > 0 || (y< 43658) && (y < 123123132 && x > 123123132)){} if (x > 0 || (y< 43658) && x!! || (y < 123123)){} if (x > 0 || (y< 43658) && (y < 123123) || x!!){} if (x > 0 || (y< 43658) && (y < 1231) || (x!!)){} } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/long_line/LongLineExpressionTest.kt ================================================ package test.paragraph3.long_line fun foo() { if (( x > 4365873654863745683)||y<238479283749238&&!isFoo()){} if (( x > 4365873654863745683) || y<238479283749238 && !isFoo()){} if (q.text == "dc" && !IsFoo() || x > 238479283749238 && y < 238479283749238 || g == "text"){} if (q.text == "dc" && !IsFoo() || x > 238479283749238 && y < 238479283749238 || g == "text"){} if (d == "very long text" && gh == "very long text" || x > 238479283749238 || y< 238479283749238){} if (x == 2384792837492387498728947289472987492){} if (x == 972938473924535278492792738497){} if (x == "veery long text to split" || x == "es" || y == 123 || b > 12 && jh==234 || h==54){} if ((x > 0 || y< 43658736548637456831231231223) || (y < 123123132 && x > 123123132)){} if ((x > 0 || ( y< 43658 )) && (y < 123123132 && x > 123123132)){} if (x > 0 || (y< 43658) && (y < 123123132 && x > 123123132)){} if (x > 0 || (y< 43658) && x!! || (y < 123123)){} if (x > 0 || (y< 43658) && (y < 123123) || x!!){} if (x > 0 || (y< 43658) && (y < 1231) || (x!!)){} } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/long_line/LongLineFunExpected.kt ================================================ package test.paragraph3.long_line fun foo() = println( "fhdbsfkhfbvsjfkvbhjdksfvhbhhjhjhjnaljfbkshvjdsjdnlvbdkhkjncdkljskbfvhdsjndlvfkbdhfjdncjsdcscsdcsdcsdcdd" ) fun foo () { println( "fhdbsfkhfbvsjfkvbhjdksfvhbhhjhjhjnaljfbkshvjdsjdnlvbdkhkjncdkljskbfvhdsjndlvfkbdhfjdncjsdcscsdcsdcsdcdd" ) } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/long_line/LongLineFunTest.kt ================================================ package test.paragraph3.long_line fun foo() = println("fhdbsfkhfbvsjfkvbhjdksfvhbhhjhjhjnaljfbkshvjdsjdnlvbdkhkjncdkljskbfvhdsjndlvfkbdhfjdncjsdcscsdcsdcsdcdd") fun foo () { println("fhdbsfkhfbvsjfkvbhjdksfvhbhhjhjhjnaljfbkshvjdsjdnlvbdkhkjncdkljskbfvhdsjndlvfkbdhfjdncjsdcscsdcsdcsdcdd") } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/long_line/LongLineRValueExpected.kt ================================================ package test.paragraph3.long_line fun foo() { val str = "This is very long string that" + " should be split" fun foo() { val veryLoooooooooooooooooongNamesList = listOf("Jack", "Nick") veryLoooooooooooooooooongNamesList .forEach { name -> if (name == "Nick") { veryLoooooooooooooooooongNamesList .map { val str = "This string shouldn't be split"} name.map { val str = "This string should be split" } } } } val longIntExpression = 12345 + 12345 + 12345 + 12345 val longIntExpression = (12345 + 12345 + 12345 + 12345) val longIntExpression = (12345) + (12345) + (12345) + (12345) val LongWithVar2 = "very long" + " woooooordsdcsdcsdcsdc $variable" val longStringExpression = "First part" + "second Part" val longStringExpression = "First" + "second Part" val longStringExpression = "First very long part" + "second Part" val longStringExpression2 = "String starts at the line len limit" val veryLooooooooooooooooooooooooooooooongVal = "text" val veryLongExpression = Method() + 12345 + baaar() val veryLongExpression = Method() + baaar() + 12345 } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/long_line/LongLineRValueTest.kt ================================================ package test.paragraph3.long_line fun foo() { val str = "This is very long string that should be split" fun foo() { val veryLoooooooooooooooooongNamesList = listOf("Jack", "Nick") veryLoooooooooooooooooongNamesList.forEach { name -> if (name == "Nick") { veryLoooooooooooooooooongNamesList.map { val str = "This string shouldn't be split"} name.map { val str = "This string should be split" } } } } val longIntExpression = 12345 + 12345 + 12345 + 12345 val longIntExpression = (12345 + 12345 + 12345 + 12345) val longIntExpression = (12345) + (12345) + (12345) + (12345) val LongWithVar2 = "very long woooooordsdcsdcsdcsdc $variable" val longStringExpression = "First part" + "second Part" val longStringExpression = "First" + "second Part" val longStringExpression = "First very long part" + "second Part" val longStringExpression2 = "String starts at the line len limit" val veryLooooooooooooooooooooooooooooooongVal = "text" val veryLongExpression = Method() + 12345 + baaar() val veryLongExpression = Method() + baaar() + 12345 } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/long_line/LongShortRValueExpected.kt ================================================ package test.paragraph3.long_line val LongWithVar2 = "${s + "a"} is a string" ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/long_line/LongShortRValueTest.kt ================================================ package test.paragraph3.long_line val LongWithVar2 = "${s + "a"} is a string" ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/long_line/LongStringTemplateExpected.kt ================================================ object Observables { // looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongggggggggggggggggggggggggg val someCode = 15 // Some // looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong line @Deprecated( "New type inference algorithm in Kotlin 1.4 makes this method obsolete. Method will be removed in future RxKotlin release.", replaceWith = ReplaceWith("Observable.combineLatest(source1, source2, source3, source4, combineFunction)", "io.reactivex.Observable"), level = DeprecationLevel.WARNING ) @CheckReturnValue @SchedulerSupport(SchedulerSupport.NONE) inline fun combineLatest() {} } class Foo() { fun Fuu() { logger.log( "<-- ${response.code} ${ if (response.message.isEmpty()) "skfnvkdjdfvd" else "dfjvndkjnbvif" + response.message}" ) logger.log( "<-- ${response.code} ${ if (response.message.isEmpty()) "skfnvsdcsdcscskdjdfvd" else "dfjvndsdcsdcsdcskjnbvif" + response.message}" ) } val q = """ <--${respodcnsee.ccode}${if (response.mesdscsage.isEmpty()) "skfnvkdjeeeeeee" else "dfjvndksdcjnbvif" + response.mecsssdcage} """.trimIndent() val w = """ first line veryy looooooooooooooooong second line didfuhyg djfghdf gjh erughdjf gbdpfughb ergen r fgdngjkdg e g s g d g d bd jk;g,e,r gd bnsfg e """.trimIndent() val e = """ another line <--${respodcnse.ccode}${if (response.mesdscsage.isEmpty()) "skfnvkdjdsdcfvd" else "dfjvndksdcjnbvif" + response.mecsssdcage} """.trimIndent() fun foo() { val q = """ re ${ if (( x > "436587365486374568343658736548637456834365873654863745683436587365486374568343658736548637456834365873654863745683") || y<238479283749238 && !isFoo()){} } """.trimIndent() } val stringName = "This is long string template with binary expression. test should be up in level binary" + " expression and cannot split in operation reference and should be split this long string template" + "this string should be after operated reference" } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/long_line/LongStringTemplateTest.kt ================================================ object Observables { val someCode = 15 // looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongggggggggggggggggggggggggg // Some looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong line @Deprecated("New type inference algorithm in Kotlin 1.4 makes this method obsolete. Method will be removed in future RxKotlin release.", replaceWith = ReplaceWith("Observable.combineLatest(source1, source2, source3, source4, combineFunction)", "io.reactivex.Observable"), level = DeprecationLevel.WARNING) @CheckReturnValue @SchedulerSupport(SchedulerSupport.NONE) inline fun combineLatest() {} } class Foo() { fun Fuu() { logger.log("<-- ${response.code} ${ if (response.message.isEmpty()) "skfnvkdjdfvd" else "dfjvndkjnbvif" + response.message}") logger.log("<-- ${response.code} ${ if (response.message.isEmpty()) "skfnvsdcsdcscskdjdfvd" else "dfjvndsdcsdcsdcskjnbvif" + response.message}") } val q = """ <--${respodcnsee.ccode}${if (response.mesdscsage.isEmpty()) "skfnvkdjeeeeeee" else "dfjvndksdcjnbvif" + response.mecsssdcage} """.trimIndent() val w = """ first line veryy looooooooooooooooong second line didfuhyg djfghdf gjh erughdjf gbdpfughb ergen r fgdngjkdg e g s g d g d bd jk;g,e,r gd bnsfg e """.trimIndent() val e = """ another line <--${respodcnse.ccode}${if (response.mesdscsage.isEmpty()) "skfnvkdjdsdcfvd" else "dfjvndksdcjnbvif" + response.mecsssdcage} """.trimIndent() fun foo() { val q = """ re ${ if (( x > "436587365486374568343658736548637456834365873654863745683436587365486374568343658736548637456834365873654863745683") || y<238479283749238 && !isFoo()){} } """.trimIndent() } val stringName = "This is long string template with binary expression. test should be up in level binary expression and cannot split in operation reference and should be split this long string template" + "this string should be after operated reference" } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/long_line/LongValueArgumentsListExpected.kt ================================================ package test.paragraph3.long_line val firstArgument = 1 val secondArgument = 2 val thirdArgument = 3 val fourthArgument = 4 val fifthArguments = 5 val sixthArguments = 6 val seventhArguments = 7 val eighthArguments = 8 // Many arguments in function val result1 = ManyParamInFunction(firstArgument, secondArgument, thirdArgument, fourthArgument, fifthArguments, sixthArguments, seventhArguments, eighthArguments) // val result2 = veryLongNameFun(firstArgument, secondArgument) // first argument cannot to be able to stay in // the first line val result3 = veryLongNameInFirstParam( firstArgument, secondArgument, thirdArgument ) // first argument cannot to be able to stay in // the first line val result4 = veryLongNameInFirstParam( firstArgument ) ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/long_line/LongValueArgumentsListTest.kt ================================================ package test.paragraph3.long_line val firstArgument = 1 val secondArgument = 2 val thirdArgument = 3 val fourthArgument = 4 val fifthArguments = 5 val sixthArguments = 6 val seventhArguments = 7 val eighthArguments = 8 // Many arguments in function val result1 = ManyParamInFunction(firstArgument, secondArgument, thirdArgument, fourthArgument, fifthArguments, sixthArguments, seventhArguments, eighthArguments) // val result2 = veryLongNameFun(firstArgument, secondArgument) // first argument cannot to be able to stay in the first line val result3 = veryLongNameInFirstParam(firstArgument, secondArgument, thirdArgument) // first argument cannot to be able to stay in the first line val result4 = veryLongNameInFirstParam(firstArgument) ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/long_numbers/LongNumericalValuesExpected.kt ================================================ package test.paragraph3.long_numbers fun foo() { val oneMillion = 1_000_000 val ten = 10 val creditCardNumber = 1_234_567_890_123_456L val socialSecurityNumber = 999_999_999L val hexBytes = 0xFF_ECD_E5E val bytes = 0b11_010_010_011_010_011_001_010_010_010_010 val flo = 192.312_341_341_344_355_345 val flo2 = 192.111_111_1 val flo3 = 1_924_345.145f val flo4 = 1_924_345.145_111_11 } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/long_numbers/LongNumericalValuesTest.kt ================================================ package test.paragraph3.long_numbers fun foo() { val oneMillion = 1000000 val ten = 10 val creditCardNumber = 1234567890123456L val socialSecurityNumber = 999999999L val hexBytes = 0xFFECDE5E val bytes = 0b11010010011010011001010010010010 val flo = 192.312341341344355345 val flo2 = 192.111_111_1 val flo3 = 1924345.145f val flo4 = 1924345.14511111 } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/multiple_modifiers/AnnotationExpected.kt ================================================ package test.paragraph3.multiple_modifiers import org.jetbrains.kotlin.javax.inject.Inject @Annotation public final fun foo() { } @Annotation public fun foo() { } @Inject() @Suppress() public fun qwe() { } @Inject() @Suppress() public fun qwe() { } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/multiple_modifiers/AnnotationTest.kt ================================================ package test.paragraph3.multiple_modifiers import org.jetbrains.kotlin.javax.inject.Inject public @Annotation final fun foo() { } public @Annotation fun foo() { } public @Suppress()@Inject() fun qwe() { } public @Suppress() @Inject() fun qwe() { } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/multiple_modifiers/ModifierExpected.kt ================================================ package test.paragraph3.multiple_modifiers public enum class Q {} protected data class Counter(val dayIndex: Int) { suspend operator fun plus(increment: Int): Counter { return Counter(dayIndex + increment) } } public final fun foo() { protected open lateinit var a: List } public internal fun interface Factory { public fun create(): List } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/multiple_modifiers/ModifierTest.kt ================================================ package test.paragraph3.multiple_modifiers enum public class Q {} data protected class Counter(val dayIndex: Int) { operator suspend fun plus(increment: Int): Counter { return Counter(dayIndex + increment) } } final public fun foo() { lateinit open protected var a: List } internal public fun interface Factory { public fun create(): List } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/newlines/ColonExpected.kt ================================================ package test.paragraph3.newlines fun foo(a: Int, b: Int) { } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/newlines/ColonTest.kt ================================================ package test.paragraph3.newlines fun foo(a : Int, b : Int) { } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/newlines/CommaExpected.kt ================================================ package test.paragraph3.newlines fun foo(a: Int, b: Int) { bar(a, b) } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/newlines/CommaTest.kt ================================================ package test.paragraph3.newlines fun foo(a: Int , b: Int) { bar(a , b) } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/newlines/ExpressionBodyExpected.kt ================================================ package test.paragraph3.newlines fun foo(): String = "lorem ipsum" fun foo():String = "lorem ipsum" fun foo() : String = "lorem ipsum" fun recFoo(): String = "lorem " + recFoo() fun recFoo():String = "lorem " + recFoo() fun recFoo(): String = "lorem " + recFoo() fun foo() = "lorem ipsum" fun foo() = println("Logging") ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/newlines/ExpressionBodyTest.kt ================================================ package test.paragraph3.newlines fun foo(): String { return "lorem ipsum" } fun foo():String{ return "lorem ipsum" } fun foo() : String { return "lorem ipsum" } fun recFoo(): String { return "lorem " + recFoo() } fun recFoo():String { return "lorem " + recFoo() } fun recFoo(): String{ return "lorem " + recFoo() } fun foo() = "lorem ipsum" fun foo() { return println("Logging") } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/newlines/FunctionalStyleExpected.kt ================================================ package test.paragraph3.newlines fun foo(list: List?) { list!! .filterNotNull() .map { it.baz() } .firstOrNull { it.condition() } ?.qux() ?:foobar } fun bar(x :Int,y:Int) :Int = x+ y fun goo() { x.map() .gro() .gh() t.map() .hg() .hg() t .map() .filter() .takefirst() x .map() .filter() .hre() t.responseBody!![0] .name } fun foo() { foo ?: bar.baz() .qux() foo ?: bar.baz() .qux() } fun controlFlow( code: CodeBlock, format: String, vararg args: Any? ): CodeBlock = CodeBlock.builder() .beginControlFlow(format, *args) .add(code) .endControlFlow() .build() fun controlFlow( code: CodeBlock, format: String, vararg args: Any?, ): CodeBlock = CodeBlock.builder() .beginControlFlow(format, *args) .add(code) .endControlFlow() .build() fun foo( a: Int, b: Int, c: Int ): Int = 42 ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/newlines/FunctionalStyleTest.kt ================================================ package test.paragraph3.newlines fun foo(list: List?) { list!!.filterNotNull().map { it.baz() }.firstOrNull { it.condition() }?.qux() ?: foobar } fun bar(x :Int,y:Int) :Int { return x+ y } fun goo() { x.map().gro() .gh() t.map().hg().hg() t .map() .filter() .takefirst() x .map() .filter().hre() t.responseBody!![0]. name } fun foo() { foo ?: bar.baz().qux() foo ?: bar.baz() .qux() } fun controlFlow(code: CodeBlock, format: String, vararg args: Any?): CodeBlock = CodeBlock.builder().beginControlFlow(format, *args).add(code) .endControlFlow().build() fun controlFlow(code: CodeBlock, format: String, vararg args: Any?,): CodeBlock = CodeBlock.builder().beginControlFlow(format, *args).add(code) .endControlFlow().build() fun foo(a: Int, b: Int, c: Int): Int = 42 ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/newlines/LParExpected.kt ================================================ package test.paragraph3.newlines val a = Foo(0) ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/newlines/LParTest.kt ================================================ package test.paragraph3.newlines val a = Foo (0) ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/newlines/LambdaExpected.kt ================================================ package test.paragraph3.newlines class Example { val a = list.map { elem -> foo(elem) } val b = list.map { elem: Type -> foo(elem) } val c = list.map { elem -> bar(elem) } val d = list.map { elem: Type -> bar(elem) foo(elem) } val e = list.map { bar(elem) foo(elem) } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/newlines/LambdaTest.kt ================================================ package test.paragraph3.newlines class Example { val a = list.map { elem -> foo(elem) } val b = list.map { elem: Type -> foo(elem) } val c = list.map { elem -> bar(elem) } val d = list.map { elem: Type -> bar(elem) foo(elem) } val e = list.map { bar(elem) foo(elem) } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/newlines/ListArgumentLambdaExpected.kt ================================================ package test.paragraph3.newlines class Example { fun foo() { foldIndexed("#") { index: Int, acc: String, pathPart: String -> } } fun bar() { foldIndexed("#") { index, acc, pathPart -> } } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/newlines/ListArgumentLambdaTest.kt ================================================ package test.paragraph3.newlines class Example { fun foo() { foldIndexed("#") { index: Int, acc: String, pathPart: String -> } } fun bar() { foldIndexed("#") { index, acc, pathPart -> } } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/newlines/LongDotQualifiedExpressionExpected.kt ================================================ package test.paragraph3.newlines val elem1 = firstArgumentDot()?.secondArgumentDot ?.thirdArgumentDot ?.fourthArgumentDot ?.fifthArgumentDot ?.sixthArgumentDot val elem2 = firstArgumentDot?.secondArgumentDot() ?.thirdArgumentDot ?.fourthArgumentDot ?.fifthArgumentDot ?.sixthArgumentDot val elem3 = firstArgumentDot?.secondArgumentDot?.thirdArgumentDot() ?.fourthArgumentDot ?.fifthArgumentDot ?.sixthArgumentDot val elem4 = firstArgumentDot?.secondArgumentDot?.thirdArgumentDot + firstArgumentDot?.secondArgumentDot?.thirdArgumentDot?.fourthArgumentDot val elem5 = firstArgumentDot()!!.secondArgumentDot()!! .thirdArgumentDot!! .fourthArgumentDot!! .fifthArgumentDot!! .sixthArgumentDot() val elem6 = firstArgumentDot!!.secondArgumentDot!!.thirdArgumentDot()!! .fourthArgumentDot!! .fifthArgumentDot()!! .sixthArgumentDot val elem7 = firstArgumentDot!!.secondArgumentDot()!! .thirdArgumentDot!! .fourthArgumentDot()!! .fifthArgumentDot!! .sixthArgumentDot val elem8 = firstArgumentDot()!!.secondArgumentDot!!.thirdArgumentDot + firstArgumentDot!!.secondArgumentDot!!.thirdArgumentDot!!.fourthArgumentDot val elem9 = firstArgumentDot().secondArgumentDot .thirdArgumentDot() .fourthArgumentDot .fifthArgumentDot .sixthArgumentDot val elem10 = firstArgumentDot.secondArgumentDot() .thirdArgumentDot .fourthArgumentDot .fifthArgumentDot() .sixthArgumentDot val elem11 = firstArgumentDot.secondArgumentDot.thirdArgumentDot() .fourthArgumentDot .fifthArgumentDot .sixthArgumentDot val elem12 = firstArgumentDot.secondArgumentDot.thirdArgumentDot + firstArgumentDot.secondArgumentDot().thirdArgumentDot.fourthArgumentDot val elem13 = firstArgumentDot!!.secondArgumentDot?.thirdArgumentDot() .fourthArgumentDot!! .fifthArgumentDot() ?.sixthArgumentDot val elem14 = firstArgumentDot.secondArgumentDot?.thirdArgumentDot()!! .fourthArgumentDot ?.fifthArgumentDot .sixthArgumentDot val elem15 = firstArgumentDot?.secondArgumentDot!!.thirdArgumentDot.fourthArgumentDot() .fifthArgumentDot!! .sixthArgumentDot val elem16 = firstArgumentDot.secondArgumentDot.thirdArgumentDot.fourthArgumentDot.fifthArgumentDot.sixthArgumentDot val elem17 = firstArgumentDot!!.secondArgumentDot.thirdArgumentDot!!.fourthArgumentDot.fifthArgumentDot!!.sixthArgumentDot val elem18 = firstArgumentDot.secondArgumentDot?.thirdArgumentDot.fourthArgumentDot?.fifthArgumentDot.sixthArgumentDot private val holder: java.util.concurrent.atomic.AtomicReference = java.util.concurrent.atomic.AtomicReference(valueToStore) private val holder: kotlin.native.concurrent.AtomicReference = kotlin.native.concurrent.AtomicReference(valueToStore) ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/newlines/LongDotQualifiedExpressionTest.kt ================================================ package test.paragraph3.newlines val elem1 = firstArgumentDot()?.secondArgumentDot?.thirdArgumentDot?.fourthArgumentDot?.fifthArgumentDot?.sixthArgumentDot val elem2 = firstArgumentDot?.secondArgumentDot()?.thirdArgumentDot ?.fourthArgumentDot?.fifthArgumentDot?.sixthArgumentDot val elem3 = firstArgumentDot?.secondArgumentDot?.thirdArgumentDot()?.fourthArgumentDot ?.fifthArgumentDot?.sixthArgumentDot val elem4 = firstArgumentDot?.secondArgumentDot?.thirdArgumentDot + firstArgumentDot?.secondArgumentDot?.thirdArgumentDot?.fourthArgumentDot val elem5 = firstArgumentDot()!!.secondArgumentDot()!!.thirdArgumentDot!!.fourthArgumentDot!!.fifthArgumentDot!!.sixthArgumentDot() val elem6 = firstArgumentDot!!.secondArgumentDot!!.thirdArgumentDot()!! .fourthArgumentDot!!.fifthArgumentDot()!!.sixthArgumentDot val elem7 = firstArgumentDot!!.secondArgumentDot()!!.thirdArgumentDot!!.fourthArgumentDot()!! .fifthArgumentDot!!.sixthArgumentDot val elem8 = firstArgumentDot()!!.secondArgumentDot!!.thirdArgumentDot + firstArgumentDot!!.secondArgumentDot!!.thirdArgumentDot!!.fourthArgumentDot val elem9 = firstArgumentDot().secondArgumentDot.thirdArgumentDot().fourthArgumentDot.fifthArgumentDot.sixthArgumentDot val elem10 = firstArgumentDot.secondArgumentDot().thirdArgumentDot .fourthArgumentDot.fifthArgumentDot().sixthArgumentDot val elem11 = firstArgumentDot.secondArgumentDot.thirdArgumentDot().fourthArgumentDot .fifthArgumentDot.sixthArgumentDot val elem12 = firstArgumentDot.secondArgumentDot.thirdArgumentDot + firstArgumentDot.secondArgumentDot().thirdArgumentDot.fourthArgumentDot val elem13 = firstArgumentDot!!.secondArgumentDot?.thirdArgumentDot().fourthArgumentDot!!.fifthArgumentDot()?.sixthArgumentDot val elem14 = firstArgumentDot.secondArgumentDot?.thirdArgumentDot()!!.fourthArgumentDot?.fifthArgumentDot.sixthArgumentDot val elem15 = firstArgumentDot?.secondArgumentDot!!.thirdArgumentDot.fourthArgumentDot().fifthArgumentDot!!.sixthArgumentDot val elem16 = firstArgumentDot.secondArgumentDot.thirdArgumentDot.fourthArgumentDot.fifthArgumentDot.sixthArgumentDot val elem17 = firstArgumentDot!!.secondArgumentDot.thirdArgumentDot!!.fourthArgumentDot.fifthArgumentDot!!.sixthArgumentDot val elem18 = firstArgumentDot.secondArgumentDot?.thirdArgumentDot.fourthArgumentDot?.fifthArgumentDot.sixthArgumentDot private val holder: java.util.concurrent.atomic.AtomicReference = java.util.concurrent.atomic.AtomicReference(valueToStore) private val holder: kotlin.native.concurrent.AtomicReference = kotlin.native.concurrent.AtomicReference(valueToStore) ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/newlines/OneLineFunctionExpected.kt ================================================ package test.paragraph3.newlines class Example { fun doubleA(): Int = 2 * a } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/newlines/OneLineFunctionTest.kt ================================================ package test.paragraph3.newlines class Example { fun doubleA(): Int { return 2 * a } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/newlines/OperatorsExpected.kt ================================================ package test.paragraph3.newlines fun foo() { val and = condition1&& condition2 val and2 = condition1&& condition2 // this isn't an expression val plus = x + y obj .foo() obj .foo() obj ?.foo() obj ?:OBJ obj ::foo } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/newlines/OperatorsTest.kt ================================================ package test.paragraph3.newlines fun foo() { val and = condition1 && condition2 val and2 = condition1 && condition2 // this isn't an expression val plus = x + y obj. foo() obj . foo() obj?. foo() obj ?: OBJ obj:: foo } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/newlines/ParameterListExpected.kt ================================================ package test.paragraph3.newlines fun bar( arg1: Int, arg2: Int, arg3: Int ) { } class Foo : FooBase(), BazInterface, BazSuperclass { } class Foo(val arg1: Int, arg2: Int) { } class Foo( val arg1: Int, arg2: Int, arg3: Int ) { constructor( arg1: Int, arg2: String, arg3: String ) : this( arg1, 0, 0 ) { } } class Foo(val arg1: Int, var arg2: Int, arg3: Int ) { } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/newlines/ParameterListTest.kt ================================================ package test.paragraph3.newlines fun bar(arg1: Int, arg2: Int, arg3: Int) { } class Foo : FooBase(), BazInterface, BazSuperclass { } class Foo(val arg1: Int, arg2: Int) { } class Foo(val arg1: Int, arg2: Int, arg3: Int) { constructor(arg1: Int, arg2: String, arg3: String) : this(arg1, 0, 0) { } } class Foo(val arg1: Int, var arg2: Int, arg3: Int) { } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/newlines/SizeParameterListExpected.kt ================================================ package test.paragraph3.newlines fun foo( a: Int, b: Int, c: Int ) { bar(a, b, c) } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/newlines/SizeParameterListTest.kt ================================================ package test.paragraph3.newlines fun foo(a: Int, b: Int, c: Int) { bar(a, b, c) } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/newlines/SuperClassListOnTheSameLineExpected.kt ================================================ package test.paragraph3.newlines class A : B(), C

, D {} ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/newlines/SuperClassListOnTheSameLineTest.kt ================================================ package test.paragraph3.newlines class A : B(), C

, D {} ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/nullable/CollectionExpected.kt ================================================ package test.paragraph3.nullable import java.util.* class A { val a: List = emptyList() val b: Iterable = emptyList() val c: Map = emptyMap() val d: Array = emptyArray() val e: Set = emptySet() val f: Sequence = emptySequence() val g: MutableList = mutableListOf() val h: MutableMap = mutableMapOf() val i: MutableSet = mutableSetOf() val j: LinkedList = LinkedList() val k: LinkedHashMap = LinkedHashMap() val l: LinkedHashSet = LinkedHashSet() val m: Queue = LinkedList() val s: Iterator? = null } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/nullable/CollectionTest.kt ================================================ package test.paragraph3.nullable import java.util.* class A { val a: List? = null val b: Iterable? = null val c: Map? = null val d: Array? = null val e: Set? = null val f: Sequence? = null val g: MutableList? = null val h: MutableMap? = null val i: MutableSet? = null val j: LinkedList? = null val k: LinkedHashMap? = null val l: LinkedHashSet? = null val m: Queue? = null val s: Iterator? = null } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/nullable/NullPrimitiveExpected.kt ================================================ package test.paragraph3.nullable class A { val a: Int = 0 val b: Double = 0.0 val c: Float = 0.0F val d: Short = 0 val f: Byte = 0 val g: Long = 0L val h: Char = '' } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/nullable/NullPrimitiveTest.kt ================================================ package test.paragraph3.nullable class A { val a: Int? = null val b: Double? = null val c: Float? = null val d: Short? = null val f: Byte? = null val g: Long? = null val h: Char? = null } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/preview_annotation/PreviewAnnotationMethodNameExpected.kt ================================================ package test.paragraph3.preview_annotation @Preview private fun BannerPreview() {} ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/preview_annotation/PreviewAnnotationMethodNameTest.kt ================================================ package test.paragraph3.preview_annotation @Preview private fun Banner() {} ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/preview_annotation/PreviewAnnotationPrivateModifierExpected.kt ================================================ package test.paragraph3.preview_annotation @Preview @Composable private fun BannerPreview1() {} @Preview private fun BannerPreview2() {} @Preview private fun BannerPreview3() {} @Preview private fun BannerPreview4() {} @Preview private fun BannerPreview5() {} @Preview final private fun BannerPreview6() {} ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/preview_annotation/PreviewAnnotationPrivateModifierTest.kt ================================================ package test.paragraph3.preview_annotation @Preview @Composable public fun BannerPreview1() {} @Preview protected fun BannerPreview2() {} @Preview internal fun BannerPreview3() {} @Preview fun BannerPreview4() {} @Preview open fun BannerPreview5() {} @Preview final fun BannerPreview6() {} ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/range/RangeToExpected.kt ================================================ package test.paragraph3.range fun foo() { val num = 1 val w = num.rangeTo(num, num) val q = 1..5 val e = num..num if (1 in num..10) { } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/range/RangeToTest.kt ================================================ package test.paragraph3.range fun foo() { val num = 1 val w = num.rangeTo(num, num) val q = 1..5 val e = num.rangeTo(num) if (1 in num.rangeTo(10)) { } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/range/RangeToUntilExpected.kt ================================================ package test.paragraph3.range class A { fun foo() { for (i in 1..4) print(i) for (i in 4 downTo 1) print(i) for (i in 1 until 4) print(i) for (i in 1..4 step 2) print(i) for (i in 4 downTo 1 step 3) print(i) if (6 in (1..10) && true) {} for (i in 1 until (4)) print(i) for (i in 1 until (b)) print(i) for (i in ((1 until ((4))))) print(i) for (i in 1..(4 - 2)) print(i) for (i in 1..(b - 10)) print(i) } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/range/RangeToUntilTest.kt ================================================ package test.paragraph3.range class A { fun foo() { for (i in 1..4) print(i) for (i in 4 downTo 1) print(i) for (i in 1 until 4) print(i) for (i in 1..4 step 2) print(i) for (i in 4 downTo 1 step 3) print(i) if (6 in (1..10) && true) {} for (i in 1..(4 - 1)) print(i) for (i in 1..(b - 1)) print(i) for (i in ((1 .. ((4 - 1))))) print(i) for (i in 1..(4 - 2)) print(i) for (i in 1..(b - 10)) print(i) } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/semicolons/SemicolonsExpected.kt ================================================ package test.paragraph3.newlines import com.saveourtool.diktat.common.config.rules.RulesConfig enum class Example { A, B ; fun foo() {} val a = 0 val b = if (condition) { bar(); baz()} else qux } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/semicolons/SemicolonsTest.kt ================================================ package test.paragraph3.newlines; import com.saveourtool.diktat.common.config.rules.RulesConfig; enum class Example { A, B ; fun foo() {}; val a = 0; val b = if (condition) { bar(); baz()} else qux }; ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/sort_error/ConstantsExpected.kt ================================================ package test.paragraph3.sort_error class Test { companion object { private const val B = 4 private const val C = 4 private const val D = 4 private val SIMPLE_VALUE = listOf(IDENTIFIER, WHITE_SPACE, COMMA, SEMICOLON) } } class Test2 { companion object { private const val A = 4 private const val D = 4 private val SIMPLE_VALUE = listOf(IDENTIFIER, WHITE_SPACE, COMMA, SEMICOLON) private const val B = 4 } } class Test3 { companion object } class Test2 { companion object { private const val A = 4 private const val D = 4 private val SIMPLE_VALUE = listOf(IDENTIFIER, WHITE_SPACE, COMMA, SEMICOLON) private const val Ba = 4 private const val Baa = 4 private const val Bb = 4 } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/sort_error/ConstantsTest.kt ================================================ package test.paragraph3.sort_error class Test { companion object { private const val D = 4 private const val C = 4 private const val B = 4 private val SIMPLE_VALUE = listOf(IDENTIFIER, WHITE_SPACE, COMMA, SEMICOLON) } } class Test2 { companion object { private const val A = 4 private const val D = 4 private val SIMPLE_VALUE = listOf(IDENTIFIER, WHITE_SPACE, COMMA, SEMICOLON) private const val B = 4 } } class Test3 { companion object } class Test2 { companion object { private const val A = 4 private const val D = 4 private val SIMPLE_VALUE = listOf(IDENTIFIER, WHITE_SPACE, COMMA, SEMICOLON) private const val Baa = 4 private const val Ba = 4 private const val Bb = 4 } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/sort_error/EnumSortExpected.kt ================================================ package test.paragraph3.sort_error enum class Alp { BLUE(0x0000FF), GREEN(0x00FF00), RED(0xFF0000), ; } enum class Warnings { TALKING { override fun signal() = TALKING }, WAITING { override fun signal() = TALKING }, ; abstract fun signal(): ProtocolState } enum class Warnings { TALKING { override fun signal() = TALKING }, WAITING { override fun signal() = TALKING }; abstract fun signal(): ProtocolState } enum class Alp { BLUE(0x0000FF), GREEN(0x00FF00), RED(0xFF0000), } enum class Alp { BLUE(0x0000FF), GREEN(0x00FF00), RED(0xFF0000) ; } enum class IssueType { PROJECT_STRUCTURE, TESTS, VCS } enum class IssueType { PROJECT_STRUCTURE,TESTS,VCS } enum class IssueType { PROJECT_STRUCTURE, // comment TESTS, VCS } enum class IssueType { PROJECT_STRUCTURE ,// comment TESTS, VCS } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/sort_error/EnumSortTest.kt ================================================ package test.paragraph3.sort_error enum class Alp { RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF), ; } enum class Warnings { WAITING { override fun signal() = TALKING }, TALKING { override fun signal() = TALKING }, ; abstract fun signal(): ProtocolState } enum class Warnings { WAITING { override fun signal() = TALKING }, TALKING { override fun signal() = TALKING }; abstract fun signal(): ProtocolState } enum class Alp { RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF), } enum class Alp { RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF) ; } enum class IssueType { VCS, PROJECT_STRUCTURE, TESTS } enum class IssueType { VCS,PROJECT_STRUCTURE,TESTS } enum class IssueType { VCS,PROJECT_STRUCTURE, // comment TESTS } enum class IssueType { VCS, TESTS, PROJECT_STRUCTURE // comment } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/spaces/AnnotationExpected.kt ================================================ package test.paragraph3.spaces @Suppress("Text") @Inject("sdc") class A { @Suppress("AnotherText") fun foo() { } @RequestMapping(value = ["/"], method = [RequestMethod.GET]) fun goo() { } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/spaces/AnnotationTest.kt ================================================ package test.paragraph3.spaces @Suppress ("Text") @Inject("sdc") class A { @Suppress ("AnotherText") fun foo() { } @RequestMapping(value =["/"], method = [RequestMethod.GET]) fun goo(){ } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/spaces/BinaryOpExpected.kt ================================================ package test.paragraph3.spaces class Example where T : UpperType, R : UpperType, Q : UpperType { fun foo(t: T) = t + 1 fun foo2(t: T) = t + 1 fun foo3(t: T) = t + 1 fun bar() { listOf().map(this::foo)?.filter { elem -> predicate(elem) }!! listOf().map(this::foo)?.filter { elem -> predicate(elem) }!!.first() listOf().map(this::foo)?.filter { elem -> predicate(elem) }!!.first() } } class Test(@field:Anno val foo: Type, @get:Anno val bar: Type, @param:Anno val baz: Type) { fun foo(): String = "lorem" fun bar(): String = "ipsum" fun baz(): String = "dolor" } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/spaces/BinaryOpTest.kt ================================================ package test.paragraph3.spaces class Example where T:UpperType, R: UpperType, Q :UpperType { fun foo(t: T) = t+ 1 fun foo2(t: T) = t+1 fun foo3(t: T) = t +1 fun bar() { listOf() .map(this ::foo) ?.filter { elem ->predicate(elem) } !! listOf() . map(this :: foo) ?. filter { elem->predicate(elem) } !!.first() listOf(). map(this:: foo)?. filter { elem-> predicate(elem) } !!. first() } } class Test(@field: Anno val foo: Type, @get :Anno val bar : Type, @param : Anno val baz :Type) { fun foo(): String = "lorem" fun bar() : String = "ipsum" fun baz() :String = "dolor" } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/spaces/BracesLambdaSpacesExpected.kt ================================================ package test.paragraph3.spaces class Some { fun foo() { list.map { it.text } } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/spaces/BracesLambdaSpacesTest.kt ================================================ package test.paragraph3.spaces class Some { fun foo() { list.map {it.text} } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/spaces/EolSpacesExpected.kt ================================================ package test.paragraph3.spaces class Example { fun foo() { bar() } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/spaces/EolSpacesTest.kt ================================================ package test.paragraph3.spaces class Example { fun foo() { bar() } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/spaces/EqualsExpected.kt ================================================ package test.paragraph3.spaces class A { fun foo() { val q = 10 var w = q w = 4 } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/spaces/EqualsTest.kt ================================================ package test.paragraph3.spaces class A { fun foo() { val q=10 var w = q w=4 } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/spaces/LBraceAfterKeywordExpected.kt ================================================ package test.paragraph3.spaces class Example { fun foo() { if (condition) { } else {} try { } finally { } } fun bar() { if (condition) { } else {} try { } finally { } } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/spaces/LBraceAfterKeywordTest.kt ================================================ package test.paragraph3.spaces class Example { fun foo() { if (condition) { } else{} try{ } finally{ } } fun bar() { if (condition) { } else {} try { } finally { } } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/spaces/LambdaAsArgumentExpected.kt ================================================ package test.paragraph3.spaces fun foo(a: (Int) -> Int, b: Int) { foo({ x: Int -> x }, 5) } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/spaces/LambdaAsArgumentTest.kt ================================================ package test.paragraph3.spaces fun foo(a: (Int) -> Int, b: Int) { foo( { x: Int -> x }, 5) } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/spaces/LbraceExpected.kt ================================================ package test.paragraph3.spaces class Example { fun foo() { list.run { map { bar(it) } } lister.map { "${ruleId}" } } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/spaces/LbraceTest.kt ================================================ package test.paragraph3.spaces class Example{ fun foo() { list.run{ map { bar(it) } } lister.map { "${ruleId}" } } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/spaces/TooManySpacesEnumExpected.kt ================================================ package test.paragraph3.spaces enum class Warnings(private val id: Int, private val canBeAutoCorrected: Boolean, private val warn: String) : Rule { PACKAGE_NAME_MISSING (1, true, "no package name declared in a file"), PACKAGE_NAME_INCORRECT_CASE (2, true, "package name should be completely in a lower case"), PACKAGE_NAME_INCORRECT_PREFIX(3, false, "package name should start from company's domain") ; fun some() { val b = 5 } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/spaces/TooManySpacesEnumTest.kt ================================================ package test.paragraph3.spaces enum class Warnings(private val id: Int, private val canBeAutoCorrected: Boolean, private val warn: String) : Rule { PACKAGE_NAME_MISSING (1, true, "no package name declared in a file"), PACKAGE_NAME_INCORRECT_CASE (2, true, "package name should be completely in a lower case"), PACKAGE_NAME_INCORRECT_PREFIX(3, false, "package name should start from company's domain") ; fun some() { val b = 5 } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/spaces/TooManySpacesExpected.kt ================================================ package test.paragraph3.spaces data class User(val name: String, val id: Int) fun main() { val user = User("Alex", 1) println(user) val secondUser = User("Alex", 1) val thirdUser = User("Max", 2) println("user == secondUser: ${user == secondUser}") println("user == thirdUser: ${user == thirdUser}") println(user.hashCode()) println(thirdUser.hashCode()) println(user.copy()) println(user.copy("Max")) println(user.copy(id = 2)) println("name = ${user.component1()}") println("id = ${user.component2()}") } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/spaces/TooManySpacesTest.kt ================================================ package test.paragraph3.spaces data class User(val name: String, val id: Int) fun main() { val user = User("Alex", 1) println(user) val secondUser = User("Alex", 1) val thirdUser = User("Max", 2) println("user == secondUser: ${user == secondUser}") println("user == thirdUser: ${user == thirdUser}") println(user.hashCode()) println(thirdUser.hashCode()) println(user.copy()) println(user.copy("Max")) println(user.copy(id = 2)) println("name = ${user.component1()}") println("id = ${user.component2()}") } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/spaces/WhiteSpaceBeforeLBraceExpected.kt ================================================ package test.paragraph3.spaces class Example { fun foo() { if (condition) { } else {} try { } finally { } } fun bar() { if (condition) { } else {} try { } finally { } } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/spaces/WhiteSpaceBeforeLBraceTest.kt ================================================ package test.paragraph3.spaces class Example { fun foo() { if (condition) { } else{} try{ } finally{ } } fun bar() { if (condition) { } else {} try { } finally { } } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/spaces/WhiteSpaceBeforeLParExpected.kt ================================================ package test.paragraph3.spaces class Example : SuperExample { constructor(val a: Int) fun foo() { if (condition) { } for (i in 1..100) { } when (expression) { } } fun bar() { if (condition) { } for (i in 1..100) { } when (expression) { } } } data class Example( val foo: Foo, val bar: Bar ) ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/spaces/WhiteSpaceBeforeLParTest.kt ================================================ package test.paragraph3.spaces class Example : SuperExample { constructor (val a: Int) fun foo() { if(condition) { } for(i in 1..100) { } when(expression) { } } fun bar () { if (condition) { } for (i in 1..100) { } when (expression) { } } } data class Example ( val foo: Foo, val bar: Bar ) ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/src/main/A/FileSize2000.kt ================================================ ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/src/main/A/FileSizeA.kt ================================================ package test.paragraph3.src.main.A class A { fun tester(a: Int) = Unit fun testER(a: Int) = Unit } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/src/main/B/FileSizeB.kt ================================================ package test.paragraph3.src.main.B class B { fun tester(a: Int) = Unit fun testER(a: Int) = Unit } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/src/main/C/FileSizeC.kt ================================================ package test.paragraph3.src.main.B class B { fun tester(a: Int) = Unit fun testER(a: Int) = Unit } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/src/main/FileSizeLarger.kt ================================================ //sdfsdf class fg{ private val sdv = 1000 } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/statement/StatementExpected.kt ================================================ package test.paragraph3.statement import com.pinterest.ktlint.core.KtLint import com.pinterest.ktlint.core.LintError fun foo(){ if (x > 0){ goo() qwe() } } fun foo(){ if (x > 0){ goo() qwe() } } fun foo() { grr() } enum class ProtocolState { WAITING { override fun signal() = TALKING }, TALKING { override fun signal() = WAITING }; abstract fun signal(): ProtocolState } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/statement/StatementTest.kt ================================================ package test.paragraph3.statement import com.pinterest.ktlint.core.KtLint; import com.pinterest.ktlint.core.LintError fun foo(){ if (x > 0){ goo(); qwe() } } fun foo(){ if (x > 0){ goo();qwe() } } fun foo() { ; grr() } enum class ProtocolState { WAITING { override fun signal() = TALKING }, TALKING { override fun signal() = WAITING }; abstract fun signal(): ProtocolState } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/string_concatenation/StringConcatenationExpected.kt ================================================ package test.chapter3.strings val valueStr = "my str" val x = 13 fun foo(): String { return "string" } val myTest1 = "my string string $valueStr other value" val myTest2 = "my string 1$valueStr other value" val myTest3 = "my string $valueStr string other value" val myTest4 = "my string one two" val myTest5 = "trying to sum with multiline" val myTest6 = "trying to sum with " + """ multiline""".trimIndent() val myTest7 = "multiline string" val myTest8 = "string ${valueStr.replace("my", "")}" val myTest9 = "string ${"valueStr".replace("my", "")}" val myTest10 = "string ${(1 + 5)}" val myTest11 = "sum other string 3 str2 5 other string 2 2 str3 4" val myTest12 = "my string 123" val myTest13 = "${(1 + 2)} my string 3 string $valueStr$valueStr" val myTest14 = "my string ${(1 + 2 + 3)} other string 3${(1 + 2 + 3)}" val myTest15 = 1 + 2 + ("13").toInt() val myTest16 = 1.0 + 2.0 + ("13.0").toFloat() val myTest17 = "sum ${(1 + 2 + 3) * 4}" val myTest18 = "my string ${(1 + 2 + 3) * 4} other string 3${(1 + (2 + 3))} third string str 5" val myTest19 = 1 + 2 + 3 + ("65").toInt() val myTest20 = "${x}string" val myTest21 = "${x} string" val myTest22 = "string${foo()}" val myTest23 = x.toString() + foo() val myTest24 = foo() + "string" val myTest25 = "String ${valueStr?.value}" val myTest26 = "my string ${if (true) "1" else "2"}" ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/string_concatenation/StringConcatenationTest.kt ================================================ package test.chapter3.strings val valueStr = "my str" val x = 13 fun foo(): String { return "string" } val myTest1 = "my string " + "string " + valueStr + " other value" val myTest2 = "my string " + 1 + valueStr + " other value" val myTest3 = "my string " + valueStr + " string " + "other value" val myTest4 = "my string" + (" one " + "two") val myTest5 = "trying to sum with " + """multiline""" val myTest6 = "trying to sum with " + """ multiline""".trimIndent() val myTest7 = """multiline""" + " string" val myTest8 = "string " + valueStr.replace("my", "") val myTest9 = "string " + "valueStr".replace("my", "") val myTest10 = "string " + (1 + 5) val myTest11 = "sum " + ("other string " + 3 + " str2 " + 5 + (" other string 2 " + 2 + " str3 " + 4)) val myTest12 = "my string " + 1 + 2 + 3 val myTest13 = (1 + 2).toString() + " my string " + 3 + " string " + valueStr + valueStr val myTest14 = "my string " + (1 + 2 + 3) + (" other string " + 3) + (1 + 2 + 3) val myTest15 = 1 + 2 + ("1" + 3).toInt() val myTest16 = 1.0 + 2.0 + ("1" + 3.0).toFloat() val myTest17 = "sum " + (1 + 2 + 3) * 4 val myTest18 = "my string " + (1 + 2 + 3) * 4 + (" other string " + 3) + (1 + (2 + 3)) + (" third string " + ("str " + 5)) val myTest19 = 1 + 2 + 3 + ("6" + 5).toInt() val myTest20 = x.toString() + "string" val myTest21 = x.toString() + " string" val myTest22 = "string" + foo() val myTest23 = x.toString() + foo() val myTest24 = foo() + "string" val myTest25 = "String " + valueStr?.value val myTest26 = "my string " + if (true) "1" else "2" ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/string_template/StringTemplateExpected.kt ================================================ package test.paragraph3.string_template class SomeClass { fun someFunc() { val x = "asd" val z = x val m = "1.0" val template = "$x, ${asd.moo()}" val binExpr = "${foo as Foo}" val trippleQuotes = x val test = """${'$'}""" val test2= "${'$'}1" val digitsWithLetters = "1.0asd" } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/string_template/StringTemplateTest.kt ================================================ package test.paragraph3.string_template class SomeClass { fun someFunc() { val x = "asd" val z = "$x" val m = "${1.0}" val template = "${x}, ${asd.moo()}" val binExpr = "${foo as Foo}" val trippleQuotes = """${x}""" val test = """${'$'}""" val test2= "${'$'}1" val digitsWithLetters = "${1.0}asd" } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/top_level/TopLevelSortExpected.kt ================================================ package test.paragraph3.top_level const val q = "1" val heh = 10 private var t = 1 typealias StringConsumer = (String) -> Unit internal typealias ExamplesList = List private typealias UsersMap = Map class Qwe() {} fun String.qq() {} internal fun kl() {} private fun foo() {} ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/top_level/TopLevelSortTest.kt ================================================ package test.paragraph3.top_level const val q = "1" private fun foo() {} private typealias UsersMap = Map class Qwe() {} fun String.qq() {} private var t = 1 internal fun kl() {} internal typealias ExamplesList = List val heh = 10 typealias StringConsumer = (String) -> Unit ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/top_level/TopLevelWithCommentExpected.kt ================================================ /** * Some text here */ package test.paragraph3.top_level import com.saveourtool.diktat.bar class A {} /** * Hehe */ fun String.ww() {} /** * Text for function */ fun foo() {} /* text here */ ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/top_level/TopLevelWithCommentTest.kt ================================================ /** * Some text here */ package test.paragraph3.top_level import com.saveourtool.diktat.bar class A {} /** * Text for function */ fun foo() {} /** * Hehe */ fun String.ww() {} /* text here */ ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/trailing_comma/TrailingCommaExpected.kt ================================================ package test.paragraph3.trailing_comma class Customer( val name: String, lastName: String, ) fun shift(x: Int, y: Int, ) { shift( 25, 20, ) val colors = listOf( "red", "green", "blue", ) } fun getZValue(mySurface: Surface, xValue: Int, yValue: Int, ) = mySurface[ xValue, yValue ] fun isReferenceApplicable(myReference: KClass<*>, ) = when (myReference) { Comparable::class, Iterable::class, String::class, -> true else -> false } fun isReferenceApplicable(myReference: KClass<*> ,// comment ) = when (myReference) { Comparable::class, Iterable::class, String::class, -> true else -> false } @ApplicableFor([ "serializer", "balancer", "database", "inMemoryCache", ], ) fun foo() {} fun foo() {} fun mains() { foo< Comparable, Iterable, >() } fun printMeanValue() { var meanValue: Int = 0 for (( _, _, year, ) in cars) { meanValue += year } println(meanValue/cars.size) } enum class SomeEnum( val a: Int, val b: Int ,// comment ) enum class SomeEnum( val a: Int, val b: Int,// comment ) enum class SomeEnum( val a: Int, val b: Int ,/* comment */ ) enum class SomeEnum( val a: Int, val b: Int, /** some comment */ ) fun foo() { val sum: (Int, Int, Int,) -> Int = fun( x, y, z ,// trailing comma ): Int { return x + y + x } println(sum(8, 8, 8)) } fun shift(x: Int, y: Int) { shift( 25, 20, // trailing comma ) val colors = listOf( "red", "green", "blue", // trailing comma ) } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph3/trailing_comma/TrailingCommaTest.kt ================================================ package test.paragraph3.trailing_comma class Customer( val name: String, lastName: String ) fun shift(x: Int, y: Int ) { shift( 25, 20 ) val colors = listOf( "red", "green", "blue" ) } fun getZValue(mySurface: Surface, xValue: Int, yValue: Int ) = mySurface[ xValue, yValue ] fun isReferenceApplicable(myReference: KClass<*> ) = when (myReference) { Comparable::class, Iterable::class, String::class -> true else -> false } fun isReferenceApplicable(myReference: KClass<*> // comment ) = when (myReference) { Comparable::class, Iterable::class, String::class -> true else -> false } @ApplicableFor([ "serializer", "balancer", "database", "inMemoryCache" ] ) fun foo() {} fun foo() {} fun mains() { foo< Comparable, Iterable >() } fun printMeanValue() { var meanValue: Int = 0 for (( _, _, year ) in cars) { meanValue += year } println(meanValue/cars.size) } enum class SomeEnum( val a: Int, val b: Int // comment ) enum class SomeEnum( val a: Int, val b: Int// comment ) enum class SomeEnum( val a: Int, val b: Int /* comment */ ) enum class SomeEnum( val a: Int, val b: Int /** some comment */ ) fun foo() { val sum: (Int, Int, Int,) -> Int = fun( x, y, z // trailing comma ): Int { return x + y + x } println(sum(8, 8, 8)) } fun shift(x: Int, y: Int) { shift( 25, 20 // trailing comma ) val colors = listOf( "red", "green", "blue" // trailing comma ) } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph4/generics/VariableGenericTypeDeclarationExpected.kt ================================================ package test.paragraph4.generics class SomeClass(val some: Map = emptyMap()) { val myVariable: Map = emptyMap() fun someFunc(myVariable: Map = emptyMap()) {} } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph4/generics/VariableGenericTypeDeclarationTest.kt ================================================ package test.paragraph4.generics class SomeClass(val some: Map = emptyMap()) { val myVariable: Map = emptyMap() fun someFunc(myVariable: Map = emptyMap()) {} } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph4/null_checks/IfConditionAssignCheckExpected.kt ================================================ package test.paragraph4.null_checks fun foo() { val x = a?.let { f(a) } ?: g(a) val y = a ?: 0 x ?: println("NULL") val z = x ?: run { println("NULL") 0 } x?.let { f(x) } ?: run { println("NULL") g(x) } } fun bar() { val x = a?.let { f(a) } ?: g(a) val y = a ?: 0 x ?: println("NULL") val z = x ?: run { println("NULL") 0 } x?.let { f(x) } ?: run { println("NULL") g(x) } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph4/null_checks/IfConditionAssignCheckTest.kt ================================================ package test.paragraph4.null_checks fun foo() { val x = if (a != null) f(a) else g(a) val y = if (a != null) a else 0 if (x != null) { x } else { println("NULL") } val z = if (x != null) { x } else { println("NULL") 0 } if (x != null) { f(x) } else { println("NULL") g(x) } } fun bar() { val x = if (a == null) g(a) else f(a) val y = if (a == null) 0 else a if (x == null) { println("NULL") } else { x } val z = if (x == null) { println("NULL") 0 } else { x } if (x == null) { println("NULL") g(x) } else { f(x) } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph4/null_checks/IfConditionBreakCheckExpected.kt ================================================ package test.paragraph4.null_checks fun foo() { var result: Int? = 19 while(result != 0 ) { if (result == null) { foo() } else { break } } } fun foo() { var result: Int? = 19 while(result != 0 ) { result?.let { foo() } ?: break } } fun foo() { var result: Int? = 19 while(result != 0 ) { if (result != null) { break } else { foo() } } } fun foo() { var result: Int? = 19 while(result != 0 ) { result?.let { foo() } ?: break } } fun foo() { var result: Int? = 19 while(result != 0 ) { result ?: break } } fun foo() { var result: Int? = 19 while(result != 0 ) { if (result != null) { break } } } fun foo() { var result: Int? = 19 while(result != 0 ) { if (result != null) { foo() break } else { break } } } fun foo() { var result: Int? = 19 while(result != 0 ) { if (result != null) { result?.let { goo() } ?: break } else { break } } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph4/null_checks/IfConditionBreakCheckTest.kt ================================================ package test.paragraph4.null_checks fun foo() { var result: Int? = 19 while(result != 0 ) { if (result == null) { foo() } else { break } } } fun foo() { var result: Int? = 19 while(result != 0 ) { if (result == null) { break } else { foo() } } } fun foo() { var result: Int? = 19 while(result != 0 ) { if (result != null) { break } else { foo() } } } fun foo() { var result: Int? = 19 while(result != 0 ) { if (result != null) { foo() } else { break } } } fun foo() { var result: Int? = 19 while(result != 0 ) { if (result == null) { break } } } fun foo() { var result: Int? = 19 while(result != 0 ) { if (result != null) { break } } } fun foo() { var result: Int? = 19 while(result != 0 ) { if (result != null) { foo() break } else { break } } } fun foo() { var result: Int? = 19 while(result != 0 ) { if (result != null) { if (result != null) { goo() } else { break } } else { break } } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph4/null_checks/IfConditionNullCheckExpected.kt ================================================ package test.paragraph4.null_checks fun test() { val some: Int? = null some ?: run { println("some") bar() } some?.let { println("some") bar() } if (some == null && true) { print("asd") } some?.let { print("qwe") } ?: print("asd") some?.let { print("qweqwe") } some?.let { print("qqq") } ?: print("www") some?.let { print("ttt") } some?.let { print("ttt") } ?: run { null value } } fun foo() { var result: Int? = 10 while (result != 0 ) { result?.let { goo() } ?: for(i in 1..10) break } while (result != 0) { result = goo() if (result != null) { goo() } else { println(123) break } } } fun checkSmartCases() { val x = a?.toString() ?: "Null" val y = a.b.c?.toString() ?: a.b.toString() a?.let { print() } a?.let { foo() } ?: boo() } fun reversedCheckSmartCases() { val x = a?.toString() ?: "Null" val y = a.b.c?.toString() ?: a.b.toString() a ?: print() a?.let { foo() } ?: boo() } fun nullCheckWithAssumption() { val a: Int? = 5 a?.let { foo() } ?: run { a = 5 } a?.let { foo() } ?: run { a = 5 } a?.let { a = 5 } ?: foo() a?.let { a = 5 } ?: foo() a?.let { foo() } ?: a == 5 } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph4/null_checks/IfConditionNullCheckTest.kt ================================================ package test.paragraph4.null_checks fun test() { val some: Int? = null if (some == null) { println("some") bar() } if (some != null) { println("some") bar() } if (some == null && true) { print("asd") } if (some == null) { print("asd") } else { print("qwe") } if (some == null) { null } else { print("qweqwe") } if (some != null) { print("qqq") } else { print("www") } if (some != null) { print("ttt") } else { null } if (some != null) { print("ttt") } else { null value } } fun foo() { var result: Int? = 10 while (result != 0 ) { if (result != null) { goo() } else { for(i in 1..10) break } } while (result != 0) { result = goo() if (result != null) { goo() } else { println(123) break } } } fun checkSmartCases() { val x = if (a != null) { a.toString() } else { "Null" } val y = if (a.b.c != null) { a.b.c.toString() } else { a.b.toString() } if (a != null) { print() } if (a != null) { foo() } else { boo() } } fun reversedCheckSmartCases() { val x = if (a == null) { "Null" } else { a.toString() } val y = if (a.b.c == null) { a.b.toString() } else { a.b.c.toString() } if (a == null) { print() } if (a == null) { boo() } else { foo() } } fun nullCheckWithAssumption() { val a: Int? = 5 if (a != null) { foo() } else { a = 5 } if (a == null) { a = 5 } else { foo() } if (a != null) { a = 5 } else { foo() } if (a == null) { foo() } else { a = 5 } if (a != null) { foo() } else { a == 5 } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph4/null_checks/RequireFunctionExpected.kt ================================================ package test.paragraph4.null_checks fun test() { val some: Int? = null requireNotNull(some) } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph4/null_checks/RequireFunctionTest.kt ================================================ package test.paragraph4.null_checks fun test() { val some: Int? = null require(some != null) } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph4/smart_cast/SmartCastExpected.kt ================================================ package test.paragraph4.smart_cast class Some { val x = "" fun someFunc() { if (x is String) { print(x.length) var a = x } } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph4/smart_cast/SmartCastTest.kt ================================================ package test.paragraph4.smart_cast class Some { val x = "" fun someFunc() { if (x is String) { print((x as String).length) var a = x as String } } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph5/method_call_names/ReplaceMethodCallNamesExpected.kt ================================================ package test.chapter6.method_call_names fun coolFunction() { val list = listOf(1, 2, 3) val testStr = "" if (list.isEmpty()) { } if (list.isNotEmpty()) { } if (list.isNotEmpty()) { } if (list.isEmpty()) { } if (testStr.isBlank()) { } if (testStr.isNotBlank()) { } if (testStr.isNotBlank()) { } if (testStr.isBlank()) { } if (list.isNotEmpty() && testStr.isBlank()) { } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph5/method_call_names/ReplaceMethodCallNamesTest.kt ================================================ package test.chapter6.method_call_names fun coolFunction() { val list = listOf(1, 2, 3) val testStr = "" if (list.isEmpty()) { } if (list.isNotEmpty()) { } if (!list.isEmpty()) { } if (!list.isNotEmpty()) { } if (testStr.isBlank()) { } if (testStr.isNotBlank()) { } if (!testStr.isBlank()) { } if (!testStr.isNotBlank()) { } if (!list.isEmpty() && !testStr.isNotBlank()) { } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph5/nested_functions/AvoidNestedFunctionsExample.kt ================================================ package test.paragraph5.nested_functions fun anotherFun() { val b = 3 } fun someFunc() { val a = 5 } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph5/nested_functions/AvoidNestedFunctionsNoTriggerExample.kt ================================================ package test.paragraph5.nested_functions fun some() { var str = "asd" fun another() { str = "sws" } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph5/nested_functions/AvoidNestedFunctionsNoTriggerTest.kt ================================================ package test.paragraph5.nested_functions fun some() { var str = "asd" fun another() { str = "sws" } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph5/nested_functions/AvoidNestedFunctionsSeveralExample.kt ================================================ package test.paragraph5.nested_functions fun some() {} fun baz() { } fun bar() { } fun foo() { } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph5/nested_functions/AvoidNestedFunctionsSeveralTest.kt ================================================ package test.paragraph5.nested_functions fun foo() { fun bar() { fun baz() { fun some() {} } } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph5/nested_functions/AvoidNestedFunctionsTest.kt ================================================ package test.paragraph5.nested_functions fun someFunc() { val a = 5 fun anotherFun() { val b = 3 } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph6/useless-override/SeveralSuperTypesExpected.kt ================================================ package test.paragraph6.`useless-override` open class A { open fun foo(){} } interface C { fun foo(){} fun goo(){} } class B: A(){ override fun foo() { super.foo() } } class D: A(), C { override fun foo() { super.foo() super.foo() super.goo() } private fun qwe(){ val q = super.goo() } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph6/useless-override/SeveralSuperTypesTest.kt ================================================ package test.paragraph6.`useless-override` open class A { open fun foo(){} } interface C { fun foo(){} fun goo(){} } class B: A(){ override fun foo() { super.foo() } } class D: A(), C { override fun foo() { super.foo() super.foo() super.goo() } private fun qwe(){ val q = super.goo() } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph6/useless-override/UselessOverrideExpected.kt ================================================ package test.paragraph6 open class Rectangle { open fun draw() { /* ... */ } } class Square() : Rectangle() { override fun draw() { /** * * hehe */ super.draw() } } class Square2() : Rectangle() { override fun draw() { //hehe /* hehe */ super.draw() } } class Square2() : Rectangle() { override fun draw() { val q = super.draw() } } class A: Runnable { override fun run() { } } ================================================ FILE: diktat-rules/src/test/resources/test/paragraph6/useless-override/UselessOverrideTest.kt ================================================ package test.paragraph6 open class Rectangle { open fun draw() { /* ... */ } } class Square() : Rectangle() { override fun draw() { /** * * hehe */ super.draw() } } class Square2() : Rectangle() { override fun draw() { //hehe /* hehe */ super.draw() } } class Square2() : Rectangle() { override fun draw() { val q = super.draw() } } class A: Runnable { override fun run() { } } ================================================ FILE: diktat-rules/src/test/resources/test-rules-config.yml ================================================ - name: DIKTAT_COMMON enabled: true configuration: domainName: com.saveourtool.diktat kotlinVersion: 1.4.21 testDirs: "test, androidUnitTest" - name: CLASS_NAME_INCORRECT enabled: true - name: CONSTANT_UPPERCASE enabled: true - name: ENUM_VALUE enabled: true - name: EXCEPTION_SUFFIX enabled: true configuration: { } - name: FILE_NAME_INCORRECT enabled: true - name: FILE_NAME_MATCH_CLASS enabled: true - name: FUNCTION_BOOLEAN_PREFIX enabled: true - name: FUNCTION_NAME_INCORRECT_CASE enabled: true - name: TYPEALIAS_NAME_INCORRECT_CASE enabled: true - name: GENERIC_NAME enabled: true - name: IDENTIFIER_LENGTH enabled: true - name: OBJECT_NAME_INCORRECT enabled: true - name: PACKAGE_NAME_INCORRECT_CASE enabled: true - name: PACKAGE_NAME_INCORRECT_PREFIX enabled: true - name: PACKAGE_NAME_INCORRECT_SYMBOLS enabled: true - name: PACKAGE_NAME_INCORRECT_PATH enabled: true - name: PACKAGE_NAME_MISSING enabled: true - name: VARIABLE_HAS_PREFIX enabled: true - name: VARIABLE_NAME_INCORRECT enabled: true - name: VARIABLE_NAME_INCORRECT_FORMAT enabled: true - name: MISSING_KDOC_ON_FUNCTION enabled: true - name: MISSING_KDOC_TOP_LEVEL enabled: true - name: MISSING_KDOC_CLASS_ELEMENTS enabled: true - name: KDOC_WITHOUT_PARAM_TAG enabled: true - name: KDOC_WITHOUT_RETURN_TAG enabled: true - name: KDOC_WITHOUT_THROWS_TAG enabled: true - name: KDOC_EMPTY_KDOC enabled: true - name: INCORRECT_PACKAGE_SEPARATOR enabled: true - name: KDOC_NO_DEPRECATED_TAG enabled: true - name: KDOC_NO_EMPTY_TAGS enabled: true - name: KDOC_WRONG_SPACES_AFTER_TAG enabled: true - name: KDOC_WRONG_TAGS_ORDER enabled: true - name: KDOC_NO_NEWLINES_BETWEEN_BASIC_TAGS enabled: true - name: KDOC_NEWLINES_BEFORE_BASIC_TAGS enabled: true - name: KDOC_NO_NEWLINE_AFTER_SPECIAL_TAGS enabled: true - name: KDOC_TRIVIAL_KDOC_ON_FUNCTION enabled: 'true' - name: HEADER_WRONG_FORMAT enabled: true - name: HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE enabled: true - name: HEADER_MISSING_OR_WRONG_COPYRIGHT enabled: true configuration: isCopyrightMandatory: false copyrightText: Copyright (c) This is an example. 2012-2020. All rights reserved. - name: HEADER_NOT_BEFORE_PACKAGE enabled: true - name: KDOC_CONTAINS_DATE_OR_AUTHOR enabled: true - name: FILE_IS_TOO_LONG enabled: true configuration: maxSize: '2000' - name: COMMENTED_OUT_CODE enabled: true - name: FILE_CONTAINS_ONLY_COMMENTS enabled: true - name: FILE_UNORDERED_IMPORTS enabled: true - name: FILE_INCORRECT_BLOCKS_ORDER enabled: true - name: FILE_NO_BLANK_LINE_BETWEEN_BLOCKS enabled: true - name: FILE_WILDCARD_IMPORTS enabled: true configuration: allowedWildcards: "" # Allowed wildcards for imports (e.g. "import com.saveourtool.diktat.*, import org.jetbrains.kotlin.*") useRecommendedImportsOrder: true - name: NO_BRACES_IN_CONDITIONALS_AND_LOOPS enabled: true - name: WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES enabled: true - name: BLANK_LINE_BETWEEN_PROPERTIES enabled: true - name: BRACES_BLOCK_STRUCTURE_ERROR enabled: true configuration: openBraceNewline: 'True' closeBraceNewline: 'True' - name: WRONG_INDENTATION enabled: true configuration: newlineAtEnd: true extendedIndentOfParameters: false alignedParameters: true extendedIndentForExpressionBodies: false extendedIndentAfterOperators: true indentationSize: 4 - name: EMPTY_BLOCK_STRUCTURE_ERROR enabled: true configuration: styleEmptyBlockWithNewline: 'True' allowEmptyBlocks: 'False' - name: MORE_THAN_ONE_STATEMENT_PER_LINE enabled: true - name: LONG_LINE enabled: true configuration: lineLength: '120' - name: REDUNDANT_SEMICOLON enabled: true - name: WRONG_NEWLINES enabled: true - name: TOO_MANY_CONSECUTIVE_SPACES enabled: true configuration: max_spaces: '1' saveInitialFormattingForEnums: false - name: TOO_MANY_BLANK_LINES enabled: true - name: WRONG_WHITESPACE enabled: true - name: BACKTICKS_PROHIBITED enabled: true - name: STRING_CONCATENATION enabled: true - name: WHEN_WITHOUT_ELSE enabled: true - name: ANNOTATION_NEW_LINE enabled: true - name: ENUMS_SEPARATED enabled: true - name: LONG_NUMERICAL_VALUES_SEPARATED enabled: true configuration: maxNumberLength: '5' maxBlockLength: '3' - name: WRONG_DECLARATIONS_ORDER enabled: true configuration: sortEnum: true sortProperty: true - name: WRONG_MULTIPLE_MODIFIERS_ORDER enabled: true ================================================ FILE: diktat-ruleset/build.gradle.kts ================================================ import com.saveourtool.diktat.buildutils.configurePom import com.saveourtool.diktat.buildutils.configurePublications import com.github.jengelman.gradle.plugins.shadow.ShadowExtension import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar @Suppress("DSL_SCOPE_VIOLATION", "RUN_IN_SCRIPT") // https://github.com/gradle/gradle/issues/22797 plugins { id("com.saveourtool.diktat.buildutils.kotlin-jvm-configuration") id("com.saveourtool.diktat.buildutils.code-quality-convention") id("com.saveourtool.diktat.buildutils.publishing-configuration") alias(libs.plugins.shadow) `maven-publish` } project.description = "This module builds jar that can be used to run diktat using ktlint -R via command line" dependencies { api(projects.diktatRules) { // Kotlin runtime & libraries will be provided by ktlint executable exclude("org.jetbrains.kotlin", "kotlin-stdlib-common") exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk8") exclude("org.jetbrains.kotlin", "kotlin-stdlib") exclude("org.jetbrains.kotlin", "kotlin-compiler-embeddable") } implementation(projects.diktatKtlintEngine) { // Kotlin runtime & libraries will be provided by ktlint executable exclude("org.jetbrains.kotlin", "kotlin-stdlib-common") exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk8") exclude("org.jetbrains.kotlin", "kotlin-stdlib") exclude("org.jetbrains.kotlin", "kotlin-compiler-embeddable") } implementation(libs.ktlint.cli.ruleset.core) implementation(libs.ktlint.logger) implementation(libs.slf4j.api) } tasks.named("shadowJar") { archiveBaseName.set("diktat") archiveClassifier.set("") // need to relocate serialization from kaml to avoid conflicts with KtLint relocate("kotlinx.serialization", "com.saveourtool.kotlinx_serialization") duplicatesStrategy = DuplicatesStrategy.FAIL } // disable default jar tasks.named("jar") { enabled = false } // it triggers shadowJar with default build tasks { build { dependsOn(shadowJar) } test { dependsOn(shadowJar) } } publishing { publications { // it creates a publication for shadowJar create("shadow") { // https://github.com/johnrengelman/shadow/issues/417#issuecomment-830668442 project.extensions.configure { component(this@create) } this.artifactId = "diktat" this.pom { configurePom(project) // need to override name name.set("diktat") } } } } configurePublications() ================================================ FILE: diktat-ruleset/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/DiktatRuleSetProviderV3Spi.kt ================================================ package com.saveourtool.diktat.ruleset.rules import com.saveourtool.diktat.DIKTAT_ANALYSIS_CONF import com.saveourtool.diktat.common.config.rules.DIKTAT_CONF_PROPERTY import com.saveourtool.diktat.common.config.rules.DIKTAT_RULE_SET_ID import com.saveourtool.diktat.ktlint.KtLintRuleWrapper.Companion.toKtLint import com.saveourtool.diktat.ruleset.config.DiktatRuleConfigYamlReader import com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3 import com.pinterest.ktlint.logger.api.initKtLintKLogger import com.pinterest.ktlint.rule.engine.core.api.RuleProvider import com.pinterest.ktlint.rule.engine.core.api.RuleSetId import io.github.oshai.kotlinlogging.KotlinLogging import org.slf4j.Logger import java.io.File import java.io.InputStream /** * [RuleSetProviderV3] that provides diKTat ruleset. * * By default, it is expected to have `diktat-analysis.yml` configuration in the root folder where 'ktlint' is run * otherwise it will use default configuration where some rules are disabled. * * This class is registered in [resources/META-INF/services/com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3] * * The no-argument constructor is used by the Java SPI interface. */ class DiktatRuleSetProviderV3Spi : RuleSetProviderV3( id = RuleSetId(DIKTAT_RULE_SET_ID), ) { private val diktatRuleConfigReader = DiktatRuleConfigYamlReader() private val diktatRuleSetFactory = DiktatRuleSetFactoryImpl() init { // Need to init KtLint logger to set log level from CLI KotlinLogging.logger(Logger.ROOT_LOGGER_NAME).initKtLintKLogger() } override fun getRuleProviders(): Set = diktatRuleSetFactory(diktatRuleConfigReader(readConfigFile())) .toKtLint() private fun readConfigFile(): InputStream { val resourceFileName = resolveConfigFile() val resourceFile = File(resourceFileName) return if (resourceFile.exists()) { log.debug { "Using $DIKTAT_ANALYSIS_CONF file from the following path: ${resourceFile.absolutePath}" } resourceFile.inputStream() } else { log.debug { "Using the default $DIKTAT_ANALYSIS_CONF file from the class path" } javaClass.classLoader.getResourceAsStream(resourceFileName) ?: run { log.error { "Not able to open file $resourceFileName from the resources" } object : InputStream() { override fun read(): Int = -1 } } } } private fun resolveConfigFile(): String { val possibleConfigs: Sequence = sequence { yield(DIKTAT_ANALYSIS_CONF) yield(resolveConfigFileFromJarLocation()) yield(resolveConfigFileFromSystemProperty()) } log.debug { "Will run $DIKTAT_RULE_SET_ID with $DIKTAT_ANALYSIS_CONF" + " (it can be placed to the run directory or the default file from resources will be used)" } val configPath = possibleConfigs .firstOrNull { it != null && File(it).exists() } return configPath ?: run { val possibleConfigsList = possibleConfigs.toList() log.warn { "Configuration file not found in directory where diktat is run (${possibleConfigsList[0]}) " + "or in the directory where diktat.jar is stored (${possibleConfigsList[1]}) " + "or in system property (${possibleConfigsList[2]}), " + "the default file included in jar will be used. " + "Some configuration options will be disabled or substituted with defaults. " + "Custom configuration file should be placed in diktat working directory if run from CLI " + "or provided as configuration options in plugins." } DIKTAT_ANALYSIS_CONF } } private fun resolveConfigFileFromJarLocation(): String { // for some aggregators of static analyzers we need to provide configuration for cli // in this case diktat would take the configuration from the directory where jar file is stored val ruleSetProviderPath = javaClass .protectionDomain .codeSource .location .toURI() val configPathWithFileName = File(ruleSetProviderPath).absolutePath val indexOfName = configPathWithFileName.lastIndexOf(File.separator) val configPath = if (indexOfName > -1) configPathWithFileName.substring(0, indexOfName) else configPathWithFileName return "$configPath${File.separator}$DIKTAT_ANALYSIS_CONF" } private fun resolveConfigFileFromSystemProperty(): String? = System.getProperty(DIKTAT_CONF_PROPERTY) companion object { private val log = KotlinLogging.logger {} } } ================================================ FILE: diktat-ruleset/src/main/resources/META-INF/services/com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3 ================================================ com.saveourtool.diktat.ruleset.rules.DiktatRuleSetProviderV3Spi ================================================ FILE: diktat-runner/build.gradle.kts ================================================ import com.saveourtool.diktat.buildutils.configurePublications import com.saveourtool.diktat.buildutils.configurePublishing @Suppress("DSL_SCOPE_VIOLATION", "RUN_IN_SCRIPT") // https://github.com/gradle/gradle/issues/22797 plugins { id("com.saveourtool.diktat.buildutils.kotlin-jvm-configuration") id("com.saveourtool.diktat.buildutils.code-quality-convention") id("com.saveourtool.diktat.buildutils.publishing-configuration") alias(libs.plugins.shadow) } project.description = "This module contains runner for diktat" dependencies { api(projects.diktatApi) implementation(projects.diktatKtlintEngine) implementation(projects.diktatRules) } tasks.shadowJar { archiveClassifier.set("shadow") duplicatesStrategy = DuplicatesStrategy.FAIL } // https://github.com/gradle/gradle/issues/10384#issuecomment-1279708395 val shadowElement: Configuration by configurations.creating { isCanBeConsumed = true isCanBeResolved = false attributes { attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY)) attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME)) attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.SHADOWED)) } outgoing.artifact(tasks.shadowJar) } components.named("java").configure { addVariantsFromConfiguration(shadowElement) {} } publishing { publications { create("maven") { from(components["java"]) } } } configurePublications() configurePublishing() ================================================ FILE: diktat-runner/src/main/kotlin/com/saveourtool/diktat/DiktatFactories.kt ================================================ /** * Contains only initialized factories */ package com.saveourtool.diktat import com.saveourtool.diktat.api.DiktatBaselineFactory import com.saveourtool.diktat.api.DiktatReporterFactory import com.saveourtool.diktat.api.DiktatRuleConfigReader import com.saveourtool.diktat.api.DiktatRuleSetFactory import com.saveourtool.diktat.ktlint.DiktatBaselineFactoryImpl import com.saveourtool.diktat.ktlint.DiktatProcessorFactoryImpl import com.saveourtool.diktat.ktlint.DiktatReporterFactoryImpl import com.saveourtool.diktat.ruleset.config.DiktatRuleConfigYamlReader import com.saveourtool.diktat.ruleset.rules.DiktatRuleSetFactoryImpl import generated.KTLINT_VERSION /** * Info about engine */ const val ENGINE_INFO: String = "Ktlint: $KTLINT_VERSION" /** * @return initialized [DiktatRuleConfigReader] */ val diktatRuleConfigReader: DiktatRuleConfigReader = DiktatRuleConfigYamlReader() /** * @return initialized [DiktatRuleSetFactory] */ val diktatRuleSetFactory: DiktatRuleSetFactory = DiktatRuleSetFactoryImpl() /** * @return initialized [DiktatProcessorFactory] */ val diktatProcessorFactory: DiktatProcessorFactory = DiktatProcessorFactoryImpl() /** * @return initialized [DiktatBaselineFactory] */ val diktatBaselineFactory: DiktatBaselineFactory = DiktatBaselineFactoryImpl() /** * @return initialized [DiktatReporterFactory] */ val diktatReporterFactory: DiktatReporterFactory = DiktatReporterFactoryImpl() /** * @return initialized [DiktatRunnerFactory] */ val diktatRunnerFactory: DiktatRunnerFactory = DiktatRunnerFactory( diktatRuleConfigReader, diktatRuleSetFactory, diktatProcessorFactory, diktatBaselineFactory, diktatReporterFactory, ) ================================================ FILE: examples/README.md ================================================ ================================================ FILE: examples/gradle-groovy-dsl/build.gradle ================================================ plugins { id "com.saveourtool.diktat" version "2.0.0" } repositories { mavenCentral() } diktat { inputs { it.include ("src/**/*.kt") } diktatConfigFile = file(rootDir.path + "/diktat-analysis.yml") } ================================================ FILE: examples/gradle-groovy-dsl/diktat-analysis.yml ================================================ # Common configuration - name: DIKTAT_COMMON configuration: # put your package name here - it will be autofixed and checked domainName: your.name.here testDirs: test # expected values: disabledChapters: "Naming, Comments, General, Variables, Functions, Classes" # or: "1, 2, 3, 4, 5, 6" disabledChapters: "" kotlinVersion: 2.1 srcDirectories: "main" # Checks that the Class/Enum/Interface name matches Pascal case - name: CLASS_NAME_INCORRECT enabled: true # all code blocks with MyAnnotation will be ignored and not checked ignoreAnnotated: [ MyAnnotation ] # Checks that CONSTANT (treated as const val from companion object or class level) is in non UPPER_SNAKE_CASE - name: CONSTANT_UPPERCASE enabled: true # Checks that enum value is in upper SNAKE_CASE or in PascalCase depending on the config. UPPER_SNAKE_CASE is the default, but can be changed by 'enumStyle' config - name: ENUM_VALUE enabled: true configuration: # Two options: SNAKE_CASE (default), PascalCase enumStyle: SNAKE_CASE # Checks that class which extends any Exception class has Exception suffix - name: EXCEPTION_SUFFIX enabled: true # Checks that file name has extension - name: FILE_NAME_INCORRECT enabled: true # Checks that file name matches class name, if it is only one class in file - name: FILE_NAME_MATCH_CLASS enabled: true # Checks that functions/methods which return boolean have special prefix like "is/should/e.t.c" - name: FUNCTION_BOOLEAN_PREFIX enabled: true configuration: allowedPrefixes: "" # A list of functions that return boolean and are allowed to use. Input is in a form "foo, bar". # Checks that function/method name is in lowerCamelCase - name: FUNCTION_NAME_INCORRECT_CASE enabled: true # Checks that typealias name is in PascalCase - name: TYPEALIAS_NAME_INCORRECT_CASE enabled: true # Checks that generic name doesn't contain more than 1 letter (capital). It can be followed by numbers, example: T12, T - name: GENERIC_NAME enabled: true # Identifier length should be in range [2,64] except names that used in industry like {i, j} and 'e' for catching exceptions - name: IDENTIFIER_LENGTH enabled: true # Checks that the object matches PascalCase - name: OBJECT_NAME_INCORRECT enabled: true # Checks that package name is in correct (lower) case - name: PACKAGE_NAME_INCORRECT_CASE enabled: true # Checks that package name starts with the company's domain - name: PACKAGE_NAME_INCORRECT_PREFIX enabled: false # Checks that package name does not have incorrect symbols like underscore or non-ASCII letters/digits - name: PACKAGE_NAME_INCORRECT_SYMBOLS enabled: true # Checks that the path for a file matches with a package name - name: PACKAGE_NAME_INCORRECT_PATH enabled: false # Checks that package name is in the file - name: PACKAGE_NAME_MISSING enabled: true # Checks that variable does not have prefix (like mVariable or M_VARIABLE) - name: VARIABLE_HAS_PREFIX enabled: true # Checks that variable does not contain one single letter, only exceptions are fixed names that used in industry like {i, j} - name: VARIABLE_NAME_INCORRECT enabled: true # Checks that the name of variable is in lowerCamelCase and contains only ASCII letters - name: VARIABLE_NAME_INCORRECT_FORMAT enabled: true # Checks that functions have kdoc - name: MISSING_KDOC_ON_FUNCTION enabled: true # Checks that on file level internal or public class or function has missing KDoc - name: MISSING_KDOC_TOP_LEVEL enabled: true # Checks that accessible internal elements (protected, public, internal) in a class are documented - name: MISSING_KDOC_CLASS_ELEMENTS enabled: true # Checks that accessible method parameters are documented in KDoc - name: KDOC_WITHOUT_PARAM_TAG enabled: true # Checks that accessible method explicit return type is documented in KDoc - name: KDOC_WITHOUT_RETURN_TAG enabled: true # Checks that accessible method throw keyword is documented in KDoc - name: KDOC_WITHOUT_THROWS_TAG enabled: true # Checks that KDoc is not empty - name: KDOC_EMPTY_KDOC enabled: true # Checks that underscore is correctly used to split package naming - name: INCORRECT_PACKAGE_SEPARATOR enabled: true # Checks that code block doesn't contain kdoc comments - name: COMMENTED_BY_KDOC enabled: true # Checks that there is no @deprecated tag in kdoc - name: KDOC_NO_DEPRECATED_TAG enabled: true # Checks that there is no empty content in kdoc tags - name: KDOC_NO_EMPTY_TAGS enabled: true # Checks that there is only one space after kdoc tag - name: KDOC_WRONG_SPACES_AFTER_TAG enabled: true # Checks tags order in kDoc. `@param`, `@return`, `@throws` - name: KDOC_WRONG_TAGS_ORDER enabled: true # Checks that there is no newline of empty KDoc line (with leading asterisk) between `@param`, `@return`, `@throws` tags - name: KDOC_NO_NEWLINES_BETWEEN_BASIC_TAGS enabled: true # Checks that block of tags @param, @return, @throws is separated from previous part of KDoc by exactly one empty line - name: KDOC_NEWLINES_BEFORE_BASIC_TAGS enabled: true # Checks that special tags `@apiNote`, `@implNote`, `@implSpec` have exactly one empty line after - name: KDOC_NO_NEWLINE_AFTER_SPECIAL_TAGS enabled: true # Checks that kdoc does not contain @author tag or date - name: KDOC_CONTAINS_DATE_OR_AUTHOR enabled: true configuration: versionRegex: \d+\.\d+\.\d+[-.\w\d]* # Checks that KDoc does not contain single line with words 'return', 'get' or 'set' - name: KDOC_TRIVIAL_KDOC_ON_FUNCTION enabled: true # Checks that there is newline after header KDoc - name: HEADER_WRONG_FORMAT enabled: true # Checks that file with zero or >1 classes has header KDoc - name: HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE enabled: true # Checks that copyright exists on top of file and is properly formatted (as a block comment) - name: HEADER_MISSING_OR_WRONG_COPYRIGHT enabled: true configuration: isCopyrightMandatory: false copyrightText: 'Copyright (c) Your Company Name Here. 2010-;@currYear;' # Checks that header kdoc is located before package directive - name: HEADER_NOT_BEFORE_PACKAGE enabled: true # Checks that file does not contain lines > maxSize - name: FILE_IS_TOO_LONG enabled: true configuration: # number of lines maxSize: '2000' # Checks that file does not contain commented out code - name: COMMENTED_OUT_CODE enabled: true # Checks that file does not contain only comments, imports and package directive - name: FILE_CONTAINS_ONLY_COMMENTS enabled: true # Orders imports alphabetically - name: FILE_UNORDERED_IMPORTS enabled: true configuration: # use logical imports grouping with sorting inside of a group useRecommendedImportsOrder: true # Checks that general order of code parts is right - name: FILE_INCORRECT_BLOCKS_ORDER enabled: true # Checks that there is exactly one line between code blocks - name: FILE_NO_BLANK_LINE_BETWEEN_BLOCKS enabled: true # Checks that there is no wildcard imports. Exception: allowedWildcards - name: FILE_WILDCARD_IMPORTS enabled: true configuration: allowedWildcards: "" # Allowed wildcards for imports (e.g. "import com.saveourtool.diktat.*, import org.jetbrains.kotlin.*") useRecommendedImportsOrder: true # Checks unused imports - name: UNUSED_IMPORT enabled: true configuration: deleteUnusedImport: true # Checks that braces are used in if, else, when, for, do, and while statements. Exception: single line ternary operator statement - name: NO_BRACES_IN_CONDITIONALS_AND_LOOPS enabled: true # Checks that the declaration part of a class-like code structures (class/interface/etc.) is in the proper order - name: WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES enabled: true # Checks that properties with comments are separated by a blank line - name: BLANK_LINE_BETWEEN_PROPERTIES enabled: true # Checks top level order - name: TOP_LEVEL_ORDER enabled: true # Checks that non-empty code blocks with braces follow the K&R style (1TBS or OTBS style) - name: BRACES_BLOCK_STRUCTURE_ERROR enabled: true configuration: openBraceNewline: 'True' closeBraceNewline: 'True' # Checks that indentation is correct - name: WRONG_INDENTATION enabled: true configuration: # Is newline at the end of a file needed newlineAtEnd: true # If true: in parameter list when parameters are split by newline they are indented with two indentations instead of one extendedIndentOfParameters: false # If true: if first parameter in parameter list is on the same line as opening parenthesis, then other parameters can be aligned with it alignedParameters: true # If true, expression bodies which begin on a separate line are indented # using a continuation indent. The default is false. # # This flag is called CONTINUATION_INDENT_FOR_EXPRESSION_BODIES in IDEA and # ij_kotlin_continuation_indent_for_expression_bodies in .editorconfig. extendedIndentForExpressionBodies: false # If true: if expression is split by newline after operator like +/-/`*`, then the next line is indented with two indentations instead of one extendedIndentAfterOperators: true # If true: when dot qualified expression starts on a new line, this line will be indented with two indentations instead of one extendedIndentBeforeDot: false # The indentation size for each file indentationSize: 4 # Checks that there is no empty blocks in a file. # If allowEmptyBlocks is true, checks that it follows correct style (have a newline) - name: EMPTY_BLOCK_STRUCTURE_ERROR enabled: true configuration: # Whether a newline after `{` is required in an empty block styleEmptyBlockWithNewline: 'True' allowEmptyBlocks: 'False' # Checks that there is no more than one statement per line - name: MORE_THAN_ONE_STATEMENT_PER_LINE enabled: true # Checks that the line length is < lineLength parameter - name: LONG_LINE enabled: true configuration: lineLength: '120' # Checks that semicolons are not used at the end of a line - name: REDUNDANT_SEMICOLON enabled: true # Checks that line breaks follow code style guide: rule 3.6 - name: WRONG_NEWLINES enabled: true configuration: # If the number of parameters on one line is more than this threshold, all parameters will be placed on separate lines. maxParametersInOneLine: 2 # 3 by default. maxCallsInOneLine: 3 # Checks trailing comma - name: TRAILING_COMMA enabled: true configuration: # VALUE_ARGUMENT valueArgument: false # VALUE_PARAMETER valueParameter: false # REFERENCE_EXPRESSION indices: false # WHEN_CONDITION_WITH_EXPRESSION whenConditions: false # STRING_TEMPLATE collectionLiteral: false # TYPE_PROJECTION typeArgument: false # TYPE_PARAMETER typeParameter: false # DESTRUCTURING_DECLARATION_ENTRY destructuringDeclaration: false # Checks that there are not too many consecutive spaces in line - name: TOO_MANY_CONSECUTIVE_SPACES enabled: true configuration: # Maximum allowed number of consecutive spaces (not counting indentation) maxSpaces: '1' # Whether formatting for enums should be kept without checking saveInitialFormattingForEnums: false # Inspection that checks if a long dot qualified expression is used in condition or as an argument - name: COMPLEX_EXPRESSION enabled: true # Checks that blank lines are used correctly. # For example: triggers when there are too many blank lines between function declaration - name: TOO_MANY_BLANK_LINES enabled: true # Checks that usage of horizontal spaces doesn't violate code style guide - name: WRONG_WHITESPACE enabled: true # Checks that backticks (``) are not used in the identifier name, except the case when it is test method (marked with @Test annotation) - name: BACKTICKS_PROHIBITED enabled: true # Checks that a single line concatenation of strings is not used - name: STRING_CONCATENATION enabled: true # Checks that each when statement have else in the end - name: WHEN_WITHOUT_ELSE enabled: true # Checks that annotation is on a single line - name: ANNOTATION_NEW_LINE enabled: true # Checks that method annotated with `Preview` annotation is private and has Preview suffix - name: PREVIEW_ANNOTATION enabled: true # Checks that enum structure is correct: enum entries should be separated by comma and line break and last entry should have semicolon in the end. - name: ENUMS_SEPARATED enabled: true # Checks that value on integer or float constant is not too big - name: LONG_NUMERICAL_VALUES_SEPARATED enabled: true configuration: # Maximum number of digits which are not split maxNumberLength: '5' # Maximum number of digits between separators maxBlockLength: '3' # Checks magic number - name: MAGIC_NUMBER enabled: true configuration: # Ignore numbers from test ignoreTest: "true" # Ignore numbers ignoreNumbers: "-1, 1, 0, 2, 0U, 1U, 2U, -1L, 0L, 1L, 2L, 0UL, 1UL, 2UL" # Is ignore override hashCode function ignoreHashCodeFunction: "true" # Is ignore property ignorePropertyDeclaration: "false" # Is ignore local variable ignoreLocalVariableDeclaration: "false" # Is ignore value parameter ignoreValueParameter: "true" # Is ignore constant ignoreConstantDeclaration: "true" # Is ignore property in companion object ignoreCompanionObjectPropertyDeclaration: "true" # Is ignore numbers in enum ignoreEnums: "false" # Is ignore number in ranges ignoreRanges: "false" # Is ignore number in extension function ignoreExtensionFunctions: "false" # Is ignore number in pairs created using to ignorePairsCreatedUsingTo: "false" # Checks that order of enum values or constant property inside companion is correct - name: WRONG_DECLARATIONS_ORDER enabled: true configuration: # Whether enum members should be sorted alphabetically sortEnum: true # Whether class properties should be sorted alphabetically sortProperty: true # Checks that multiple modifiers sequence is in the correct order - name: WRONG_MULTIPLE_MODIFIERS_ORDER enabled: true # Checks that identifier has appropriate name (See table of rule 1.2 part 6) - name: CONFUSING_IDENTIFIER_NAMING enabled: true # Checks year in the copyright - name: WRONG_COPYRIGHT_YEAR enabled: true # Inspection that checks if local variables are declared close to the first usage site - name: LOCAL_VARIABLE_EARLY_DECLARATION enabled: true # Try to avoid initialize val by null (e.g. val a: Int? = null -> val a: Int = 0) - name: NULLABLE_PROPERTY_TYPE enabled: true # Inspection that checks if there is a blank line before kDoc and none after - name: WRONG_NEWLINES_AROUND_KDOC enabled: true # Inspection that checks if there is no blank lines before first comment - name: FIRST_COMMENT_NO_BLANK_LINE enabled: true # Inspection that checks if there are blank lines between code and comment and between code start token and comment's text - name: COMMENT_WHITE_SPACE enabled: true configuration: maxSpacesBeforeComment: 2 maxSpacesInComment: 1 # Inspection that checks if all comment's are inside if-else code blocks. Exception is general if comment - name: IF_ELSE_COMMENTS enabled: true # Type aliases provide alternative names for existing types when type's reference text is longer 25 chars - name: TYPE_ALIAS enabled: true configuration: typeReferenceLength: '25' # max length of type reference # Checks if casting can be omitted - name: SMART_CAST_NEEDED enabled: true # Checks that variables of generic types have explicit type declaration - name: GENERIC_VARIABLE_WRONG_DECLARATION enabled: true # Inspection that checks if string template has redundant curly braces - name: STRING_TEMPLATE_CURLY_BRACES enabled: true # Variables with `val` modifier - are immutable (read-only). Usage of such variables instead of `var` variables increases # robustness and readability of code, because `var` variables can be reassigned several times in the business logic. # This rule prohibits usage of `var`s as local variables - the only exception is accumulators and counters - name: SAY_NO_TO_VAR enabled: true # Inspection that checks if string template has redundant quotes - name: STRING_TEMPLATE_QUOTES enabled: true # Check if there are redundant nested if-statements, which could be collapsed into a single one by concatenating their conditions - name: COLLAPSE_IF_STATEMENTS enabled: true configuration: startCollapseFromNestedLevel: 2 # Checks that floating-point values are not used in arithmetic expressions - name: FLOAT_IN_ACCURATE_CALCULATIONS enabled: true # Checks that function length isn't too long - name: TOO_LONG_FUNCTION enabled: true configuration: maxFunctionLength: '30' # max length of function isIncludeHeader: 'false' # count function's header # Warns if there are nested functions - name: AVOID_NESTED_FUNCTIONS enabled: true # Checks that lambda inside function parameters is in the end - name: LAMBDA_IS_NOT_LAST_PARAMETER enabled: true # Checks that function doesn't contains too many parameters - name: TOO_MANY_PARAMETERS enabled: true configuration: maxParameterListSize: '5' # max parameters size # Checks that function doesn't have too many nested blocks - name: NESTED_BLOCK enabled: true configuration: maxNestedBlockQuantity: '4' # Checks that function use default values, instead overloading - name: WRONG_OVERLOADING_FUNCTION_ARGUMENTS enabled: true # Checks that using runBlocking inside async block code - name: RUN_BLOCKING_INSIDE_ASYNC enabled: true # Checks that property in constructor doesn't contain comment - name: KDOC_NO_CONSTRUCTOR_PROPERTY enabled: true configuration: isParamTagsForParameters: true # create param tags for parameters without val or var isParamTagsForPrivateProperties: true # create param tags for private properties isParamTagsForGenericTypes: true # create param tags for generic types # Checks that the long lambda has parameters - name: TOO_MANY_LINES_IN_LAMBDA enabled: true configuration: maxLambdaLength: 10 # max length of lambda without parameters # Checks that using unnecessary, custom label - name: CUSTOM_LABEL enabled: true # Check that lambda with inner lambda doesn't use implicit parameter - name: PARAMETER_NAME_IN_OUTER_LAMBDA enabled: true # Checks that property in KDoc present in class - name: KDOC_EXTRA_PROPERTY enabled: true # There's a property in KDoc which is already present - name: KDOC_DUPLICATE_PROPERTY enabled: true # Checks that KDoc in constructor has property tag but with comment inside constructor - name: KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT enabled: true # if a class has single constructor, it should be converted to a primary constructor - name: SINGLE_CONSTRUCTOR_SHOULD_BE_PRIMARY enabled: true # Checks if class can be made as data class - name: USE_DATA_CLASS enabled: true # Checks that never use the name of a variable in the custom getter or setter - name: WRONG_NAME_OF_VARIABLE_INSIDE_ACCESSOR enabled: true # Checks that classes have only one init block - name: MULTIPLE_INIT_BLOCKS enabled: true # Checks that there are abstract functions in abstract class - name: CLASS_SHOULD_NOT_BE_ABSTRACT enabled: true # Checks if there are any trivial getters or setters - name: TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED enabled: true # Checks that no custom getters and setters are used for properties. It is a more wide rule than TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED # Kotlin compiler automatically generates `get` and `set` methods for properties and also lets the possibility to override it. # But in all cases it is very confusing when `get` and `set` are overridden for a developer who uses this particular class. # Developer expects to get the value of the property, but receives some unknown value and some extra side effect hidden by the custom getter/setter. # Use extra functions for it instead. - name: CUSTOM_GETTERS_SETTERS enabled: true # Checks if null-check was used explicitly (for example: if (a == null)) # Try to avoid explicit null checks (explicit comparison with `null`) # Kotlin is declared as [Null-safe](https://kotlinlang.org/docs/reference/null-safety.html) language. # But Kotlin architects wanted Kotlin to be fully compatible with Java, that's why `null` keyword was also introduced in Kotlin. # There are several code-structures that can be used in Kotlin to avoid null-checks. For example: `?:`, `.let {}`, `.also {}`, e.t.c - name: AVOID_NULL_CHECKS enabled: true # Checks if class instantiation can be wrapped in `apply` for better readability - name: COMPACT_OBJECT_INITIALIZATION enabled: true # Checks explicit supertype qualification - name: USELESS_SUPERTYPE enabled: true # Checks if extension function with the same signature don't have related classes - name: EXTENSION_FUNCTION_SAME_SIGNATURE enabled: true # Checks if there is empty primary constructor - name: EMPTY_PRIMARY_CONSTRUCTOR enabled: true # In case of not using field keyword in property accessors, # there should be explicit backing property with the name of real property # Example: val table get() {if (_table == null) ...} -> table should have _table - name: NO_CORRESPONDING_PROPERTY enabled: true # Checks if there is class/object that can be replace with extension function - name: AVOID_USING_UTILITY_CLASS enabled: true # If there is stateless class it is preferred to use object - name: OBJECT_IS_PREFERRED enabled: true # If there exists negated version of function you should prefer it instead of !functionCall - name: INVERSE_FUNCTION_PREFERRED enabled: true # Checks if class can be converted to inline class - name: INLINE_CLASS_CAN_BE_USED enabled: true # If file contains class, then it can't contain extension functions for the same class - name: EXTENSION_FUNCTION_WITH_CLASS enabled: true # Check if kts script contains other functions except run code - name: RUN_IN_SCRIPT enabled: true # Check if boolean expression can be simplified - name: COMPLEX_BOOLEAN_EXPRESSION enabled: true # Check if range can replace with until or `rangeTo` function with range - name: CONVENTIONAL_RANGE enabled: true configuration: isRangeToIgnore: false # Check if there is a call of print()\println() or console.log(). Assumption that it's a debug print - name: DEBUG_PRINT enabled: true # Check that typealias name is in PascalCase - name: TYPEALIAS_NAME_INCORRECT_CASE enabled: true # Should change property length - 1 to property lastIndex - name: USE_LAST_INDEX enabled: true # Only properties from the primary constructor should be documented in a @property tag in class KDoc - name: KDOC_NO_CLASS_BODY_PROPERTIES_IN_HEADER enabled: true ================================================ FILE: examples/gradle-groovy-dsl/src/main/kotlin/AnotherTest.kt ================================================ package whate.ver fun String.createPluginConfig() { val pluginConfig = TomlDecoder.decode( serializer(), fakeFileNode, DecoderConf() ) pluginConfig.configLocation = this.toPath() pluginConfig.prop1 = property1 // comment1 pluginConfig.configLocation2 = this.toPath() // comment2 pluginConfig.prop2 = property2 } ================================================ FILE: examples/gradle-groovy-dsl/src/main/kotlin/Test.kt ================================================ package incorrect class incorrectname: Exception() { fun INCORRECT_FUNCTION() { throw Exception() } // fun myCommentedFunction() { // } val Incorrect_Val = 5 } ================================================ FILE: examples/gradle-kotlin-dsl/build.gradle.kts ================================================ plugins { id("com.saveourtool.diktat") version "2.0.0" } repositories { mavenCentral() } diktat { inputs { include("src/**/*.kt") } } ================================================ FILE: examples/gradle-kotlin-dsl/diktat-analysis.yml ================================================ # Common configuration - name: DIKTAT_COMMON configuration: # put your package name here - it will be autofixed and checked domainName: your.name.here testDirs: test # expected values: disabledChapters: "Naming, Comments, General, Variables, Functions, Classes" # or: "1, 2, 3, 4, 5, 6" disabledChapters: "" kotlinVersion: 2.1 srcDirectories: "main" # Checks that the Class/Enum/Interface name matches Pascal case - name: CLASS_NAME_INCORRECT enabled: true # all code blocks with MyAnnotation will be ignored and not checked ignoreAnnotated: [ MyAnnotation ] # Checks that CONSTANT (treated as const val from companion object or class level) is in non UPPER_SNAKE_CASE - name: CONSTANT_UPPERCASE enabled: true # Checks that enum value is in upper SNAKE_CASE or in PascalCase depending on the config. UPPER_SNAKE_CASE is the default, but can be changed by 'enumStyle' config - name: ENUM_VALUE enabled: true configuration: # Two options: SNAKE_CASE (default), PascalCase enumStyle: SNAKE_CASE # Checks that class which extends any Exception class has Exception suffix - name: EXCEPTION_SUFFIX enabled: true # Checks that file name has extension - name: FILE_NAME_INCORRECT enabled: true # Checks that file name matches class name, if it is only one class in file - name: FILE_NAME_MATCH_CLASS enabled: true # Checks that functions/methods which return boolean have special prefix like "is/should/e.t.c" - name: FUNCTION_BOOLEAN_PREFIX enabled: true configuration: allowedPrefixes: "" # A list of functions that return boolean and are allowed to use. Input is in a form "foo, bar". # Checks that function/method name is in lowerCamelCase - name: FUNCTION_NAME_INCORRECT_CASE enabled: true # Checks that typealias name is in PascalCase - name: TYPEALIAS_NAME_INCORRECT_CASE enabled: true # Checks that generic name doesn't contain more than 1 letter (capital). It can be followed by numbers, example: T12, T - name: GENERIC_NAME enabled: true # Identifier length should be in range [2,64] except names that used in industry like {i, j} and 'e' for catching exceptions - name: IDENTIFIER_LENGTH enabled: true # Checks that the object matches PascalCase - name: OBJECT_NAME_INCORRECT enabled: true # Checks that package name is in correct (lower) case - name: PACKAGE_NAME_INCORRECT_CASE enabled: true # Checks that package name starts with the company's domain - name: PACKAGE_NAME_INCORRECT_PREFIX enabled: false # Checks that package name does not have incorrect symbols like underscore or non-ASCII letters/digits - name: PACKAGE_NAME_INCORRECT_SYMBOLS enabled: true # Checks that the path for a file matches with a package name - name: PACKAGE_NAME_INCORRECT_PATH enabled: false # Checks that package name is in the file - name: PACKAGE_NAME_MISSING enabled: true # Checks that variable does not have prefix (like mVariable or M_VARIABLE) - name: VARIABLE_HAS_PREFIX enabled: true # Checks that variable does not contain one single letter, only exceptions are fixed names that used in industry like {i, j} - name: VARIABLE_NAME_INCORRECT enabled: true # Checks that the name of variable is in lowerCamelCase and contains only ASCII letters - name: VARIABLE_NAME_INCORRECT_FORMAT enabled: true # Checks that functions have kdoc - name: MISSING_KDOC_ON_FUNCTION enabled: true # Checks that on file level internal or public class or function has missing KDoc - name: MISSING_KDOC_TOP_LEVEL enabled: true # Checks that accessible internal elements (protected, public, internal) in a class are documented - name: MISSING_KDOC_CLASS_ELEMENTS enabled: true # Checks that accessible method parameters are documented in KDoc - name: KDOC_WITHOUT_PARAM_TAG enabled: true # Checks that accessible method explicit return type is documented in KDoc - name: KDOC_WITHOUT_RETURN_TAG enabled: true # Checks that accessible method throw keyword is documented in KDoc - name: KDOC_WITHOUT_THROWS_TAG enabled: true # Checks that KDoc is not empty - name: KDOC_EMPTY_KDOC enabled: true # Checks that underscore is correctly used to split package naming - name: INCORRECT_PACKAGE_SEPARATOR enabled: true # Checks that code block doesn't contain kdoc comments - name: COMMENTED_BY_KDOC enabled: true # Checks that there is no @deprecated tag in kdoc - name: KDOC_NO_DEPRECATED_TAG enabled: true # Checks that there is no empty content in kdoc tags - name: KDOC_NO_EMPTY_TAGS enabled: true # Checks that there is only one space after kdoc tag - name: KDOC_WRONG_SPACES_AFTER_TAG enabled: true # Checks tags order in kDoc. `@param`, `@return`, `@throws` - name: KDOC_WRONG_TAGS_ORDER enabled: true # Checks that there is no newline of empty KDoc line (with leading asterisk) between `@param`, `@return`, `@throws` tags - name: KDOC_NO_NEWLINES_BETWEEN_BASIC_TAGS enabled: true # Checks that block of tags @param, @return, @throws is separated from previous part of KDoc by exactly one empty line - name: KDOC_NEWLINES_BEFORE_BASIC_TAGS enabled: true # Checks that special tags `@apiNote`, `@implNote`, `@implSpec` have exactly one empty line after - name: KDOC_NO_NEWLINE_AFTER_SPECIAL_TAGS enabled: true # Checks that kdoc does not contain @author tag or date - name: KDOC_CONTAINS_DATE_OR_AUTHOR enabled: true configuration: versionRegex: \d+\.\d+\.\d+[-.\w\d]* # Checks that KDoc does not contain single line with words 'return', 'get' or 'set' - name: KDOC_TRIVIAL_KDOC_ON_FUNCTION enabled: true # Checks that there is newline after header KDoc - name: HEADER_WRONG_FORMAT enabled: true # Checks that file with zero or >1 classes has header KDoc - name: HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE enabled: true # Checks that copyright exists on top of file and is properly formatted (as a block comment) - name: HEADER_MISSING_OR_WRONG_COPYRIGHT enabled: true configuration: isCopyrightMandatory: false copyrightText: 'Copyright (c) Your Company Name Here. 2010-;@currYear;' # Checks that header kdoc is located before package directive - name: HEADER_NOT_BEFORE_PACKAGE enabled: true # Checks that file does not contain lines > maxSize - name: FILE_IS_TOO_LONG enabled: true configuration: # number of lines maxSize: '2000' # Checks that file does not contain commented out code - name: COMMENTED_OUT_CODE enabled: true # Checks that file does not contain only comments, imports and package directive - name: FILE_CONTAINS_ONLY_COMMENTS enabled: true # Orders imports alphabetically - name: FILE_UNORDERED_IMPORTS enabled: true configuration: # use logical imports grouping with sorting inside of a group useRecommendedImportsOrder: true # Checks that general order of code parts is right - name: FILE_INCORRECT_BLOCKS_ORDER enabled: true # Checks that there is exactly one line between code blocks - name: FILE_NO_BLANK_LINE_BETWEEN_BLOCKS enabled: true # Checks that there is no wildcard imports. Exception: allowedWildcards - name: FILE_WILDCARD_IMPORTS enabled: true configuration: allowedWildcards: "" # Allowed wildcards for imports (e.g. "import com.saveourtool.diktat.*, import org.jetbrains.kotlin.*") useRecommendedImportsOrder: true # Checks unused imports - name: UNUSED_IMPORT enabled: true configuration: deleteUnusedImport: true # Checks that braces are used in if, else, when, for, do, and while statements. Exception: single line ternary operator statement - name: NO_BRACES_IN_CONDITIONALS_AND_LOOPS enabled: true # Checks that the declaration part of a class-like code structures (class/interface/etc.) is in the proper order - name: WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES enabled: true # Checks that properties with comments are separated by a blank line - name: BLANK_LINE_BETWEEN_PROPERTIES enabled: true # Checks top level order - name: TOP_LEVEL_ORDER enabled: true # Checks that non-empty code blocks with braces follow the K&R style (1TBS or OTBS style) - name: BRACES_BLOCK_STRUCTURE_ERROR enabled: true configuration: openBraceNewline: 'True' closeBraceNewline: 'True' # Checks that indentation is correct - name: WRONG_INDENTATION enabled: true configuration: # Is newline at the end of a file needed newlineAtEnd: true # If true: in parameter list when parameters are split by newline they are indented with two indentations instead of one extendedIndentOfParameters: false # If true: if first parameter in parameter list is on the same line as opening parenthesis, then other parameters can be aligned with it alignedParameters: true # If true, expression bodies which begin on a separate line are indented # using a continuation indent. The default is false. # # This flag is called CONTINUATION_INDENT_FOR_EXPRESSION_BODIES in IDEA and # ij_kotlin_continuation_indent_for_expression_bodies in .editorconfig. extendedIndentForExpressionBodies: false # If true: if expression is split by newline after operator like +/-/`*`, then the next line is indented with two indentations instead of one extendedIndentAfterOperators: true # If true: when dot qualified expression starts on a new line, this line will be indented with two indentations instead of one extendedIndentBeforeDot: false # The indentation size for each file indentationSize: 4 # Checks that there is no empty blocks in a file. # If allowEmptyBlocks is true, checks that it follows correct style (have a newline) - name: EMPTY_BLOCK_STRUCTURE_ERROR enabled: true configuration: # Whether a newline after `{` is required in an empty block styleEmptyBlockWithNewline: 'True' allowEmptyBlocks: 'False' # Checks that there is no more than one statement per line - name: MORE_THAN_ONE_STATEMENT_PER_LINE enabled: true # Checks that the line length is < lineLength parameter - name: LONG_LINE enabled: true configuration: lineLength: '120' # Checks that semicolons are not used at the end of a line - name: REDUNDANT_SEMICOLON enabled: true # Checks that line breaks follow code style guide: rule 3.6 - name: WRONG_NEWLINES enabled: true configuration: # If the number of parameters on one line is more than this threshold, all parameters will be placed on separate lines. maxParametersInOneLine: 2 # 3 by default. maxCallsInOneLine: 3 # Checks trailing comma - name: TRAILING_COMMA enabled: true configuration: # VALUE_ARGUMENT valueArgument: false # VALUE_PARAMETER valueParameter: false # REFERENCE_EXPRESSION indices: false # WHEN_CONDITION_WITH_EXPRESSION whenConditions: false # STRING_TEMPLATE collectionLiteral: false # TYPE_PROJECTION typeArgument: false # TYPE_PARAMETER typeParameter: false # DESTRUCTURING_DECLARATION_ENTRY destructuringDeclaration: false # Checks that there are not too many consecutive spaces in line - name: TOO_MANY_CONSECUTIVE_SPACES enabled: true configuration: # Maximum allowed number of consecutive spaces (not counting indentation) maxSpaces: '1' # Whether formatting for enums should be kept without checking saveInitialFormattingForEnums: false # Inspection that checks if a long dot qualified expression is used in condition or as an argument - name: COMPLEX_EXPRESSION enabled: true # Checks that blank lines are used correctly. # For example: triggers when there are too many blank lines between function declaration - name: TOO_MANY_BLANK_LINES enabled: true # Checks that usage of horizontal spaces doesn't violate code style guide - name: WRONG_WHITESPACE enabled: true # Checks that backticks (``) are not used in the identifier name, except the case when it is test method (marked with @Test annotation) - name: BACKTICKS_PROHIBITED enabled: true # Checks that a single line concatenation of strings is not used - name: STRING_CONCATENATION enabled: true # Checks that each when statement have else in the end - name: WHEN_WITHOUT_ELSE enabled: true # Checks that annotation is on a single line - name: ANNOTATION_NEW_LINE enabled: true # Checks that method annotated with `Preview` annotation is private and has Preview suffix - name: PREVIEW_ANNOTATION enabled: true # Checks that enum structure is correct: enum entries should be separated by comma and line break and last entry should have semicolon in the end. - name: ENUMS_SEPARATED enabled: true # Checks that value on integer or float constant is not too big - name: LONG_NUMERICAL_VALUES_SEPARATED enabled: true configuration: # Maximum number of digits which are not split maxNumberLength: '5' # Maximum number of digits between separators maxBlockLength: '3' # Checks magic number - name: MAGIC_NUMBER enabled: true configuration: # Ignore numbers from test ignoreTest: "true" # Ignore numbers ignoreNumbers: "-1, 1, 0, 2, 0U, 1U, 2U, -1L, 0L, 1L, 2L, 0UL, 1UL, 2UL" # Is ignore override hashCode function ignoreHashCodeFunction: "true" # Is ignore property ignorePropertyDeclaration: "false" # Is ignore local variable ignoreLocalVariableDeclaration: "false" # Is ignore value parameter ignoreValueParameter: "true" # Is ignore constant ignoreConstantDeclaration: "true" # Is ignore property in companion object ignoreCompanionObjectPropertyDeclaration: "true" # Is ignore numbers in enum ignoreEnums: "false" # Is ignore number in ranges ignoreRanges: "false" # Is ignore number in extension function ignoreExtensionFunctions: "false" # Is ignore number in pairs created using to ignorePairsCreatedUsingTo: "false" # Checks that order of enum values or constant property inside companion is correct - name: WRONG_DECLARATIONS_ORDER enabled: true configuration: # Whether enum members should be sorted alphabetically sortEnum: true # Whether class properties should be sorted alphabetically sortProperty: true # Checks that multiple modifiers sequence is in the correct order - name: WRONG_MULTIPLE_MODIFIERS_ORDER enabled: true # Checks that identifier has appropriate name (See table of rule 1.2 part 6) - name: CONFUSING_IDENTIFIER_NAMING enabled: true # Checks year in the copyright - name: WRONG_COPYRIGHT_YEAR enabled: true # Inspection that checks if local variables are declared close to the first usage site - name: LOCAL_VARIABLE_EARLY_DECLARATION enabled: true # Try to avoid initialize val by null (e.g. val a: Int? = null -> val a: Int = 0) - name: NULLABLE_PROPERTY_TYPE enabled: true # Inspection that checks if there is a blank line before kDoc and none after - name: WRONG_NEWLINES_AROUND_KDOC enabled: true # Inspection that checks if there is no blank lines before first comment - name: FIRST_COMMENT_NO_BLANK_LINE enabled: true # Inspection that checks if there are blank lines between code and comment and between code start token and comment's text - name: COMMENT_WHITE_SPACE enabled: true configuration: maxSpacesBeforeComment: 2 maxSpacesInComment: 1 # Inspection that checks if all comment's are inside if-else code blocks. Exception is general if comment - name: IF_ELSE_COMMENTS enabled: true # Type aliases provide alternative names for existing types when type's reference text is longer 25 chars - name: TYPE_ALIAS enabled: true configuration: typeReferenceLength: '25' # max length of type reference # Checks if casting can be omitted - name: SMART_CAST_NEEDED enabled: true # Checks that variables of generic types have explicit type declaration - name: GENERIC_VARIABLE_WRONG_DECLARATION enabled: true # Inspection that checks if string template has redundant curly braces - name: STRING_TEMPLATE_CURLY_BRACES enabled: true # Variables with `val` modifier - are immutable (read-only). Usage of such variables instead of `var` variables increases # robustness and readability of code, because `var` variables can be reassigned several times in the business logic. # This rule prohibits usage of `var`s as local variables - the only exception is accumulators and counters - name: SAY_NO_TO_VAR enabled: true # Inspection that checks if string template has redundant quotes - name: STRING_TEMPLATE_QUOTES enabled: true # Check if there are redundant nested if-statements, which could be collapsed into a single one by concatenating their conditions - name: COLLAPSE_IF_STATEMENTS enabled: true configuration: startCollapseFromNestedLevel: 2 # Checks that floating-point values are not used in arithmetic expressions - name: FLOAT_IN_ACCURATE_CALCULATIONS enabled: true # Checks that function length isn't too long - name: TOO_LONG_FUNCTION enabled: true configuration: maxFunctionLength: '30' # max length of function isIncludeHeader: 'false' # count function's header # Warns if there are nested functions - name: AVOID_NESTED_FUNCTIONS enabled: true # Checks that lambda inside function parameters is in the end - name: LAMBDA_IS_NOT_LAST_PARAMETER enabled: true # Checks that function doesn't contains too many parameters - name: TOO_MANY_PARAMETERS enabled: true configuration: maxParameterListSize: '5' # max parameters size # Checks that function doesn't have too many nested blocks - name: NESTED_BLOCK enabled: true configuration: maxNestedBlockQuantity: '4' # Checks that function use default values, instead overloading - name: WRONG_OVERLOADING_FUNCTION_ARGUMENTS enabled: true # Checks that using runBlocking inside async block code - name: RUN_BLOCKING_INSIDE_ASYNC enabled: true # Checks that property in constructor doesn't contain comment - name: KDOC_NO_CONSTRUCTOR_PROPERTY enabled: true configuration: isParamTagsForParameters: true # create param tags for parameters without val or var isParamTagsForPrivateProperties: true # create param tags for private properties isParamTagsForGenericTypes: true # create param tags for generic types # Checks that the long lambda has parameters - name: TOO_MANY_LINES_IN_LAMBDA enabled: true configuration: maxLambdaLength: 10 # max length of lambda without parameters # Checks that using unnecessary, custom label - name: CUSTOM_LABEL enabled: true # Check that lambda with inner lambda doesn't use implicit parameter - name: PARAMETER_NAME_IN_OUTER_LAMBDA enabled: true # Checks that property in KDoc present in class - name: KDOC_EXTRA_PROPERTY enabled: true # There's a property in KDoc which is already present - name: KDOC_DUPLICATE_PROPERTY enabled: true # Checks that KDoc in constructor has property tag but with comment inside constructor - name: KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT enabled: true # if a class has single constructor, it should be converted to a primary constructor - name: SINGLE_CONSTRUCTOR_SHOULD_BE_PRIMARY enabled: true # Checks if class can be made as data class - name: USE_DATA_CLASS enabled: true # Checks that never use the name of a variable in the custom getter or setter - name: WRONG_NAME_OF_VARIABLE_INSIDE_ACCESSOR enabled: true # Checks that classes have only one init block - name: MULTIPLE_INIT_BLOCKS enabled: true # Checks that there are abstract functions in abstract class - name: CLASS_SHOULD_NOT_BE_ABSTRACT enabled: true # Checks if there are any trivial getters or setters - name: TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED enabled: true # Checks that no custom getters and setters are used for properties. It is a more wide rule than TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED # Kotlin compiler automatically generates `get` and `set` methods for properties and also lets the possibility to override it. # But in all cases it is very confusing when `get` and `set` are overridden for a developer who uses this particular class. # Developer expects to get the value of the property, but receives some unknown value and some extra side effect hidden by the custom getter/setter. # Use extra functions for it instead. - name: CUSTOM_GETTERS_SETTERS enabled: true # Checks if null-check was used explicitly (for example: if (a == null)) # Try to avoid explicit null checks (explicit comparison with `null`) # Kotlin is declared as [Null-safe](https://kotlinlang.org/docs/reference/null-safety.html) language. # But Kotlin architects wanted Kotlin to be fully compatible with Java, that's why `null` keyword was also introduced in Kotlin. # There are several code-structures that can be used in Kotlin to avoid null-checks. For example: `?:`, `.let {}`, `.also {}`, e.t.c - name: AVOID_NULL_CHECKS enabled: true # Checks if class instantiation can be wrapped in `apply` for better readability - name: COMPACT_OBJECT_INITIALIZATION enabled: true # Checks explicit supertype qualification - name: USELESS_SUPERTYPE enabled: true # Checks if extension function with the same signature don't have related classes - name: EXTENSION_FUNCTION_SAME_SIGNATURE enabled: true # Checks if there is empty primary constructor - name: EMPTY_PRIMARY_CONSTRUCTOR enabled: true # In case of not using field keyword in property accessors, # there should be explicit backing property with the name of real property # Example: val table get() {if (_table == null) ...} -> table should have _table - name: NO_CORRESPONDING_PROPERTY enabled: true # Checks if there is class/object that can be replace with extension function - name: AVOID_USING_UTILITY_CLASS enabled: true # If there is stateless class it is preferred to use object - name: OBJECT_IS_PREFERRED enabled: true # If there exists negated version of function you should prefer it instead of !functionCall - name: INVERSE_FUNCTION_PREFERRED enabled: true # Checks if class can be converted to inline class - name: INLINE_CLASS_CAN_BE_USED enabled: true # If file contains class, then it can't contain extension functions for the same class - name: EXTENSION_FUNCTION_WITH_CLASS enabled: true # Check if kts script contains other functions except run code - name: RUN_IN_SCRIPT enabled: true # Check if boolean expression can be simplified - name: COMPLEX_BOOLEAN_EXPRESSION enabled: true # Check if range can replace with until or `rangeTo` function with range - name: CONVENTIONAL_RANGE enabled: true configuration: isRangeToIgnore: false # Check if there is a call of print()\println() or console.log(). Assumption that it's a debug print - name: DEBUG_PRINT enabled: true # Check that typealias name is in PascalCase - name: TYPEALIAS_NAME_INCORRECT_CASE enabled: true # Should change property length - 1 to property lastIndex - name: USE_LAST_INDEX enabled: true # Only properties from the primary constructor should be documented in a @property tag in class KDoc - name: KDOC_NO_CLASS_BODY_PROPERTIES_IN_HEADER enabled: true ================================================ FILE: examples/gradle-kotlin-dsl/settings.gradle.kts ================================================ ================================================ FILE: examples/gradle-kotlin-dsl/src/main/kotlin/AnotherTest.kt ================================================ package whate.ver fun String.createPluginConfig() { val pluginConfig = TomlDecoder.decode( serializer(), fakeFileNode, DecoderConf() ) pluginConfig.configLocation = this.toPath() pluginConfig.prop1 = property1 // comment1 pluginConfig.configLocation2 = this.toPath() // comment2 pluginConfig.prop2 = property2 } ================================================ FILE: examples/gradle-kotlin-dsl/src/main/kotlin/Test.kt ================================================ package incorrect class incorrectname: Exception() { fun INCORRECT_FUNCTION() { throw Exception() } // fun myCommentedFunction() { // } val Incorrect_Val = 5 } ================================================ FILE: examples/gradle-kotlin-dsl-multiproject/backend/build.gradle.kts ================================================ plugins { kotlin("jvm") } ================================================ FILE: examples/gradle-kotlin-dsl-multiproject/backend/src/main/kotlin/Test.kt ================================================ package incorrect class incorrectname: Exception() { fun INCORRECT_FUNCTION() { throw Exception() } // fun myCommentedFunction() { // } val Incorrect_Val = 5 } ================================================ FILE: examples/gradle-kotlin-dsl-multiproject/build.gradle.kts ================================================ import com.saveourtool.diktat.plugin.gradle.DiktatExtension import com.saveourtool.diktat.plugin.gradle.DiktatGradlePlugin plugins { kotlin("jvm") version "2.1.0" id("com.saveourtool.diktat") version "2.0.0" apply false } allprojects { repositories { mavenLocal() mavenCentral() } apply() configure { diktatConfigFile = rootProject.file("diktat-analysis.yml") inputs { include("src/**/*.kt") } debug = true } } ================================================ FILE: examples/gradle-kotlin-dsl-multiproject/diktat-analysis.yml ================================================ # Common configuration - name: DIKTAT_COMMON configuration: # put your package name here - it will be autofixed and checked domainName: your.name.here testDirs: test # expected values: disabledChapters: "Naming, Comments, General, Variables, Functions, Classes" # or: "1, 2, 3, 4, 5, 6" disabledChapters: "" kotlinVersion: 2.1 srcDirectories: "main" # Checks that the Class/Enum/Interface name matches Pascal case - name: CLASS_NAME_INCORRECT enabled: true # all code blocks with MyAnnotation will be ignored and not checked ignoreAnnotated: [ MyAnnotation ] # Checks that CONSTANT (treated as const val from companion object or class level) is in non UPPER_SNAKE_CASE - name: CONSTANT_UPPERCASE enabled: true # Checks that enum value is in upper SNAKE_CASE or in PascalCase depending on the config. UPPER_SNAKE_CASE is the default, but can be changed by 'enumStyle' config - name: ENUM_VALUE enabled: true configuration: # Two options: SNAKE_CASE (default), PascalCase enumStyle: SNAKE_CASE # Checks that class which extends any Exception class has Exception suffix - name: EXCEPTION_SUFFIX enabled: true # Checks that file name has extension - name: FILE_NAME_INCORRECT enabled: true # Checks that file name matches class name, if it is only one class in file - name: FILE_NAME_MATCH_CLASS enabled: true # Checks that functions/methods which return boolean have special prefix like "is/should/e.t.c" - name: FUNCTION_BOOLEAN_PREFIX enabled: true configuration: allowedPrefixes: "" # A list of functions that return boolean and are allowed to use. Input is in a form "foo, bar". # Checks that function/method name is in lowerCamelCase - name: FUNCTION_NAME_INCORRECT_CASE enabled: true # Checks that typealias name is in PascalCase - name: TYPEALIAS_NAME_INCORRECT_CASE enabled: true # Checks that generic name doesn't contain more than 1 letter (capital). It can be followed by numbers, example: T12, T - name: GENERIC_NAME enabled: true # Identifier length should be in range [2,64] except names that used in industry like {i, j} and 'e' for catching exceptions - name: IDENTIFIER_LENGTH enabled: true # Checks that the object matches PascalCase - name: OBJECT_NAME_INCORRECT enabled: true # Checks that package name is in correct (lower) case - name: PACKAGE_NAME_INCORRECT_CASE enabled: true # Checks that package name starts with the company's domain - name: PACKAGE_NAME_INCORRECT_PREFIX enabled: false # Checks that package name does not have incorrect symbols like underscore or non-ASCII letters/digits - name: PACKAGE_NAME_INCORRECT_SYMBOLS enabled: true # Checks that the path for a file matches with a package name - name: PACKAGE_NAME_INCORRECT_PATH enabled: false # Checks that package name is in the file - name: PACKAGE_NAME_MISSING enabled: true # Checks that variable does not have prefix (like mVariable or M_VARIABLE) - name: VARIABLE_HAS_PREFIX enabled: true # Checks that variable does not contain one single letter, only exceptions are fixed names that used in industry like {i, j} - name: VARIABLE_NAME_INCORRECT enabled: true # Checks that the name of variable is in lowerCamelCase and contains only ASCII letters - name: VARIABLE_NAME_INCORRECT_FORMAT enabled: true # Checks that functions have kdoc - name: MISSING_KDOC_ON_FUNCTION enabled: true # Checks that on file level internal or public class or function has missing KDoc - name: MISSING_KDOC_TOP_LEVEL enabled: true # Checks that accessible internal elements (protected, public, internal) in a class are documented - name: MISSING_KDOC_CLASS_ELEMENTS enabled: true # Checks that accessible method parameters are documented in KDoc - name: KDOC_WITHOUT_PARAM_TAG enabled: true # Checks that accessible method explicit return type is documented in KDoc - name: KDOC_WITHOUT_RETURN_TAG enabled: true # Checks that accessible method throw keyword is documented in KDoc - name: KDOC_WITHOUT_THROWS_TAG enabled: true # Checks that KDoc is not empty - name: KDOC_EMPTY_KDOC enabled: true # Checks that underscore is correctly used to split package naming - name: INCORRECT_PACKAGE_SEPARATOR enabled: true # Checks that code block doesn't contain kdoc comments - name: COMMENTED_BY_KDOC enabled: true # Checks that there is no @deprecated tag in kdoc - name: KDOC_NO_DEPRECATED_TAG enabled: true # Checks that there is no empty content in kdoc tags - name: KDOC_NO_EMPTY_TAGS enabled: true # Checks that there is only one space after kdoc tag - name: KDOC_WRONG_SPACES_AFTER_TAG enabled: true # Checks tags order in kDoc. `@param`, `@return`, `@throws` - name: KDOC_WRONG_TAGS_ORDER enabled: true # Checks that there is no newline of empty KDoc line (with leading asterisk) between `@param`, `@return`, `@throws` tags - name: KDOC_NO_NEWLINES_BETWEEN_BASIC_TAGS enabled: true # Checks that block of tags @param, @return, @throws is separated from previous part of KDoc by exactly one empty line - name: KDOC_NEWLINES_BEFORE_BASIC_TAGS enabled: true # Checks that special tags `@apiNote`, `@implNote`, `@implSpec` have exactly one empty line after - name: KDOC_NO_NEWLINE_AFTER_SPECIAL_TAGS enabled: true # Checks that kdoc does not contain @author tag or date - name: KDOC_CONTAINS_DATE_OR_AUTHOR enabled: true configuration: versionRegex: \d+\.\d+\.\d+[-.\w\d]* # Checks that KDoc does not contain single line with words 'return', 'get' or 'set' - name: KDOC_TRIVIAL_KDOC_ON_FUNCTION enabled: true # Checks that there is newline after header KDoc - name: HEADER_WRONG_FORMAT enabled: true # Checks that file with zero or >1 classes has header KDoc - name: HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE enabled: true # Checks that copyright exists on top of file and is properly formatted (as a block comment) - name: HEADER_MISSING_OR_WRONG_COPYRIGHT enabled: true configuration: isCopyrightMandatory: false copyrightText: 'Copyright (c) Your Company Name Here. 2010-;@currYear;' # Checks that header kdoc is located before package directive - name: HEADER_NOT_BEFORE_PACKAGE enabled: true # Checks that file does not contain lines > maxSize - name: FILE_IS_TOO_LONG enabled: true configuration: # number of lines maxSize: '2000' # Checks that file does not contain commented out code - name: COMMENTED_OUT_CODE enabled: true # Checks that file does not contain only comments, imports and package directive - name: FILE_CONTAINS_ONLY_COMMENTS enabled: true # Orders imports alphabetically - name: FILE_UNORDERED_IMPORTS enabled: true configuration: # use logical imports grouping with sorting inside of a group useRecommendedImportsOrder: true # Checks that general order of code parts is right - name: FILE_INCORRECT_BLOCKS_ORDER enabled: true # Checks that there is exactly one line between code blocks - name: FILE_NO_BLANK_LINE_BETWEEN_BLOCKS enabled: true # Checks that there is no wildcard imports. Exception: allowedWildcards - name: FILE_WILDCARD_IMPORTS enabled: true configuration: allowedWildcards: "" # Allowed wildcards for imports (e.g. "import com.saveourtool.diktat.*, import org.jetbrains.kotlin.*") useRecommendedImportsOrder: true # Checks unused imports - name: UNUSED_IMPORT enabled: true configuration: deleteUnusedImport: true # Checks that braces are used in if, else, when, for, do, and while statements. Exception: single line ternary operator statement - name: NO_BRACES_IN_CONDITIONALS_AND_LOOPS enabled: true # Checks that the declaration part of a class-like code structures (class/interface/etc.) is in the proper order - name: WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES enabled: true # Checks that properties with comments are separated by a blank line - name: BLANK_LINE_BETWEEN_PROPERTIES enabled: true # Checks top level order - name: TOP_LEVEL_ORDER enabled: true # Checks that non-empty code blocks with braces follow the K&R style (1TBS or OTBS style) - name: BRACES_BLOCK_STRUCTURE_ERROR enabled: true configuration: openBraceNewline: 'True' closeBraceNewline: 'True' # Checks that indentation is correct - name: WRONG_INDENTATION enabled: true configuration: # Is newline at the end of a file needed newlineAtEnd: true # If true: in parameter list when parameters are split by newline they are indented with two indentations instead of one extendedIndentOfParameters: false # If true: if first parameter in parameter list is on the same line as opening parenthesis, then other parameters can be aligned with it alignedParameters: true # If true, expression bodies which begin on a separate line are indented # using a continuation indent. The default is false. # # This flag is called CONTINUATION_INDENT_FOR_EXPRESSION_BODIES in IDEA and # ij_kotlin_continuation_indent_for_expression_bodies in .editorconfig. extendedIndentForExpressionBodies: false # If true: if expression is split by newline after operator like +/-/`*`, then the next line is indented with two indentations instead of one extendedIndentAfterOperators: true # If true: when dot qualified expression starts on a new line, this line will be indented with two indentations instead of one extendedIndentBeforeDot: false # The indentation size for each file indentationSize: 4 # Checks that there is no empty blocks in a file. # If allowEmptyBlocks is true, checks that it follows correct style (have a newline) - name: EMPTY_BLOCK_STRUCTURE_ERROR enabled: true configuration: # Whether a newline after `{` is required in an empty block styleEmptyBlockWithNewline: 'True' allowEmptyBlocks: 'False' # Checks that there is no more than one statement per line - name: MORE_THAN_ONE_STATEMENT_PER_LINE enabled: true # Checks that the line length is < lineLength parameter - name: LONG_LINE enabled: true configuration: lineLength: '120' # Checks that semicolons are not used at the end of a line - name: REDUNDANT_SEMICOLON enabled: true # Checks that line breaks follow code style guide: rule 3.6 - name: WRONG_NEWLINES enabled: true configuration: # If the number of parameters on one line is more than this threshold, all parameters will be placed on separate lines. maxParametersInOneLine: 2 # 3 by default. maxCallsInOneLine: 3 # Checks trailing comma - name: TRAILING_COMMA enabled: true configuration: # VALUE_ARGUMENT valueArgument: false # VALUE_PARAMETER valueParameter: false # REFERENCE_EXPRESSION indices: false # WHEN_CONDITION_WITH_EXPRESSION whenConditions: false # STRING_TEMPLATE collectionLiteral: false # TYPE_PROJECTION typeArgument: false # TYPE_PARAMETER typeParameter: false # DESTRUCTURING_DECLARATION_ENTRY destructuringDeclaration: false # Checks that there are not too many consecutive spaces in line - name: TOO_MANY_CONSECUTIVE_SPACES enabled: true configuration: # Maximum allowed number of consecutive spaces (not counting indentation) maxSpaces: '1' # Whether formatting for enums should be kept without checking saveInitialFormattingForEnums: false # Inspection that checks if a long dot qualified expression is used in condition or as an argument - name: COMPLEX_EXPRESSION enabled: true # Checks that blank lines are used correctly. # For example: triggers when there are too many blank lines between function declaration - name: TOO_MANY_BLANK_LINES enabled: true # Checks that usage of horizontal spaces doesn't violate code style guide - name: WRONG_WHITESPACE enabled: true # Checks that backticks (``) are not used in the identifier name, except the case when it is test method (marked with @Test annotation) - name: BACKTICKS_PROHIBITED enabled: true # Checks that a single line concatenation of strings is not used - name: STRING_CONCATENATION enabled: true # Checks that each when statement have else in the end - name: WHEN_WITHOUT_ELSE enabled: true # Checks that annotation is on a single line - name: ANNOTATION_NEW_LINE enabled: true # Checks that method annotated with `Preview` annotation is private and has Preview suffix - name: PREVIEW_ANNOTATION enabled: true # Checks that enum structure is correct: enum entries should be separated by comma and line break and last entry should have semicolon in the end. - name: ENUMS_SEPARATED enabled: true # Checks that value on integer or float constant is not too big - name: LONG_NUMERICAL_VALUES_SEPARATED enabled: true configuration: # Maximum number of digits which are not split maxNumberLength: '5' # Maximum number of digits between separators maxBlockLength: '3' # Checks magic number - name: MAGIC_NUMBER enabled: true configuration: # Ignore numbers from test ignoreTest: "true" # Ignore numbers ignoreNumbers: "-1, 1, 0, 2, 0U, 1U, 2U, -1L, 0L, 1L, 2L, 0UL, 1UL, 2UL" # Is ignore override hashCode function ignoreHashCodeFunction: "true" # Is ignore property ignorePropertyDeclaration: "false" # Is ignore local variable ignoreLocalVariableDeclaration: "false" # Is ignore value parameter ignoreValueParameter: "true" # Is ignore constant ignoreConstantDeclaration: "true" # Is ignore property in companion object ignoreCompanionObjectPropertyDeclaration: "true" # Is ignore numbers in enum ignoreEnums: "false" # Is ignore number in ranges ignoreRanges: "false" # Is ignore number in extension function ignoreExtensionFunctions: "false" # Is ignore number in pairs created using to ignorePairsCreatedUsingTo: "false" # Checks that order of enum values or constant property inside companion is correct - name: WRONG_DECLARATIONS_ORDER enabled: true configuration: # Whether enum members should be sorted alphabetically sortEnum: true # Whether class properties should be sorted alphabetically sortProperty: true # Checks that multiple modifiers sequence is in the correct order - name: WRONG_MULTIPLE_MODIFIERS_ORDER enabled: true # Checks that identifier has appropriate name (See table of rule 1.2 part 6) - name: CONFUSING_IDENTIFIER_NAMING enabled: true # Checks year in the copyright - name: WRONG_COPYRIGHT_YEAR enabled: true # Inspection that checks if local variables are declared close to the first usage site - name: LOCAL_VARIABLE_EARLY_DECLARATION enabled: true # Try to avoid initialize val by null (e.g. val a: Int? = null -> val a: Int = 0) - name: NULLABLE_PROPERTY_TYPE enabled: true # Inspection that checks if there is a blank line before kDoc and none after - name: WRONG_NEWLINES_AROUND_KDOC enabled: true # Inspection that checks if there is no blank lines before first comment - name: FIRST_COMMENT_NO_BLANK_LINE enabled: true # Inspection that checks if there are blank lines between code and comment and between code start token and comment's text - name: COMMENT_WHITE_SPACE enabled: true configuration: maxSpacesBeforeComment: 2 maxSpacesInComment: 1 # Inspection that checks if all comment's are inside if-else code blocks. Exception is general if comment - name: IF_ELSE_COMMENTS enabled: true # Type aliases provide alternative names for existing types when type's reference text is longer 25 chars - name: TYPE_ALIAS enabled: true configuration: typeReferenceLength: '25' # max length of type reference # Checks if casting can be omitted - name: SMART_CAST_NEEDED enabled: true # Checks that variables of generic types have explicit type declaration - name: GENERIC_VARIABLE_WRONG_DECLARATION enabled: true # Inspection that checks if string template has redundant curly braces - name: STRING_TEMPLATE_CURLY_BRACES enabled: true # Variables with `val` modifier - are immutable (read-only). Usage of such variables instead of `var` variables increases # robustness and readability of code, because `var` variables can be reassigned several times in the business logic. # This rule prohibits usage of `var`s as local variables - the only exception is accumulators and counters - name: SAY_NO_TO_VAR enabled: true # Inspection that checks if string template has redundant quotes - name: STRING_TEMPLATE_QUOTES enabled: true # Check if there are redundant nested if-statements, which could be collapsed into a single one by concatenating their conditions - name: COLLAPSE_IF_STATEMENTS enabled: true configuration: startCollapseFromNestedLevel: 2 # Checks that floating-point values are not used in arithmetic expressions - name: FLOAT_IN_ACCURATE_CALCULATIONS enabled: true # Checks that function length isn't too long - name: TOO_LONG_FUNCTION enabled: true configuration: maxFunctionLength: '30' # max length of function isIncludeHeader: 'false' # count function's header # Warns if there are nested functions - name: AVOID_NESTED_FUNCTIONS enabled: true # Checks that lambda inside function parameters is in the end - name: LAMBDA_IS_NOT_LAST_PARAMETER enabled: true # Checks that function doesn't contains too many parameters - name: TOO_MANY_PARAMETERS enabled: true configuration: maxParameterListSize: '5' # max parameters size # Checks that function doesn't have too many nested blocks - name: NESTED_BLOCK enabled: true configuration: maxNestedBlockQuantity: '4' # Checks that function use default values, instead overloading - name: WRONG_OVERLOADING_FUNCTION_ARGUMENTS enabled: true # Checks that using runBlocking inside async block code - name: RUN_BLOCKING_INSIDE_ASYNC enabled: true # Checks that property in constructor doesn't contain comment - name: KDOC_NO_CONSTRUCTOR_PROPERTY enabled: true configuration: isParamTagsForParameters: true # create param tags for parameters without val or var isParamTagsForPrivateProperties: true # create param tags for private properties isParamTagsForGenericTypes: true # create param tags for generic types # Checks that the long lambda has parameters - name: TOO_MANY_LINES_IN_LAMBDA enabled: true configuration: maxLambdaLength: 10 # max length of lambda without parameters # Checks that using unnecessary, custom label - name: CUSTOM_LABEL enabled: true # Check that lambda with inner lambda doesn't use implicit parameter - name: PARAMETER_NAME_IN_OUTER_LAMBDA enabled: true # Checks that property in KDoc present in class - name: KDOC_EXTRA_PROPERTY enabled: true # There's a property in KDoc which is already present - name: KDOC_DUPLICATE_PROPERTY enabled: true # Checks that KDoc in constructor has property tag but with comment inside constructor - name: KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT enabled: true # if a class has single constructor, it should be converted to a primary constructor - name: SINGLE_CONSTRUCTOR_SHOULD_BE_PRIMARY enabled: true # Checks if class can be made as data class - name: USE_DATA_CLASS enabled: true # Checks that never use the name of a variable in the custom getter or setter - name: WRONG_NAME_OF_VARIABLE_INSIDE_ACCESSOR enabled: true # Checks that classes have only one init block - name: MULTIPLE_INIT_BLOCKS enabled: true # Checks that there are abstract functions in abstract class - name: CLASS_SHOULD_NOT_BE_ABSTRACT enabled: true # Checks if there are any trivial getters or setters - name: TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED enabled: true # Checks that no custom getters and setters are used for properties. It is a more wide rule than TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED # Kotlin compiler automatically generates `get` and `set` methods for properties and also lets the possibility to override it. # But in all cases it is very confusing when `get` and `set` are overridden for a developer who uses this particular class. # Developer expects to get the value of the property, but receives some unknown value and some extra side effect hidden by the custom getter/setter. # Use extra functions for it instead. - name: CUSTOM_GETTERS_SETTERS enabled: true # Checks if null-check was used explicitly (for example: if (a == null)) # Try to avoid explicit null checks (explicit comparison with `null`) # Kotlin is declared as [Null-safe](https://kotlinlang.org/docs/reference/null-safety.html) language. # But Kotlin architects wanted Kotlin to be fully compatible with Java, that's why `null` keyword was also introduced in Kotlin. # There are several code-structures that can be used in Kotlin to avoid null-checks. For example: `?:`, `.let {}`, `.also {}`, e.t.c - name: AVOID_NULL_CHECKS enabled: true # Checks if class instantiation can be wrapped in `apply` for better readability - name: COMPACT_OBJECT_INITIALIZATION enabled: true # Checks explicit supertype qualification - name: USELESS_SUPERTYPE enabled: true # Checks if extension function with the same signature don't have related classes - name: EXTENSION_FUNCTION_SAME_SIGNATURE enabled: true # Checks if there is empty primary constructor - name: EMPTY_PRIMARY_CONSTRUCTOR enabled: true # In case of not using field keyword in property accessors, # there should be explicit backing property with the name of real property # Example: val table get() {if (_table == null) ...} -> table should have _table - name: NO_CORRESPONDING_PROPERTY enabled: true # Checks if there is class/object that can be replace with extension function - name: AVOID_USING_UTILITY_CLASS enabled: true # If there is stateless class it is preferred to use object - name: OBJECT_IS_PREFERRED enabled: true # If there exists negated version of function you should prefer it instead of !functionCall - name: INVERSE_FUNCTION_PREFERRED enabled: true # Checks if class can be converted to inline class - name: INLINE_CLASS_CAN_BE_USED enabled: true # If file contains class, then it can't contain extension functions for the same class - name: EXTENSION_FUNCTION_WITH_CLASS enabled: true # Check if kts script contains other functions except run code - name: RUN_IN_SCRIPT enabled: true # Check if boolean expression can be simplified - name: COMPLEX_BOOLEAN_EXPRESSION enabled: true # Check if range can replace with until or `rangeTo` function with range - name: CONVENTIONAL_RANGE enabled: true configuration: isRangeToIgnore: false # Check if there is a call of print()\println() or console.log(). Assumption that it's a debug print - name: DEBUG_PRINT enabled: true # Check that typealias name is in PascalCase - name: TYPEALIAS_NAME_INCORRECT_CASE enabled: true # Should change property length - 1 to property lastIndex - name: USE_LAST_INDEX enabled: true # Only properties from the primary constructor should be documented in a @property tag in class KDoc - name: KDOC_NO_CLASS_BODY_PROPERTIES_IN_HEADER enabled: true ================================================ FILE: examples/gradle-kotlin-dsl-multiproject/frontend/build.gradle.kts ================================================ plugins { kotlin("jvm") } ================================================ FILE: examples/gradle-kotlin-dsl-multiproject/frontend/src/main/kotlin/AnotherTest.kt ================================================ package whate.ver fun String.createPluginConfig() { val pluginConfig = TomlDecoder.decode( serializer(), fakeFileNode, DecoderConf() ) pluginConfig.configLocation = this.toPath() pluginConfig.prop1 = property1 // comment1 pluginConfig.configLocation2 = this.toPath() // comment2 pluginConfig.prop2 = property2 } ================================================ FILE: examples/gradle-kotlin-dsl-multiproject/settings.gradle.kts ================================================ include(":backend") include(":frontend") ================================================ FILE: examples/maven/diktat-analysis.yml ================================================ # Common configuration - name: DIKTAT_COMMON configuration: # put your package name here - it will be autofixed and checked domainName: your.name.here testDirs: test # expected values: disabledChapters: "Naming, Comments, General, Variables, Functions, Classes" # or: "1, 2, 3, 4, 5, 6" disabledChapters: "" kotlinVersion: 2.1 srcDirectories: "main" # Checks that the Class/Enum/Interface name matches Pascal case - name: CLASS_NAME_INCORRECT enabled: true # all code blocks with MyAnnotation will be ignored and not checked ignoreAnnotated: [ MyAnnotation ] # Checks that CONSTANT (treated as const val from companion object or class level) is in non UPPER_SNAKE_CASE - name: CONSTANT_UPPERCASE enabled: true # Checks that enum value is in upper SNAKE_CASE or in PascalCase depending on the config. UPPER_SNAKE_CASE is the default, but can be changed by 'enumStyle' config - name: ENUM_VALUE enabled: true configuration: # Two options: SNAKE_CASE (default), PascalCase enumStyle: SNAKE_CASE # Checks that class which extends any Exception class has Exception suffix - name: EXCEPTION_SUFFIX enabled: true # Checks that file name has extension - name: FILE_NAME_INCORRECT enabled: true # Checks that file name matches class name, if it is only one class in file - name: FILE_NAME_MATCH_CLASS enabled: true # Checks that functions/methods which return boolean have special prefix like "is/should/e.t.c" - name: FUNCTION_BOOLEAN_PREFIX enabled: true configuration: allowedPrefixes: "" # A list of functions that return boolean and are allowed to use. Input is in a form "foo, bar". # Checks that function/method name is in lowerCamelCase - name: FUNCTION_NAME_INCORRECT_CASE enabled: true # Checks that typealias name is in PascalCase - name: TYPEALIAS_NAME_INCORRECT_CASE enabled: true # Checks that generic name doesn't contain more than 1 letter (capital). It can be followed by numbers, example: T12, T - name: GENERIC_NAME enabled: true # Identifier length should be in range [2,64] except names that used in industry like {i, j} and 'e' for catching exceptions - name: IDENTIFIER_LENGTH enabled: true # Checks that the object matches PascalCase - name: OBJECT_NAME_INCORRECT enabled: true # Checks that package name is in correct (lower) case - name: PACKAGE_NAME_INCORRECT_CASE enabled: true # Checks that package name starts with the company's domain - name: PACKAGE_NAME_INCORRECT_PREFIX enabled: false # Checks that package name does not have incorrect symbols like underscore or non-ASCII letters/digits - name: PACKAGE_NAME_INCORRECT_SYMBOLS enabled: true # Checks that the path for a file matches with a package name - name: PACKAGE_NAME_INCORRECT_PATH enabled: false # Checks that package name is in the file - name: PACKAGE_NAME_MISSING enabled: true # Checks that variable does not have prefix (like mVariable or M_VARIABLE) - name: VARIABLE_HAS_PREFIX enabled: true # Checks that variable does not contain one single letter, only exceptions are fixed names that used in industry like {i, j} - name: VARIABLE_NAME_INCORRECT enabled: true # Checks that the name of variable is in lowerCamelCase and contains only ASCII letters - name: VARIABLE_NAME_INCORRECT_FORMAT enabled: true # Checks that functions have kdoc - name: MISSING_KDOC_ON_FUNCTION enabled: true # Checks that on file level internal or public class or function has missing KDoc - name: MISSING_KDOC_TOP_LEVEL enabled: true # Checks that accessible internal elements (protected, public, internal) in a class are documented - name: MISSING_KDOC_CLASS_ELEMENTS enabled: true # Checks that accessible method parameters are documented in KDoc - name: KDOC_WITHOUT_PARAM_TAG enabled: true # Checks that accessible method explicit return type is documented in KDoc - name: KDOC_WITHOUT_RETURN_TAG enabled: true # Checks that accessible method throw keyword is documented in KDoc - name: KDOC_WITHOUT_THROWS_TAG enabled: true # Checks that KDoc is not empty - name: KDOC_EMPTY_KDOC enabled: true # Checks that underscore is correctly used to split package naming - name: INCORRECT_PACKAGE_SEPARATOR enabled: true # Checks that code block doesn't contain kdoc comments - name: COMMENTED_BY_KDOC enabled: true # Checks that there is no @deprecated tag in kdoc - name: KDOC_NO_DEPRECATED_TAG enabled: true # Checks that there is no empty content in kdoc tags - name: KDOC_NO_EMPTY_TAGS enabled: true # Checks that there is only one space after kdoc tag - name: KDOC_WRONG_SPACES_AFTER_TAG enabled: true # Checks tags order in kDoc. `@param`, `@return`, `@throws` - name: KDOC_WRONG_TAGS_ORDER enabled: true # Checks that there is no newline of empty KDoc line (with leading asterisk) between `@param`, `@return`, `@throws` tags - name: KDOC_NO_NEWLINES_BETWEEN_BASIC_TAGS enabled: true # Checks that block of tags @param, @return, @throws is separated from previous part of KDoc by exactly one empty line - name: KDOC_NEWLINES_BEFORE_BASIC_TAGS enabled: true # Checks that special tags `@apiNote`, `@implNote`, `@implSpec` have exactly one empty line after - name: KDOC_NO_NEWLINE_AFTER_SPECIAL_TAGS enabled: true # Checks that kdoc does not contain @author tag or date - name: KDOC_CONTAINS_DATE_OR_AUTHOR enabled: true configuration: versionRegex: \d+\.\d+\.\d+[-.\w\d]* # Checks that KDoc does not contain single line with words 'return', 'get' or 'set' - name: KDOC_TRIVIAL_KDOC_ON_FUNCTION enabled: true # Checks that there is newline after header KDoc - name: HEADER_WRONG_FORMAT enabled: true # Checks that file with zero or >1 classes has header KDoc - name: HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE enabled: true # Checks that copyright exists on top of file and is properly formatted (as a block comment) - name: HEADER_MISSING_OR_WRONG_COPYRIGHT enabled: true configuration: isCopyrightMandatory: false copyrightText: 'Copyright (c) Your Company Name Here. 2010-;@currYear;' # Checks that header kdoc is located before package directive - name: HEADER_NOT_BEFORE_PACKAGE enabled: true # Checks that file does not contain lines > maxSize - name: FILE_IS_TOO_LONG enabled: true configuration: # number of lines maxSize: '2000' # Checks that file does not contain commented out code - name: COMMENTED_OUT_CODE enabled: true # Checks that file does not contain only comments, imports and package directive - name: FILE_CONTAINS_ONLY_COMMENTS enabled: true # Orders imports alphabetically - name: FILE_UNORDERED_IMPORTS enabled: true configuration: # use logical imports grouping with sorting inside of a group useRecommendedImportsOrder: true # Checks that general order of code parts is right - name: FILE_INCORRECT_BLOCKS_ORDER enabled: true # Checks that there is exactly one line between code blocks - name: FILE_NO_BLANK_LINE_BETWEEN_BLOCKS enabled: true # Checks that there is no wildcard imports. Exception: allowedWildcards - name: FILE_WILDCARD_IMPORTS enabled: true configuration: allowedWildcards: "" # Allowed wildcards for imports (e.g. "import com.saveourtool.diktat.*, import org.jetbrains.kotlin.*") useRecommendedImportsOrder: true # Checks unused imports - name: UNUSED_IMPORT enabled: true configuration: deleteUnusedImport: true # Checks that braces are used in if, else, when, for, do, and while statements. Exception: single line ternary operator statement - name: NO_BRACES_IN_CONDITIONALS_AND_LOOPS enabled: true # Checks that the declaration part of a class-like code structures (class/interface/etc.) is in the proper order - name: WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES enabled: true # Checks that properties with comments are separated by a blank line - name: BLANK_LINE_BETWEEN_PROPERTIES enabled: true # Checks top level order - name: TOP_LEVEL_ORDER enabled: true # Checks that non-empty code blocks with braces follow the K&R style (1TBS or OTBS style) - name: BRACES_BLOCK_STRUCTURE_ERROR enabled: true configuration: openBraceNewline: 'True' closeBraceNewline: 'True' # Checks that indentation is correct - name: WRONG_INDENTATION enabled: true configuration: # Is newline at the end of a file needed newlineAtEnd: true # If true: in parameter list when parameters are split by newline they are indented with two indentations instead of one extendedIndentOfParameters: false # If true: if first parameter in parameter list is on the same line as opening parenthesis, then other parameters can be aligned with it alignedParameters: true # If true, expression bodies which begin on a separate line are indented # using a continuation indent. The default is false. # # This flag is called CONTINUATION_INDENT_FOR_EXPRESSION_BODIES in IDEA and # ij_kotlin_continuation_indent_for_expression_bodies in .editorconfig. extendedIndentForExpressionBodies: false # If true: if expression is split by newline after operator like +/-/`*`, then the next line is indented with two indentations instead of one extendedIndentAfterOperators: true # If true: when dot qualified expression starts on a new line, this line will be indented with two indentations instead of one extendedIndentBeforeDot: false # The indentation size for each file indentationSize: 4 # Checks that there is no empty blocks in a file. # If allowEmptyBlocks is true, checks that it follows correct style (have a newline) - name: EMPTY_BLOCK_STRUCTURE_ERROR enabled: true configuration: # Whether a newline after `{` is required in an empty block styleEmptyBlockWithNewline: 'True' allowEmptyBlocks: 'False' # Checks that there is no more than one statement per line - name: MORE_THAN_ONE_STATEMENT_PER_LINE enabled: true # Checks that the line length is < lineLength parameter - name: LONG_LINE enabled: true configuration: lineLength: '120' # Checks that semicolons are not used at the end of a line - name: REDUNDANT_SEMICOLON enabled: true # Checks that line breaks follow code style guide: rule 3.6 - name: WRONG_NEWLINES enabled: true configuration: # If the number of parameters on one line is more than this threshold, all parameters will be placed on separate lines. maxParametersInOneLine: 2 # 3 by default. maxCallsInOneLine: 3 # Checks trailing comma - name: TRAILING_COMMA enabled: true configuration: # VALUE_ARGUMENT valueArgument: false # VALUE_PARAMETER valueParameter: false # REFERENCE_EXPRESSION indices: false # WHEN_CONDITION_WITH_EXPRESSION whenConditions: false # STRING_TEMPLATE collectionLiteral: false # TYPE_PROJECTION typeArgument: false # TYPE_PARAMETER typeParameter: false # DESTRUCTURING_DECLARATION_ENTRY destructuringDeclaration: false # Checks that there are not too many consecutive spaces in line - name: TOO_MANY_CONSECUTIVE_SPACES enabled: true configuration: # Maximum allowed number of consecutive spaces (not counting indentation) maxSpaces: '1' # Whether formatting for enums should be kept without checking saveInitialFormattingForEnums: false # Inspection that checks if a long dot qualified expression is used in condition or as an argument - name: COMPLEX_EXPRESSION enabled: true # Checks that blank lines are used correctly. # For example: triggers when there are too many blank lines between function declaration - name: TOO_MANY_BLANK_LINES enabled: true # Checks that usage of horizontal spaces doesn't violate code style guide - name: WRONG_WHITESPACE enabled: true # Checks that backticks (``) are not used in the identifier name, except the case when it is test method (marked with @Test annotation) - name: BACKTICKS_PROHIBITED enabled: true # Checks that a single line concatenation of strings is not used - name: STRING_CONCATENATION enabled: true # Checks that each when statement have else in the end - name: WHEN_WITHOUT_ELSE enabled: true # Checks that annotation is on a single line - name: ANNOTATION_NEW_LINE enabled: true # Checks that method annotated with `Preview` annotation is private and has Preview suffix - name: PREVIEW_ANNOTATION enabled: true # Checks that enum structure is correct: enum entries should be separated by comma and line break and last entry should have semicolon in the end. - name: ENUMS_SEPARATED enabled: true # Checks that value on integer or float constant is not too big - name: LONG_NUMERICAL_VALUES_SEPARATED enabled: true configuration: # Maximum number of digits which are not split maxNumberLength: '5' # Maximum number of digits between separators maxBlockLength: '3' # Checks magic number - name: MAGIC_NUMBER enabled: true configuration: # Ignore numbers from test ignoreTest: "true" # Ignore numbers ignoreNumbers: "-1, 1, 0, 2, 0U, 1U, 2U, -1L, 0L, 1L, 2L, 0UL, 1UL, 2UL" # Is ignore override hashCode function ignoreHashCodeFunction: "true" # Is ignore property ignorePropertyDeclaration: "false" # Is ignore local variable ignoreLocalVariableDeclaration: "false" # Is ignore value parameter ignoreValueParameter: "true" # Is ignore constant ignoreConstantDeclaration: "true" # Is ignore property in companion object ignoreCompanionObjectPropertyDeclaration: "true" # Is ignore numbers in enum ignoreEnums: "false" # Is ignore number in ranges ignoreRanges: "false" # Is ignore number in extension function ignoreExtensionFunctions: "false" # Is ignore number in pairs created using to ignorePairsCreatedUsingTo: "false" # Checks that order of enum values or constant property inside companion is correct - name: WRONG_DECLARATIONS_ORDER enabled: true configuration: # Whether enum members should be sorted alphabetically sortEnum: true # Whether class properties should be sorted alphabetically sortProperty: true # Checks that multiple modifiers sequence is in the correct order - name: WRONG_MULTIPLE_MODIFIERS_ORDER enabled: true # Checks that identifier has appropriate name (See table of rule 1.2 part 6) - name: CONFUSING_IDENTIFIER_NAMING enabled: true # Checks year in the copyright - name: WRONG_COPYRIGHT_YEAR enabled: true # Inspection that checks if local variables are declared close to the first usage site - name: LOCAL_VARIABLE_EARLY_DECLARATION enabled: true # Try to avoid initialize val by null (e.g. val a: Int? = null -> val a: Int = 0) - name: NULLABLE_PROPERTY_TYPE enabled: true # Inspection that checks if there is a blank line before kDoc and none after - name: WRONG_NEWLINES_AROUND_KDOC enabled: true # Inspection that checks if there is no blank lines before first comment - name: FIRST_COMMENT_NO_BLANK_LINE enabled: true # Inspection that checks if there are blank lines between code and comment and between code start token and comment's text - name: COMMENT_WHITE_SPACE enabled: true configuration: maxSpacesBeforeComment: 2 maxSpacesInComment: 1 # Inspection that checks if all comment's are inside if-else code blocks. Exception is general if comment - name: IF_ELSE_COMMENTS enabled: true # Type aliases provide alternative names for existing types when type's reference text is longer 25 chars - name: TYPE_ALIAS enabled: true configuration: typeReferenceLength: '25' # max length of type reference # Checks if casting can be omitted - name: SMART_CAST_NEEDED enabled: true # Checks that variables of generic types have explicit type declaration - name: GENERIC_VARIABLE_WRONG_DECLARATION enabled: true # Inspection that checks if string template has redundant curly braces - name: STRING_TEMPLATE_CURLY_BRACES enabled: true # Variables with `val` modifier - are immutable (read-only). Usage of such variables instead of `var` variables increases # robustness and readability of code, because `var` variables can be reassigned several times in the business logic. # This rule prohibits usage of `var`s as local variables - the only exception is accumulators and counters - name: SAY_NO_TO_VAR enabled: true # Inspection that checks if string template has redundant quotes - name: STRING_TEMPLATE_QUOTES enabled: true # Check if there are redundant nested if-statements, which could be collapsed into a single one by concatenating their conditions - name: COLLAPSE_IF_STATEMENTS enabled: true configuration: startCollapseFromNestedLevel: 2 # Checks that floating-point values are not used in arithmetic expressions - name: FLOAT_IN_ACCURATE_CALCULATIONS enabled: true # Checks that function length isn't too long - name: TOO_LONG_FUNCTION enabled: true configuration: maxFunctionLength: '30' # max length of function isIncludeHeader: 'false' # count function's header # Warns if there are nested functions - name: AVOID_NESTED_FUNCTIONS enabled: true # Checks that lambda inside function parameters is in the end - name: LAMBDA_IS_NOT_LAST_PARAMETER enabled: true # Checks that function doesn't contains too many parameters - name: TOO_MANY_PARAMETERS enabled: true configuration: maxParameterListSize: '5' # max parameters size # Checks that function doesn't have too many nested blocks - name: NESTED_BLOCK enabled: true configuration: maxNestedBlockQuantity: '4' # Checks that function use default values, instead overloading - name: WRONG_OVERLOADING_FUNCTION_ARGUMENTS enabled: true # Checks that using runBlocking inside async block code - name: RUN_BLOCKING_INSIDE_ASYNC enabled: true # Checks that property in constructor doesn't contain comment - name: KDOC_NO_CONSTRUCTOR_PROPERTY enabled: true configuration: isParamTagsForParameters: true # create param tags for parameters without val or var isParamTagsForPrivateProperties: true # create param tags for private properties isParamTagsForGenericTypes: true # create param tags for generic types # Checks that the long lambda has parameters - name: TOO_MANY_LINES_IN_LAMBDA enabled: true configuration: maxLambdaLength: 10 # max length of lambda without parameters # Checks that using unnecessary, custom label - name: CUSTOM_LABEL enabled: true # Check that lambda with inner lambda doesn't use implicit parameter - name: PARAMETER_NAME_IN_OUTER_LAMBDA enabled: true # Checks that property in KDoc present in class - name: KDOC_EXTRA_PROPERTY enabled: true # There's a property in KDoc which is already present - name: KDOC_DUPLICATE_PROPERTY enabled: true # Checks that KDoc in constructor has property tag but with comment inside constructor - name: KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT enabled: true # if a class has single constructor, it should be converted to a primary constructor - name: SINGLE_CONSTRUCTOR_SHOULD_BE_PRIMARY enabled: true # Checks if class can be made as data class - name: USE_DATA_CLASS enabled: true # Checks that never use the name of a variable in the custom getter or setter - name: WRONG_NAME_OF_VARIABLE_INSIDE_ACCESSOR enabled: true # Checks that classes have only one init block - name: MULTIPLE_INIT_BLOCKS enabled: true # Checks that there are abstract functions in abstract class - name: CLASS_SHOULD_NOT_BE_ABSTRACT enabled: true # Checks if there are any trivial getters or setters - name: TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED enabled: true # Checks that no custom getters and setters are used for properties. It is a more wide rule than TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED # Kotlin compiler automatically generates `get` and `set` methods for properties and also lets the possibility to override it. # But in all cases it is very confusing when `get` and `set` are overridden for a developer who uses this particular class. # Developer expects to get the value of the property, but receives some unknown value and some extra side effect hidden by the custom getter/setter. # Use extra functions for it instead. - name: CUSTOM_GETTERS_SETTERS enabled: true # Checks if null-check was used explicitly (for example: if (a == null)) # Try to avoid explicit null checks (explicit comparison with `null`) # Kotlin is declared as [Null-safe](https://kotlinlang.org/docs/reference/null-safety.html) language. # But Kotlin architects wanted Kotlin to be fully compatible with Java, that's why `null` keyword was also introduced in Kotlin. # There are several code-structures that can be used in Kotlin to avoid null-checks. For example: `?:`, `.let {}`, `.also {}`, e.t.c - name: AVOID_NULL_CHECKS enabled: true # Checks if class instantiation can be wrapped in `apply` for better readability - name: COMPACT_OBJECT_INITIALIZATION enabled: true # Checks explicit supertype qualification - name: USELESS_SUPERTYPE enabled: true # Checks if extension function with the same signature don't have related classes - name: EXTENSION_FUNCTION_SAME_SIGNATURE enabled: true # Checks if there is empty primary constructor - name: EMPTY_PRIMARY_CONSTRUCTOR enabled: true # In case of not using field keyword in property accessors, # there should be explicit backing property with the name of real property # Example: val table get() {if (_table == null) ...} -> table should have _table - name: NO_CORRESPONDING_PROPERTY enabled: true # Checks if there is class/object that can be replace with extension function - name: AVOID_USING_UTILITY_CLASS enabled: true # If there is stateless class it is preferred to use object - name: OBJECT_IS_PREFERRED enabled: true # If there exists negated version of function you should prefer it instead of !functionCall - name: INVERSE_FUNCTION_PREFERRED enabled: true # Checks if class can be converted to inline class - name: INLINE_CLASS_CAN_BE_USED enabled: true # If file contains class, then it can't contain extension functions for the same class - name: EXTENSION_FUNCTION_WITH_CLASS enabled: true # Check if kts script contains other functions except run code - name: RUN_IN_SCRIPT enabled: true # Check if boolean expression can be simplified - name: COMPLEX_BOOLEAN_EXPRESSION enabled: true # Check if range can replace with until or `rangeTo` function with range - name: CONVENTIONAL_RANGE enabled: true configuration: isRangeToIgnore: false # Check if there is a call of print()\println() or console.log(). Assumption that it's a debug print - name: DEBUG_PRINT enabled: true # Check that typealias name is in PascalCase - name: TYPEALIAS_NAME_INCORRECT_CASE enabled: true # Should change property length - 1 to property lastIndex - name: USE_LAST_INDEX enabled: true # Only properties from the primary constructor should be documented in a @property tag in class KDoc - name: KDOC_NO_CLASS_BODY_PROPERTIES_IN_HEADER enabled: true ================================================ FILE: examples/maven/pom.xml ================================================ 4.0.0 com.saveourtool.diktat diktat-examples-maven pom 0.0-SNAPSHOT 2.0.0 com.saveourtool.diktat diktat-maven-plugin ${diktat.version} diktat-analysis.yml ${project.basedir}/src/main/kotlin ${project.basedir}/src/test/kotlin diktat check fix ================================================ FILE: examples/maven/src/main/kotlin/AnotherTest.kt ================================================ package whate.ver fun String.createPluginConfig() { val pluginConfig = TomlDecoder.decode( serializer(), fakeFileNode, DecoderConf() ) pluginConfig.configLocation = this.toPath() pluginConfig.prop1 = property1 // comment1 pluginConfig.configLocation2 = this.toPath() // comment2 pluginConfig.prop2 = property2 } ================================================ FILE: examples/maven/src/main/kotlin/Test.kt ================================================ package incorrect class incorrectname: Exception() { fun INCORRECT_FUNCTION() { throw Exception() } // fun myCommentedFunction() { // } val Incorrect_Val = 5 } ================================================ FILE: examples/maven-multiproject/backend/pom.xml ================================================ 4.0.0 com.saveourtool.diktat diktat-examples-maven-multiproject 0.0-SNAPSHOT ../pom.xml backend ================================================ FILE: examples/maven-multiproject/backend/src/main/kotlin/Test.kt ================================================ package incorrect class incorrectname: Exception() { fun INCORRECT_FUNCTION() { throw Exception() } // fun myCommentedFunction() { // } val Incorrect_Val = 5 } ================================================ FILE: examples/maven-multiproject/diktat-analysis.yml ================================================ # Common configuration - name: DIKTAT_COMMON configuration: # put your package name here - it will be autofixed and checked domainName: your.name.here testDirs: test # expected values: disabledChapters: "Naming, Comments, General, Variables, Functions, Classes" # or: "1, 2, 3, 4, 5, 6" disabledChapters: "" kotlinVersion: 2.1 srcDirectories: "main" # Checks that the Class/Enum/Interface name matches Pascal case - name: CLASS_NAME_INCORRECT enabled: true # all code blocks with MyAnnotation will be ignored and not checked ignoreAnnotated: [ MyAnnotation ] # Checks that CONSTANT (treated as const val from companion object or class level) is in non UPPER_SNAKE_CASE - name: CONSTANT_UPPERCASE enabled: true # Checks that enum value is in upper SNAKE_CASE or in PascalCase depending on the config. UPPER_SNAKE_CASE is the default, but can be changed by 'enumStyle' config - name: ENUM_VALUE enabled: true configuration: # Two options: SNAKE_CASE (default), PascalCase enumStyle: SNAKE_CASE # Checks that class which extends any Exception class has Exception suffix - name: EXCEPTION_SUFFIX enabled: true # Checks that file name has extension - name: FILE_NAME_INCORRECT enabled: true # Checks that file name matches class name, if it is only one class in file - name: FILE_NAME_MATCH_CLASS enabled: true # Checks that functions/methods which return boolean have special prefix like "is/should/e.t.c" - name: FUNCTION_BOOLEAN_PREFIX enabled: true configuration: allowedPrefixes: "" # A list of functions that return boolean and are allowed to use. Input is in a form "foo, bar". # Checks that function/method name is in lowerCamelCase - name: FUNCTION_NAME_INCORRECT_CASE enabled: true # Checks that typealias name is in PascalCase - name: TYPEALIAS_NAME_INCORRECT_CASE enabled: true # Checks that generic name doesn't contain more than 1 letter (capital). It can be followed by numbers, example: T12, T - name: GENERIC_NAME enabled: true # Identifier length should be in range [2,64] except names that used in industry like {i, j} and 'e' for catching exceptions - name: IDENTIFIER_LENGTH enabled: true # Checks that the object matches PascalCase - name: OBJECT_NAME_INCORRECT enabled: true # Checks that package name is in correct (lower) case - name: PACKAGE_NAME_INCORRECT_CASE enabled: true # Checks that package name starts with the company's domain - name: PACKAGE_NAME_INCORRECT_PREFIX enabled: false # Checks that package name does not have incorrect symbols like underscore or non-ASCII letters/digits - name: PACKAGE_NAME_INCORRECT_SYMBOLS enabled: true # Checks that the path for a file matches with a package name - name: PACKAGE_NAME_INCORRECT_PATH enabled: false # Checks that package name is in the file - name: PACKAGE_NAME_MISSING enabled: true # Checks that variable does not have prefix (like mVariable or M_VARIABLE) - name: VARIABLE_HAS_PREFIX enabled: true # Checks that variable does not contain one single letter, only exceptions are fixed names that used in industry like {i, j} - name: VARIABLE_NAME_INCORRECT enabled: true # Checks that the name of variable is in lowerCamelCase and contains only ASCII letters - name: VARIABLE_NAME_INCORRECT_FORMAT enabled: true # Checks that functions have kdoc - name: MISSING_KDOC_ON_FUNCTION enabled: true # Checks that on file level internal or public class or function has missing KDoc - name: MISSING_KDOC_TOP_LEVEL enabled: true # Checks that accessible internal elements (protected, public, internal) in a class are documented - name: MISSING_KDOC_CLASS_ELEMENTS enabled: true # Checks that accessible method parameters are documented in KDoc - name: KDOC_WITHOUT_PARAM_TAG enabled: true # Checks that accessible method explicit return type is documented in KDoc - name: KDOC_WITHOUT_RETURN_TAG enabled: true # Checks that accessible method throw keyword is documented in KDoc - name: KDOC_WITHOUT_THROWS_TAG enabled: true # Checks that KDoc is not empty - name: KDOC_EMPTY_KDOC enabled: true # Checks that underscore is correctly used to split package naming - name: INCORRECT_PACKAGE_SEPARATOR enabled: true # Checks that code block doesn't contain kdoc comments - name: COMMENTED_BY_KDOC enabled: true # Checks that there is no @deprecated tag in kdoc - name: KDOC_NO_DEPRECATED_TAG enabled: true # Checks that there is no empty content in kdoc tags - name: KDOC_NO_EMPTY_TAGS enabled: true # Checks that there is only one space after kdoc tag - name: KDOC_WRONG_SPACES_AFTER_TAG enabled: true # Checks tags order in kDoc. `@param`, `@return`, `@throws` - name: KDOC_WRONG_TAGS_ORDER enabled: true # Checks that there is no newline of empty KDoc line (with leading asterisk) between `@param`, `@return`, `@throws` tags - name: KDOC_NO_NEWLINES_BETWEEN_BASIC_TAGS enabled: true # Checks that block of tags @param, @return, @throws is separated from previous part of KDoc by exactly one empty line - name: KDOC_NEWLINES_BEFORE_BASIC_TAGS enabled: true # Checks that special tags `@apiNote`, `@implNote`, `@implSpec` have exactly one empty line after - name: KDOC_NO_NEWLINE_AFTER_SPECIAL_TAGS enabled: true # Checks that kdoc does not contain @author tag or date - name: KDOC_CONTAINS_DATE_OR_AUTHOR enabled: true configuration: versionRegex: \d+\.\d+\.\d+[-.\w\d]* # Checks that KDoc does not contain single line with words 'return', 'get' or 'set' - name: KDOC_TRIVIAL_KDOC_ON_FUNCTION enabled: true # Checks that there is newline after header KDoc - name: HEADER_WRONG_FORMAT enabled: true # Checks that file with zero or >1 classes has header KDoc - name: HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE enabled: true # Checks that copyright exists on top of file and is properly formatted (as a block comment) - name: HEADER_MISSING_OR_WRONG_COPYRIGHT enabled: true configuration: isCopyrightMandatory: false copyrightText: 'Copyright (c) Your Company Name Here. 2010-;@currYear;' # Checks that header kdoc is located before package directive - name: HEADER_NOT_BEFORE_PACKAGE enabled: true # Checks that file does not contain lines > maxSize - name: FILE_IS_TOO_LONG enabled: true configuration: # number of lines maxSize: '2000' # Checks that file does not contain commented out code - name: COMMENTED_OUT_CODE enabled: true # Checks that file does not contain only comments, imports and package directive - name: FILE_CONTAINS_ONLY_COMMENTS enabled: true # Orders imports alphabetically - name: FILE_UNORDERED_IMPORTS enabled: true configuration: # use logical imports grouping with sorting inside of a group useRecommendedImportsOrder: true # Checks that general order of code parts is right - name: FILE_INCORRECT_BLOCKS_ORDER enabled: true # Checks that there is exactly one line between code blocks - name: FILE_NO_BLANK_LINE_BETWEEN_BLOCKS enabled: true # Checks that there is no wildcard imports. Exception: allowedWildcards - name: FILE_WILDCARD_IMPORTS enabled: true configuration: allowedWildcards: "" # Allowed wildcards for imports (e.g. "import com.saveourtool.diktat.*, import org.jetbrains.kotlin.*") useRecommendedImportsOrder: true # Checks unused imports - name: UNUSED_IMPORT enabled: true configuration: deleteUnusedImport: true # Checks that braces are used in if, else, when, for, do, and while statements. Exception: single line ternary operator statement - name: NO_BRACES_IN_CONDITIONALS_AND_LOOPS enabled: true # Checks that the declaration part of a class-like code structures (class/interface/etc.) is in the proper order - name: WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES enabled: true # Checks that properties with comments are separated by a blank line - name: BLANK_LINE_BETWEEN_PROPERTIES enabled: true # Checks top level order - name: TOP_LEVEL_ORDER enabled: true # Checks that non-empty code blocks with braces follow the K&R style (1TBS or OTBS style) - name: BRACES_BLOCK_STRUCTURE_ERROR enabled: true configuration: openBraceNewline: 'True' closeBraceNewline: 'True' # Checks that indentation is correct - name: WRONG_INDENTATION enabled: true configuration: # Is newline at the end of a file needed newlineAtEnd: true # If true: in parameter list when parameters are split by newline they are indented with two indentations instead of one extendedIndentOfParameters: false # If true: if first parameter in parameter list is on the same line as opening parenthesis, then other parameters can be aligned with it alignedParameters: true # If true, expression bodies which begin on a separate line are indented # using a continuation indent. The default is false. # # This flag is called CONTINUATION_INDENT_FOR_EXPRESSION_BODIES in IDEA and # ij_kotlin_continuation_indent_for_expression_bodies in .editorconfig. extendedIndentForExpressionBodies: false # If true: if expression is split by newline after operator like +/-/`*`, then the next line is indented with two indentations instead of one extendedIndentAfterOperators: true # If true: when dot qualified expression starts on a new line, this line will be indented with two indentations instead of one extendedIndentBeforeDot: false # The indentation size for each file indentationSize: 4 # Checks that there is no empty blocks in a file. # If allowEmptyBlocks is true, checks that it follows correct style (have a newline) - name: EMPTY_BLOCK_STRUCTURE_ERROR enabled: true configuration: # Whether a newline after `{` is required in an empty block styleEmptyBlockWithNewline: 'True' allowEmptyBlocks: 'False' # Checks that there is no more than one statement per line - name: MORE_THAN_ONE_STATEMENT_PER_LINE enabled: true # Checks that the line length is < lineLength parameter - name: LONG_LINE enabled: true configuration: lineLength: '120' # Checks that semicolons are not used at the end of a line - name: REDUNDANT_SEMICOLON enabled: true # Checks that line breaks follow code style guide: rule 3.6 - name: WRONG_NEWLINES enabled: true configuration: # If the number of parameters on one line is more than this threshold, all parameters will be placed on separate lines. maxParametersInOneLine: 2 # 3 by default. maxCallsInOneLine: 3 # Checks trailing comma - name: TRAILING_COMMA enabled: true configuration: # VALUE_ARGUMENT valueArgument: false # VALUE_PARAMETER valueParameter: false # REFERENCE_EXPRESSION indices: false # WHEN_CONDITION_WITH_EXPRESSION whenConditions: false # STRING_TEMPLATE collectionLiteral: false # TYPE_PROJECTION typeArgument: false # TYPE_PARAMETER typeParameter: false # DESTRUCTURING_DECLARATION_ENTRY destructuringDeclaration: false # Checks that there are not too many consecutive spaces in line - name: TOO_MANY_CONSECUTIVE_SPACES enabled: true configuration: # Maximum allowed number of consecutive spaces (not counting indentation) maxSpaces: '1' # Whether formatting for enums should be kept without checking saveInitialFormattingForEnums: false # Inspection that checks if a long dot qualified expression is used in condition or as an argument - name: COMPLEX_EXPRESSION enabled: true # Checks that blank lines are used correctly. # For example: triggers when there are too many blank lines between function declaration - name: TOO_MANY_BLANK_LINES enabled: true # Checks that usage of horizontal spaces doesn't violate code style guide - name: WRONG_WHITESPACE enabled: true # Checks that backticks (``) are not used in the identifier name, except the case when it is test method (marked with @Test annotation) - name: BACKTICKS_PROHIBITED enabled: true # Checks that a single line concatenation of strings is not used - name: STRING_CONCATENATION enabled: true # Checks that each when statement have else in the end - name: WHEN_WITHOUT_ELSE enabled: true # Checks that annotation is on a single line - name: ANNOTATION_NEW_LINE enabled: true # Checks that method annotated with `Preview` annotation is private and has Preview suffix - name: PREVIEW_ANNOTATION enabled: true # Checks that enum structure is correct: enum entries should be separated by comma and line break and last entry should have semicolon in the end. - name: ENUMS_SEPARATED enabled: true # Checks that value on integer or float constant is not too big - name: LONG_NUMERICAL_VALUES_SEPARATED enabled: true configuration: # Maximum number of digits which are not split maxNumberLength: '5' # Maximum number of digits between separators maxBlockLength: '3' # Checks magic number - name: MAGIC_NUMBER enabled: true configuration: # Ignore numbers from test ignoreTest: "true" # Ignore numbers ignoreNumbers: "-1, 1, 0, 2, 0U, 1U, 2U, -1L, 0L, 1L, 2L, 0UL, 1UL, 2UL" # Is ignore override hashCode function ignoreHashCodeFunction: "true" # Is ignore property ignorePropertyDeclaration: "false" # Is ignore local variable ignoreLocalVariableDeclaration: "false" # Is ignore value parameter ignoreValueParameter: "true" # Is ignore constant ignoreConstantDeclaration: "true" # Is ignore property in companion object ignoreCompanionObjectPropertyDeclaration: "true" # Is ignore numbers in enum ignoreEnums: "false" # Is ignore number in ranges ignoreRanges: "false" # Is ignore number in extension function ignoreExtensionFunctions: "false" # Is ignore number in pairs created using to ignorePairsCreatedUsingTo: "false" # Checks that order of enum values or constant property inside companion is correct - name: WRONG_DECLARATIONS_ORDER enabled: true configuration: # Whether enum members should be sorted alphabetically sortEnum: true # Whether class properties should be sorted alphabetically sortProperty: true # Checks that multiple modifiers sequence is in the correct order - name: WRONG_MULTIPLE_MODIFIERS_ORDER enabled: true # Checks that identifier has appropriate name (See table of rule 1.2 part 6) - name: CONFUSING_IDENTIFIER_NAMING enabled: true # Checks year in the copyright - name: WRONG_COPYRIGHT_YEAR enabled: true # Inspection that checks if local variables are declared close to the first usage site - name: LOCAL_VARIABLE_EARLY_DECLARATION enabled: true # Try to avoid initialize val by null (e.g. val a: Int? = null -> val a: Int = 0) - name: NULLABLE_PROPERTY_TYPE enabled: true # Inspection that checks if there is a blank line before kDoc and none after - name: WRONG_NEWLINES_AROUND_KDOC enabled: true # Inspection that checks if there is no blank lines before first comment - name: FIRST_COMMENT_NO_BLANK_LINE enabled: true # Inspection that checks if there are blank lines between code and comment and between code start token and comment's text - name: COMMENT_WHITE_SPACE enabled: true configuration: maxSpacesBeforeComment: 2 maxSpacesInComment: 1 # Inspection that checks if all comment's are inside if-else code blocks. Exception is general if comment - name: IF_ELSE_COMMENTS enabled: true # Type aliases provide alternative names for existing types when type's reference text is longer 25 chars - name: TYPE_ALIAS enabled: true configuration: typeReferenceLength: '25' # max length of type reference # Checks if casting can be omitted - name: SMART_CAST_NEEDED enabled: true # Checks that variables of generic types have explicit type declaration - name: GENERIC_VARIABLE_WRONG_DECLARATION enabled: true # Inspection that checks if string template has redundant curly braces - name: STRING_TEMPLATE_CURLY_BRACES enabled: true # Variables with `val` modifier - are immutable (read-only). Usage of such variables instead of `var` variables increases # robustness and readability of code, because `var` variables can be reassigned several times in the business logic. # This rule prohibits usage of `var`s as local variables - the only exception is accumulators and counters - name: SAY_NO_TO_VAR enabled: true # Inspection that checks if string template has redundant quotes - name: STRING_TEMPLATE_QUOTES enabled: true # Check if there are redundant nested if-statements, which could be collapsed into a single one by concatenating their conditions - name: COLLAPSE_IF_STATEMENTS enabled: true configuration: startCollapseFromNestedLevel: 2 # Checks that floating-point values are not used in arithmetic expressions - name: FLOAT_IN_ACCURATE_CALCULATIONS enabled: true # Checks that function length isn't too long - name: TOO_LONG_FUNCTION enabled: true configuration: maxFunctionLength: '30' # max length of function isIncludeHeader: 'false' # count function's header # Warns if there are nested functions - name: AVOID_NESTED_FUNCTIONS enabled: true # Checks that lambda inside function parameters is in the end - name: LAMBDA_IS_NOT_LAST_PARAMETER enabled: true # Checks that function doesn't contains too many parameters - name: TOO_MANY_PARAMETERS enabled: true configuration: maxParameterListSize: '5' # max parameters size # Checks that function doesn't have too many nested blocks - name: NESTED_BLOCK enabled: true configuration: maxNestedBlockQuantity: '4' # Checks that function use default values, instead overloading - name: WRONG_OVERLOADING_FUNCTION_ARGUMENTS enabled: true # Checks that using runBlocking inside async block code - name: RUN_BLOCKING_INSIDE_ASYNC enabled: true # Checks that property in constructor doesn't contain comment - name: KDOC_NO_CONSTRUCTOR_PROPERTY enabled: true configuration: isParamTagsForParameters: true # create param tags for parameters without val or var isParamTagsForPrivateProperties: true # create param tags for private properties isParamTagsForGenericTypes: true # create param tags for generic types # Checks that the long lambda has parameters - name: TOO_MANY_LINES_IN_LAMBDA enabled: true configuration: maxLambdaLength: 10 # max length of lambda without parameters # Checks that using unnecessary, custom label - name: CUSTOM_LABEL enabled: true # Check that lambda with inner lambda doesn't use implicit parameter - name: PARAMETER_NAME_IN_OUTER_LAMBDA enabled: true # Checks that property in KDoc present in class - name: KDOC_EXTRA_PROPERTY enabled: true # There's a property in KDoc which is already present - name: KDOC_DUPLICATE_PROPERTY enabled: true # Checks that KDoc in constructor has property tag but with comment inside constructor - name: KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT enabled: true # if a class has single constructor, it should be converted to a primary constructor - name: SINGLE_CONSTRUCTOR_SHOULD_BE_PRIMARY enabled: true # Checks if class can be made as data class - name: USE_DATA_CLASS enabled: true # Checks that never use the name of a variable in the custom getter or setter - name: WRONG_NAME_OF_VARIABLE_INSIDE_ACCESSOR enabled: true # Checks that classes have only one init block - name: MULTIPLE_INIT_BLOCKS enabled: true # Checks that there are abstract functions in abstract class - name: CLASS_SHOULD_NOT_BE_ABSTRACT enabled: true # Checks if there are any trivial getters or setters - name: TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED enabled: true # Checks that no custom getters and setters are used for properties. It is a more wide rule than TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED # Kotlin compiler automatically generates `get` and `set` methods for properties and also lets the possibility to override it. # But in all cases it is very confusing when `get` and `set` are overridden for a developer who uses this particular class. # Developer expects to get the value of the property, but receives some unknown value and some extra side effect hidden by the custom getter/setter. # Use extra functions for it instead. - name: CUSTOM_GETTERS_SETTERS enabled: true # Checks if null-check was used explicitly (for example: if (a == null)) # Try to avoid explicit null checks (explicit comparison with `null`) # Kotlin is declared as [Null-safe](https://kotlinlang.org/docs/reference/null-safety.html) language. # But Kotlin architects wanted Kotlin to be fully compatible with Java, that's why `null` keyword was also introduced in Kotlin. # There are several code-structures that can be used in Kotlin to avoid null-checks. For example: `?:`, `.let {}`, `.also {}`, e.t.c - name: AVOID_NULL_CHECKS enabled: true # Checks if class instantiation can be wrapped in `apply` for better readability - name: COMPACT_OBJECT_INITIALIZATION enabled: true # Checks explicit supertype qualification - name: USELESS_SUPERTYPE enabled: true # Checks if extension function with the same signature don't have related classes - name: EXTENSION_FUNCTION_SAME_SIGNATURE enabled: true # Checks if there is empty primary constructor - name: EMPTY_PRIMARY_CONSTRUCTOR enabled: true # In case of not using field keyword in property accessors, # there should be explicit backing property with the name of real property # Example: val table get() {if (_table == null) ...} -> table should have _table - name: NO_CORRESPONDING_PROPERTY enabled: true # Checks if there is class/object that can be replace with extension function - name: AVOID_USING_UTILITY_CLASS enabled: true # If there is stateless class it is preferred to use object - name: OBJECT_IS_PREFERRED enabled: true # If there exists negated version of function you should prefer it instead of !functionCall - name: INVERSE_FUNCTION_PREFERRED enabled: true # Checks if class can be converted to inline class - name: INLINE_CLASS_CAN_BE_USED enabled: true # If file contains class, then it can't contain extension functions for the same class - name: EXTENSION_FUNCTION_WITH_CLASS enabled: true # Check if kts script contains other functions except run code - name: RUN_IN_SCRIPT enabled: true # Check if boolean expression can be simplified - name: COMPLEX_BOOLEAN_EXPRESSION enabled: true # Check if range can replace with until or `rangeTo` function with range - name: CONVENTIONAL_RANGE enabled: true configuration: isRangeToIgnore: false # Check if there is a call of print()\println() or console.log(). Assumption that it's a debug print - name: DEBUG_PRINT enabled: true # Check that typealias name is in PascalCase - name: TYPEALIAS_NAME_INCORRECT_CASE enabled: true # Should change property length - 1 to property lastIndex - name: USE_LAST_INDEX enabled: true # Only properties from the primary constructor should be documented in a @property tag in class KDoc - name: KDOC_NO_CLASS_BODY_PROPERTIES_IN_HEADER enabled: true ================================================ FILE: examples/maven-multiproject/frontend/pom.xml ================================================ 4.0.0 com.saveourtool.diktat diktat-examples-maven-multiproject 0.0-SNAPSHOT ../pom.xml frontend ================================================ FILE: examples/maven-multiproject/frontend/src/main/kotlin/AnotherTest.kt ================================================ package whate.ver fun String.createPluginConfig() { val pluginConfig = TomlDecoder.decode( serializer(), fakeFileNode, DecoderConf() ) pluginConfig.configLocation = this.toPath() pluginConfig.prop1 = property1 // comment1 pluginConfig.configLocation2 = this.toPath() // comment2 pluginConfig.prop2 = property2 } ================================================ FILE: examples/maven-multiproject/pom.xml ================================================ 4.0.0 com.saveourtool.diktat diktat-examples-maven-multiproject pom 0.0-SNAPSHOT 2.0.0 com.saveourtool.diktat diktat-maven-plugin ${diktat.version} diktat-analysis.yml ${project.basedir}/src/main/kotlin ${project.basedir}/src/test/kotlin diktat check fix backend frontend ================================================ FILE: gradle/libs.versions.toml ================================================ [versions] kotlin = "2.1.0" kotlin-ksp = "2.1.0-1.0.29" serialization = "1.6.3" ktlint = "1.5.0" junit = "5.10.2" junit-platfrom = "1.10.2" guava = "33.0.0-jre" commons-cli = "1.6.0" commons-io = "2.15.1" detekt = "1.23.5" dokka = "1.9.20" gradle-nexus-publish-plugin = "2.0.0" jacoco = "0.8.8" # maven maven-api = "3.9.6" maven-plugin-tools = "3.8.1" maven-plugin-testing-harness = "3.3.0" plexus = "3.0.0" jbool = "1.24" kotlin-logging = "6.0.3" log4j2 = "2.23.1" kaml = "0.57.0" sarif4k = "0.6.0" jupiter-itf-extension = "0.13.1" # executable jar kotlinx-cli = "0.3.6" gradle-shadow = "8.1.1" # copied from save-cloud jetbrains-annotations = "26.0.1" kotlinx-coroutines = "1.8.0" assertj = "3.25.3" diktat = "2.0.0" reckon = "0.18.3" spotless = "6.25.0" download = "5.6.0" [plugins] kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-js = { id = "org.jetbrains.kotlin.js", version.ref = "kotlin" } kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlin-plugin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } kotlin-plugin-jpa = { id = "org.jetbrains.kotlin.plugin.jpa", version.ref = "kotlin" } kotlin-plugin-allopen = { id = "org.jetbrains.kotlin.plugin.allopen", version.ref = "kotlin" } kotlin-ksp = { id = "com.google.devtools.ksp", version.ref = "kotlin-ksp" } talaiot-base = { id = "io.github.cdsap.talaiot.plugin.base", version = "2.0.4" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } spotless = { id = "com.diffplug.gradle.spotless", version.ref = "spotless" } download = { id = "de.undercouch.download", version.ref = "download" } shadow = { id = "com.github.johnrengelman.shadow", version.ref = "gradle-shadow" } [libraries] # plugins as dependency dokka-gradle-plugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" } gradle-nexus-publish-plugin = { module = "io.github.gradle-nexus:publish-plugin", version.ref = "gradle-nexus-publish-plugin"} download-plugin = { module = "de.undercouch:gradle-download-task", version.ref = "download" } # kotlin kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } kotlin-stdlib-common = { module = "org.jetbrains.kotlin:kotlin-stdlib-common", version.ref = "kotlin" } kotlin-stdlib-jdk7 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk7", version.ref = "kotlin" } kotlin-stdlib-jdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" } kotlin-compiler-embeddable = { module = "org.jetbrains.kotlin:kotlin-compiler-embeddable", version.ref = "kotlin" } kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } # ksp kotlin-ksp-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "kotlin-ksp" } # kotlinx serialization kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "serialization" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" } kotlinx-serialization-json-jvm = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-jvm", version.ref = "serialization" } # another serialization kaml = { module = "com.charleskorn.kaml:kaml", version.ref = "kaml" } # kotlinx coroutines kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } #kotlin libs # ktlint & detekt ktlint-rule-engine = { module = "com.pinterest.ktlint:ktlint-rule-engine", version.ref = "ktlint" } ktlint-rule-engine-core = { module = "com.pinterest.ktlint:ktlint-rule-engine-core", version.ref = "ktlint" } ktlint-logger = { module = "com.pinterest.ktlint:ktlint-logger", version.ref = "ktlint" } ktlint-cli-ruleset-core = { module = "com.pinterest.ktlint:ktlint-cli-ruleset-core", version.ref = "ktlint" } ktlint-cli-reporter-core = { module = "com.pinterest.ktlint:ktlint-cli-reporter-core", version.ref = "ktlint" } ktlint-cli-reporter-baseline = { module = "com.pinterest.ktlint:ktlint-cli-reporter-baseline", version.ref = "ktlint" } ktlint-cli-reporter-checkstyle = { module = "com.pinterest.ktlint:ktlint-cli-reporter-checkstyle", version.ref = "ktlint" } ktlint-cli-reporter-html = { module = "com.pinterest.ktlint:ktlint-cli-reporter-html", version.ref = "ktlint" } ktlint-cli-reporter-json = { module = "com.pinterest.ktlint:ktlint-cli-reporter-json", version.ref = "ktlint" } ktlint-cli-reporter-plain = { module = "com.pinterest.ktlint:ktlint-cli-reporter-plain", version.ref = "ktlint" } ktlint-cli-reporter-sarif = { module = "com.pinterest.ktlint:ktlint-cli-reporter-sarif", version.ref = "ktlint" } sarif4k = { module = "io.github.detekt.sarif4k:sarif4k", version.ref = "sarif4k" } sarif4k-jvm = { module = "io.github.detekt.sarif4k:sarif4k-jvm", version.ref = "sarif4k" } # apache apache-commons-cli = { module = "commons-cli:commons-cli", version.ref = "commons-cli" } apache-commons-io = { module = "commons-io:commons-io", version.ref = "commons-io" } # others guava = { module = "com.google.guava:guava", version.ref = "guava" } jbool-expressions = { module = "com.bpodgursky:jbool_expressions", version.ref = "jbool" } # logging kotlin-logging = { module = "io.github.oshai:kotlin-logging", version.ref = "kotlin-logging" } slf4j-api = { module = "org.slf4j:slf4j-api", version = "2.0.12" } log4j2-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j2" } log4j2-slf4j2 = { module = "org.apache.logging.log4j:log4j-slf4j2-impl", version.ref = "log4j2" } # cli kotlinx-cli = { module = "org.jetbrains.kotlinx:kotlinx-cli", version.ref = "kotlinx-cli" } # testing junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" } junit-vintage-engine = { module = "org.junit.vintage:junit-vintage-engine", version.ref = "junit" } junit-jupiter-extension-itf = { module = "com.soebes.itf.jupiter.extension:itf-jupiter-extension", version.ref = "jupiter-itf-extension" } assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj" } junit-platform-suite = { module = "org.junit.platform:junit-platform-suite-engine", version.ref = "junit-platfrom" } # maven maven-core = { module = "org.apache.maven:maven-core", version.ref = "maven-api" } maven-compat = { module = "org.apache.maven:maven-compat", version.ref = "maven-api" } maven-plugin-annotations = { module = "org.apache.maven.plugin-tools:maven-plugin-annotations", version.ref = "maven-plugin-tools" } maven-plugin-testing-harness = { module = "org.apache.maven.plugin-testing:maven-plugin-testing-harness", version.ref = "maven-plugin-testing-harness" } plexus-cipher = { module = "org.codehaus.plexus:plexus-cipher", version.ref = "plexus" } ######### copied from save-cloud kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } kotlin-plugin-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" } jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "jetbrains-annotations" } # java core libraries validation-api = { module = "jakarta.validation:jakarta.validation-api"} annotation-api = { module = "jakarta.annotation:jakarta.annotation-api"} # code quality diktat-gradle-plugin = { module = "com.saveourtool.diktat:diktat-gradle-plugin", version.ref = "diktat" } detekt-gradle-plugin = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" } gradle-plugin-spotless = { module = "com.diffplug.spotless:spotless-plugin-gradle", version.ref = "spotless" } reckon-gradle-plugin = { module = "org.ajoberstar.reckon:reckon-gradle", version.ref = "reckon" } ================================================ FILE: gradle/plugins/build.gradle.kts ================================================ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { `kotlin-dsl` } repositories { file("$rootDir/../../build/diktat-snapshot") .takeIf { it.exists() } ?.run { maven { url = this@run.toURI() } } maven { url = uri("https://s01.oss.sonatype.org/content/repositories/snapshots/") content { includeGroup("com.saveourtool.diktat") } } mavenCentral() gradlePluginPortal() } tasks.withType { compilerOptions { freeCompilerArgs.add("-opt-in=kotlin.RequiresOptIn") } } run { @Suppress("COMMENTED_OUT_CODE", "WRONG_INDENTATION") dependencies { // workaround https://github.com/gradle/gradle/issues/15383 implementation(files(project.libs.javaClass.superclass.protectionDomain.codeSource.location)) implementation(libs.kotlin.gradle.plugin) implementation(libs.reckon.gradle.plugin) implementation(libs.detekt.gradle.plugin) { exclude("io.github.detekt.sarif4k", "sarif4k") } implementation(libs.diktat.gradle.plugin) { exclude("io.github.detekt.sarif4k", "sarif4k") } implementation(libs.sarif4k) implementation(libs.gradle.plugin.spotless) implementation(libs.dokka.gradle.plugin) implementation(libs.gradle.nexus.publish.plugin) // extra dependencies implementation(libs.kotlin.stdlib) implementation(libs.kotlin.stdlib.common) implementation(libs.kotlin.stdlib.jdk7) implementation(libs.kotlin.stdlib.jdk8) implementation(libs.jetbrains.annotations) } } ================================================ FILE: gradle/plugins/settings.gradle.kts ================================================ rootProject.name = "buildutils" dependencyResolutionManagement { versionCatalogs { create("libs") { from(files("../libs.versions.toml")) } } } ================================================ FILE: gradle/plugins/src/main/kotlin/Versions.kt ================================================ @file:Suppress("CONSTANT_UPPERCASE", "PACKAGE_NAME_MISSING") object Versions { /** * JDK version which is used for building and running the project. */ const val jdk = "8" } ================================================ FILE: gradle/plugins/src/main/kotlin/com/saveourtool/diktat/buildutils/JacocoConfiguration.kt ================================================ /** * Configuration for code coverage calculation via Jacoco */ package com.saveourtool.diktat.buildutils import org.gradle.accessors.dm.LibrariesForLibs import org.gradle.api.Project import org.gradle.api.tasks.testing.Test import org.gradle.kotlin.dsl.apply import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.named import org.gradle.kotlin.dsl.the import org.gradle.testing.jacoco.plugins.JacocoPlugin import org.gradle.testing.jacoco.plugins.JacocoPluginExtension import org.gradle.testing.jacoco.tasks.JacocoReport /** * Configure jacoco for [this] project */ fun Project.configureJacoco() { apply() configure { toolVersion = the() .versions .jacoco .get() } tasks.named("test") { finalizedBy("jacocoTestReport") } tasks.named("jacocoTestReport") { dependsOn(tasks.named("test")) reports { xml.required.set(true) html.required.set(true) } } } ================================================ FILE: gradle/plugins/src/main/kotlin/com/saveourtool/diktat/buildutils/PublishingConfiguration.kt ================================================ /** * Publishing configuration file. */ @file:Suppress( "MISSING_KDOC_TOP_LEVEL", "MISSING_KDOC_ON_FUNCTION", ) package com.saveourtool.diktat.buildutils import io.github.gradlenexus.publishplugin.NexusPublishExtension import io.github.gradlenexus.publishplugin.NexusPublishPlugin import org.gradle.api.Named import org.gradle.api.Project import org.gradle.api.publish.PublishingExtension import org.gradle.api.publish.maven.MavenPom import org.gradle.api.publish.maven.MavenPublication import org.gradle.api.publish.maven.plugins.MavenPublishPlugin import org.gradle.api.publish.maven.tasks.PublishToMavenRepository import org.gradle.api.tasks.bundling.Jar import org.gradle.internal.logging.text.StyledTextOutput import org.gradle.internal.logging.text.StyledTextOutput.Style.Failure import org.gradle.internal.logging.text.StyledTextOutput.Style.Success import org.gradle.internal.logging.text.StyledTextOutputFactory import org.gradle.kotlin.dsl.apply import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.extra import org.gradle.kotlin.dsl.getByType import org.gradle.kotlin.dsl.register import org.gradle.kotlin.dsl.support.serviceOf import org.gradle.kotlin.dsl.withType import org.gradle.plugins.signing.Sign import org.gradle.plugins.signing.SigningExtension import org.gradle.plugins.signing.SigningPlugin import org.jetbrains.dokka.gradle.DokkaPlugin /** * Configures all aspects of the publishing process. */ fun Project.configurePublishing() { apply() if (this == rootProject) { configureNexusPublishing() configureGitHubPublishing() } afterEvaluate { configureSigning() } } /** * Configures _pom.xml_ * * @param project */ @Suppress("TOO_LONG_FUNCTION") fun MavenPom.configurePom(project: Project) { name.set(project.name) description.set(project.description ?: project.name) url.set("https://github.com/saveourtool/diktat") licenses { license { name.set("MIT License") url.set("https://opensource.org/license/MIT") distribution.set("repo") } } developers { developer { id.set("akuleshov7") name.set("Andrey Kuleshov") email.set("andrewkuleshov7@gmail.com") url.set("https://github.com/akuleshov7") } developer { id.set("petertrr") name.set("Peter Trifanov") email.set("peter.trifanov@gmail.com") url.set("https://github.com/petertrr") } developer { id.set("nulls") name.set("Nariman Abdullin") email.set("nulls.narik@gmail.com") url.set("https://github.com/nulls") } } scm { url.set("https://github.com/saveourtool/diktat") connection.set("scm:git:git://github.com/saveourtool/diktat.git") developerConnection.set("scm:git:git@github.com:saveourtool/diktat.git") } } /** * Configures all publications. The publications must already exist. */ @Suppress("TOO_LONG_FUNCTION") fun Project.configurePublications() { if (this == rootProject) { return } val sourcesJar = tasks.named(SOURCES_JAR) apply() @Suppress("GENERIC_VARIABLE_WRONG_DECLARATION") val dokkaJarProvider = tasks.register("dokkaJar") { group = "documentation" archiveClassifier.set("javadoc") from(tasks.named("dokkaHtml")) } configure { repositories { mavenLocal() } publications.withType().configureEach { artifact(sourcesJar) artifact(dokkaJarProvider) pom { configurePom(project) } } } } /** * Configures Maven Central as the publish destination. */ @Suppress("TOO_LONG_FUNCTION") private fun Project.configureNexusPublishing() { setPropertyFromEnv("OSSRH_USERNAME", "sonatypeUsername") setPropertyFromEnv("OSSRH_PASSWORD", "sonatypePassword") if (!hasProperties("sonatypeUsername", "sonatypePassword")) { styledOut(logCategory = "nexus") .style(StyledTextOutput.Style.Info) .text("Skipping Nexus publishing configuration as either ") .style(StyledTextOutput.Style.Identifier) .text("sonatypeUsername") .style(StyledTextOutput.Style.Info) .text(" or ") .style(StyledTextOutput.Style.Identifier) .text("sonatypePassword") .style(StyledTextOutput.Style.Info) .text(" are not set") .println() return } apply() configure { repositories { sonatype { /* * The default is https://oss.sonatype.org/service/local/. */ nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) /* * The default is https://oss.sonatype.org/content/repositories/snapshots/. */ snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) username.set(property("sonatypeUsername") as String) password.set(property("sonatypePassword") as String) } } } } /** * Configures GitHub Packages as the publish destination. */ private fun Project.configureGitHubPublishing() { configure { repositories { maven { name = "GitHub" url = uri("https://maven.pkg.github.com/saveourtool/diktat") credentials { username = findProperty("gpr.user") as String? ?: System.getenv("GITHUB_ACTOR") password = findProperty("gpr.key") as String? ?: System.getenv("GITHUB_TOKEN") } } } } } /** * Enables signing of the artifacts if the `signingKey` project property is set. * * Should be explicitly called after each custom `publishing {}` section. */ private fun Project.configureSigning() { setPropertyFromEnv("GPG_SEC", "signingKey") setPropertyFromEnv("GPG_PASSWORD", "signingPassword") if (hasProperty("signingKey")) { /* * GitHub Actions. */ configureSigningCommon { useInMemoryPgpKeys(property("signingKey") as String?, findProperty("signingPassword") as String?) } } else if ( this.hasProperties( "signing.keyId", "signing.password", "signing.secretKeyRingFile", ) ) { /*- * Pure-Java signing mechanism via `org.bouncycastle.bcpg`. * * Requires an 8-digit (short form) PGP key id and a present `~/.gnupg/secring.gpg` * (for gpg 2.1, run * `gpg --keyring secring.gpg --export-secret-keys >~/.gnupg/secring.gpg` * to generate one). */ configureSigningCommon() } else if (hasProperty("signing.gnupg.keyName")) { /*- * Use an external `gpg` executable. * * On Windows, you may need to additionally specify the path to `gpg` via * `signing.gnupg.executable`. */ configureSigningCommon { useGpgCmd() } } } /** * @param useKeys the block which configures the PGP keys. Use either * [SigningExtension.useInMemoryPgpKeys], [SigningExtension.useGpgCmd], or an * empty lambda. * @see SigningExtension.useInMemoryPgpKeys * @see SigningExtension.useGpgCmd */ private fun Project.configureSigningCommon(useKeys: SigningExtension.() -> Unit = {}) { apply() configure { useKeys() val publications = extensions.getByType().publications val publicationCount = publications.size val message = "The following $publicationCount publication(s) are getting signed: ${publications.map(Named::getName)}" val style = when (publicationCount) { 0 -> Failure else -> Success } styledOut(logCategory = "signing").style(style).println(message) sign(*publications.toTypedArray()) } tasks.withType().configureEach { // Workaround for the problem described at https://github.com/saveourtool/save-cli/pull/501#issuecomment-1439705340. // We have a single Javadoc artifact shared by all platforms, hence all publications depend on signing of this artifact. // This causes weird implicit dependencies, like `publishJsPublication...` depends on `signJvmPublication`. dependsOn(tasks.withType()) } } /** * Creates a styled text output. * * @param logCategory * @return [StyledTextOutput] */ private fun Project.styledOut(logCategory: String): StyledTextOutput = serviceOf().create(logCategory) /** * Determines if this project has all the given properties. * * @param propertyNames the names of the properties to locate. * @return `true` if this project has all the given properties, `false` otherwise. * @see Project.hasProperty */ private fun Project.hasProperties(vararg propertyNames: String): Boolean = propertyNames.asSequence().all(this::hasProperty) private fun Project.setPropertyFromEnv(envName: String, propertyName: String) { System.getenv(envName)?.let { extra.set(propertyName, it) } } ================================================ FILE: gradle/plugins/src/main/kotlin/com/saveourtool/diktat/buildutils/TaskNames.kt ================================================ /** * Names for common tasks */ package com.saveourtool.diktat.buildutils /** * Tasks with sources */ const val SOURCES_JAR = "sourcesJar" ================================================ FILE: gradle/plugins/src/main/kotlin/com/saveourtool/diktat/buildutils/VersioningConfiguration.kt ================================================ /** * Configuration for project versioning */ package com.saveourtool.diktat.buildutils import org.ajoberstar.reckon.core.Scope import org.ajoberstar.reckon.core.VersionTagParser import org.ajoberstar.reckon.gradle.ReckonExtension import org.ajoberstar.reckon.gradle.ReckonPlugin import org.eclipse.jgit.api.Git import org.eclipse.jgit.internal.storage.file.FileRepository import org.eclipse.jgit.storage.file.FileRepositoryBuilder import org.gradle.api.Project import org.gradle.kotlin.dsl.apply import org.gradle.kotlin.dsl.configure import java.util.Optional /** * Configures reckon plugin for [this] project, should be applied for root project only */ fun Project.configureVersioning() { apply() // should be provided in the gradle.properties configure { setDefaultInferredScope(Scope.MINOR.name) if (findProperty("reckon.stage")?.toString() == "snapshot") { snapshots() // skip -rc candidates tags setTagParser { tagName -> if (tagName.contains("-rc.[0-9]+".toRegex())) { Optional.empty() } else { VersionTagParser.getDefault().parse(tagName) } } } else { stages("rc", "final") } setScopeCalc(calcScopeFromProp()) setStageCalc(calcStageFromProp()) } val status = FileRepositoryBuilder() .findGitDir(project.rootDir) .setup() .let(::FileRepository) .let(::Git) .status() .call() if (!status.isClean) { logger.warn("git tree is not clean; " + "Untracked files: ${status.untracked}, uncommitted changes: ${status.uncommittedChanges}" ) } } ================================================ FILE: gradle/plugins/src/main/kotlin/com/saveourtool/diktat/buildutils/code-quality-convention.gradle.kts ================================================ package com.saveourtool.diktat.buildutils // FixMe: remove after 2.0.0 run { @Suppress("RUN_IN_SCRIPT", "AVOID_NULL_CHECKS") plugins { id("com.saveourtool.diktat.buildutils.detekt-convention-configuration") id("com.saveourtool.diktat.buildutils.diktat-convention-configuration") } } ================================================ FILE: gradle/plugins/src/main/kotlin/com/saveourtool/diktat/buildutils/detekt-convention-configuration.gradle.kts ================================================ package com.saveourtool.diktat.buildutils import io.gitlab.arturbosch.detekt.Detekt import io.gitlab.arturbosch.detekt.report.ReportMergeTask plugins { id("io.gitlab.arturbosch.detekt") } detekt { config.setFrom(rootProject.files("detekt-config.yml")) basePath = rootDir.canonicalPath buildUponDefaultConfig = true } @Suppress("RUN_IN_SCRIPT") if (path == rootProject.path) { tasks.register("detektAll") { allprojects { this@register.dependsOn(tasks.withType()) } } tasks.register("mergeDetektReports", ReportMergeTask::class) { output.set(layout.buildDirectory.file("detekt-sarif-reports/detekt-merged.sarif").get().asFile) } } @Suppress("GENERIC_VARIABLE_WRONG_DECLARATION") val reportMerge: TaskProvider = rootProject.tasks.named("mergeDetektReports") { input.from( tasks.withType().map { it.sarifReportFile } ) shouldRunAfter(tasks.withType()) } tasks.withType().configureEach { reports.sarif.required.set(true) finalizedBy(reportMerge) } ================================================ FILE: gradle/plugins/src/main/kotlin/com/saveourtool/diktat/buildutils/diktat-convention-configuration.gradle.kts ================================================ package com.saveourtool.diktat.buildutils plugins { id("com.saveourtool.diktat") } diktat { diktatConfigFile = rootProject.file("diktat-analysis.yml") githubActions = findProperty("diktat.githubActions")?.toString()?.toBoolean() ?: false inputs { // using `Project#path` here, because it must be unique in gradle's project hierarchy if (path == rootProject.path) { include("gradle/plugins/src/**/*.kt", "*.kts", "gradle/plugins/**/*.kts") exclude("gradle/plugins/build/**") } else { include("src/**/*.kt", "**/*.kts") exclude( "src/test/**/*.kt", "src/test/**/*.kts", "src/*Test/**/*.kt", "build/**/*.kts", ) } } } ================================================ FILE: gradle/plugins/src/main/kotlin/com/saveourtool/diktat/buildutils/git-hook-configuration.gradle.kts ================================================ package com.saveourtool.diktat.buildutils /** * Task of type [Copy] that install git hooks from directory in repo to .git directory */ val installGitHooksTask = tasks.register("installGitHooks", Copy::class) { from(file("$rootDir/.git-hooks")) into(file("$rootDir/.git/hooks")) } // add git hooks installation to build by adding it as a dependency for some common task run { tasks.findByName("build")?.dependsOn(installGitHooksTask) } ================================================ FILE: gradle/plugins/src/main/kotlin/com/saveourtool/diktat/buildutils/kotlin-jvm-configuration.gradle.kts ================================================ package com.saveourtool.diktat.buildutils import org.gradle.api.tasks.testing.Test plugins { kotlin("jvm") } java { toolchain { languageVersion.set(JavaLanguageVersion.of(Versions.jdk)) } } kotlin { compilerOptions { optIn.add("kotlin.RequiresOptIn") } jvmToolchain { languageVersion.set(JavaLanguageVersion.of(Versions.jdk)) } } tasks.register(SOURCES_JAR) { archiveClassifier.set("sources") from(kotlin.sourceSets.main.map { it.kotlin }) } configureJacoco() tasks.withType { useJUnitPlatform() } ================================================ FILE: gradle/plugins/src/main/kotlin/com/saveourtool/diktat/buildutils/publishing-configuration.gradle.kts ================================================ package com.saveourtool.diktat.buildutils import org.gradle.kotlin.dsl.`maven-publish` plugins { `maven-publish` } configurePublishing() ================================================ FILE: gradle/plugins/src/main/kotlin/com/saveourtool/diktat/buildutils/publishing-default-configuration.gradle.kts ================================================ package com.saveourtool.diktat.buildutils import org.gradle.api.publish.maven.MavenPublication import org.gradle.kotlin.dsl.create import org.gradle.kotlin.dsl.get plugins { `maven-publish` } publishing { publications { create("maven") { from(components["java"]) } } } configurePublications() configurePublishing() ================================================ FILE: gradle/plugins/src/main/kotlin/com/saveourtool/diktat/buildutils/versioning-configuration.gradle.kts ================================================ package com.saveourtool.diktat.buildutils configureVersioning() ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradle.properties ================================================ group=com.saveourtool.diktat # gradle performance org.gradle.jvmargs=-Xmx3g -XX:MaxMetaspaceSize=512m org.gradle.parallel=true org.gradle.vfs.watch=true # See # # If this is enabled, the Gradle Enterprise plug-in will be conflicting with # the Test Retry plug-in (org.gradle.test-retry, # ). systemProp.gradle.enterprise.testretry.enabled=false ================================================ FILE: gradlew ================================================ #!/bin/sh # # Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # SPDX-License-Identifier: Apache-2.0 # ############################################################################## # # Gradle start up script for POSIX generated by Gradle. # # Important for running: # # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is # noncompliant, but you have some other compliant shell such as ksh or # bash, then to run this script, type that shell name before the whole # command line, like: # # ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», # «${var#prefix}», «${var%suffix}», and «$( cmd )»; # * compound commands having a testable exit status, especially «case»; # * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # # (2) This script targets any POSIX shell, so it avoids extensions provided # by Bash, Ksh, etc; in particular arrays are avoided. # # The "traditional" practice of packing multiple parameters into a # space-separated string is a well documented source of bugs and security # problems, so this is (mostly) avoided, by progressively accumulating # options in "$@", and eventually passing that to Java. # # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; # see the in-line comments for details. # # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s ' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { echo "$*" } >&2 die () { echo echo "$*" echo exit 1 } >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "$( uname )" in #( CYGWIN* ) cygwin=true ;; #( Darwin* ) darwin=true ;; #( MSYS* | MINGW* ) msys=true ;; #( NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD=$JAVA_HOME/jre/sh/java else JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD=java if ! command -v java >/dev/null 2>&1 then die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi # Collect all arguments for the java command, stacking in reverse order: # * args from the command line # * the main class name # * -classpath # * -D...appname settings # * --module-path (only if needed) # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) # Now convert the arguments - kludge to limit ourselves to /bin/sh for arg do if case $arg in #( -*) false ;; # don't mess with options #( /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath [ -e "$t" ] ;; #( *) false ;; esac then arg=$( cygpath --path --ignore --mixed "$arg" ) fi # Roll the args list around exactly as many times as the number of # args, so each arg winds up back in the position where it started, but # possibly modified. # # NB: a `for` loop captures its iteration list before it begins, so # changing the positional parameters here affects neither the number of # iterations, nor the values presented in `arg`. shift # remove old arg set -- "$@" "$arg" # push replacement arg done fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ org.gradle.wrapper.GradleWrapperMain \ "$@" # Stop when "xargs" is not available. if ! command -v xargs >/dev/null 2>&1 then die "xargs is not available" fi # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. # # In Bash we could simply go: # # readarray ARGS < <( xargs -n1 <<<"$var" ) && # set -- "${ARGS[@]}" "$@" # # but POSIX shell has neither arrays nor command substitution, so instead we # post-process each arg (as a line of input to sed) to backslash-escape any # character that might be a shell metacharacter, then use eval to reverse # that process (while maintaining the separation between arguments), and wrap # the whole thing up as a single "set" statement. # # This will of course break if any of these variables contains a newline or # an unmatched quote. # eval "set -- $( printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | xargs -n1 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | tr '\n' ' ' )" '"$@"' exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @rem SPDX-License-Identifier: Apache-2.0 @rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute echo. 1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. 1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! set EXIT_CODE=%ERRORLEVEL% if %EXIT_CODE% equ 0 set EXIT_CODE=1 if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: info/README.md ================================================ # UPDATE THE MAIN [README.md](../README.md) #### To update the codestyle text in the main [README.md](../README.md): * You **NEED TO CHANGE** the content of the file `guide-chapter-.md`, contained in `info/guide`, the corresponding section of the rules that you changed / added. * ```console $ cd info/ $ ./gradlew :generateFullDoc $ ./gradlew :generateAvailableRules ``` #### You **DO NOT NEED TO CHANGE** the content of the [`diktat-coding-convention.md`](guide/diktat-coding-convention.md) file. ================================================ FILE: info/available-rules.md ================================================ | Chap | Standard | Rule name | Description | Fix | Config | FixMe | |------|----------|-------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 | 1.1.1 | CONFUSING_IDENTIFIER_NAMING | Check: warns if the identifier has an inappropriate name (see table of [rule 1.2 part 6](guide/diktat-coding-convention.md#-111-identifiers-naming-conventions)). | no | no | no | | 1 | 1.1.1 | BACKTICKS_PROHIBITED | Check: warns if backticks (``) are used in the identifier name, except in the case when it is a test method (marked with `@Test` annotation). | no | no | - | | 1 | 1.1.1 | VARIABLE_NAME_INCORRECT | Check: warns if a variable name consists of only a single character; the only exceptions are industry-standard fixed names, such as {`i`, `j`}.
Fix: No fix is available, since it is the responsibility of a human to choose a meaningful identifier name. | no | no | Recursively update usages of this class in the projectMake exceptions configurable. | | 1 | 1.1.1 | EXCEPTION_SUFFIX | Check: warns if a class that extends any `Exception` class does not have an "Exception" suffix.
Fix: adding the "Exception" suffix to the class name. | yes | no | Need to add tests for this. | | 1 | 1.1.1 | IDENTIFIER_LENGTH | Check: identifier length should be in the [2,64] range , except for industry-standard names, such as {`i`, `j`} and 'e' for catching exceptions.
Fix: Fix: no fix is available, since only a human can choose a meaningful identifier name, depending on the context. | no | no | May be make this rule configurable (length) | | 1 | 1.1.1 | FILE_NAME_INCORRECT | Check: warns if a file name does not have a `.kt`/`.kts` extension.
Fix: no | no | no | Extensions should be configurable; it can be autofixed. | | 1 | 1.1.1 | GENERIC_NAME | Check: warns if the name of a [_generic type parameter_](https://kotlinlang.org/docs/generics.html) (e.g. `T`) consists of more than a single capital letter. The capital letter can be followed by numbers, though (e.g. `T12`).
Fix: | yes | no | Recursively update usages of this identifier in the project. | | 1 | 1.1.1 | VARIABLE_HAS_PREFIX | Check: warns if the name of a variable has a [Hungarian notation](https://en.wikipedia.org/wiki/Hungarian_notation) specific prefix (like `mVariable` or `M_VARIABLE`), which is considered a bad code style (_Android_ is the only exception).
Fix: none is available, as only a human can choose a meaningful name, depending on a particular context. | no | no | | | 1 | 1.2.1 | PACKAGE_NAME_MISSING | Check: warns if a package declaration is missing in the file.
Fix: automatically adds a package directive with the name that starts from the domain name (example - com.huawei) and contains the real directory. | yes | no | Recursively fix all imports in the project.
Fix the directory where the code is stored.
Make this check isolated from the domain name addition. | | 1 | 1.2.1 | PACKAGE_NAME_INCORRECT_CASE | Check: warns if a package name is incorrect (non-lower) case.
Fix: automatically update the case in package name. | yes | no | Recursively update all imports in the project. | | 1 | 1.2.1 | PACKAGE_NAME_INCORRECT_PREFIX | Check: warns if a package name does not start with the company's domain.
Fix: automatically update the prefix in the package name. | yes | no | Fix the directory where the code is stored.
Recursively update all imports in the project. | | 1 | 1.2.1 | PACKAGE_NAME_INCORRECT_SYMBOLS | Check: warns if a package name has incorrect symbols, such as underscore or non-ASCII letters/digits. Exception: underscores that are used for differentiating of keywords in a name.
Fix: no fix currently available; will be suggested later. | no | no | Add autofix for at least converting underscore to a dot or replacing itFix the directory where the code is stored. Cover autofix with tests. | | 1 | 1.2.1 | PACKAGE_NAME_INCORRECT_PATH | Check: warns if the path for a file does not match with a package name.
Fix: replacing the incorrect package name with the name constructed from a path to the file. | yes | no | Make this check isolated from domain name creation. Recursively update all imports in the project. Fix the directory where the code is stored. Add a test mechanism to test checker. | | 1 | 1.2.1 | INCORRECT_PACKAGE_SEPARATOR | Check: warns if the underscore is incorrectly used in package naming to split name parts.
Fix: fixing all nodes in AST and the package name to remove all underscores. | no | no | Recursively update usages of this class in the project. | | 1 | 1.3.1 | CLASS_NAME_INCORRECT | Check: warns if the Class/Enum/Interface name does not match the Pascal case ("([A-Z][a-z0-9]+)+").
Fix: fixing the case. If it is some fixed case (like Snake or Camel) - with word saving; if not - will restore PascalCase as is. | yes | no | Recursively update usages of this class in the projectCheck and find the better way of converting the identifier to PascalCaseNeed to add checks using natural language processing and check that the class name contains only nouns. | | 1 | 1.3.1 | OBJECT_NAME_INCORRECT | Check: warns if the object does not match the Pascal case ("([A-Z][a-z0-9]+)+").
Fix: fixing the case in the same way as for the classes. | yes | no | Recursively update usages of this class in the project. | | 1 | 1.3.1 | ENUM_VALUE | Check: verifies if the enum value is in UPPER_SNAKE_CASE or in PascalCase depending on the configuration. UPPER_SNAKE_CASE is the default configuration, but can be changed by 'enumStyle' config.
Fix: automatically converting the enum case to a properly selected case | yes | enumStyle: snakeCase, pascalCase | Recursively update usages of this identifier in the project. | | 1 | 1.3.1 | TYPEALIAS_NAME_INCORRECT_CASE | Check: typealias name should be in pascalCase.
Fix: | yes | no | Recursively update usages of this typealias in the project. | | 1 | 1.4.1 | FUNCTION_NAME_INCORRECT_CASE | Check: function/method name should be in lowerCamelCase.
Fix: | yes | no | Recursively update usages of this function in the project. | | 1 | 1.5.1 | CONSTANT_UPPERCASE | Check: warns if CONSTANT (treated as const val from companion object or class level) is not in UPPER_SNAKE_CASE.
Fix: name is changed to UPPER_SNAKE_CASE. | yes | no | Recursively update usages of this identifier in the project. | | 1 | 1.6.1 | VARIABLE_NAME_INCORRECT_FORMAT | Check: warns if the name of a variable is not in lowerCamelCase or contains non-ASCII letters.
Fix: fixing the case format to lowerCamelCase. | yes | no | - | | 1 | 1.6.2 | FUNCTION_BOOLEAN_PREFIX | Check: functions/methods that return boolean should have a special prefix, such as "is/should/etc."
Fix: | yes | no | Recursively update usages of this function in the project. Aggressive fix - what if the new name will not be valid? | | 2 | 2.1.1 | MISSING_KDOC_TOP_LEVEL | Check: warns at a file level internal or public class or if the function has a missing KDoc.
Fix: no | no | no | Support extension for setters/getters. Support extension for method "override". | | 2 | 2.1.1 | KDOC_EXTRA_PROPERTY | Check: warn if there is a property in KDoc that is not present in the class. | no | no | - | | 2 | 2.1.1 | KDOC_DUPLICATE_PROPERTY | Check: warn if there's a property in KDoc which is already present. | no | no | - | | 2 | 2.1.1 | MISSING_KDOC_CLASS_ELEMENTS | Check: warns if accessible internal elements (protected, public, internal) in a class are not documented.
Fix: no | no | no | Maybe exception cases for setters and getters are needed; no sense in adding KDoc to them. | | 2 | 2.1.1 | MISSING_KDOC_ON_FUNCTION | Check: warns if accessible function doesn't have KDoc.
Fix: adds KDoc template if it is not empty. | yes | no | | | 2 | 2.1.1 | KDOC_NO_CONSTRUCTOR_PROPERTY | Check: warns if there is no property tag inside KDoc before the constructor. | yes | isParamTagsForParameters, isParamTagsForPrivateProperties, isParamTagsForGenericTypes | | | 2 | 2.1.1 | KDOC_NO_CLASS_BODY_PROPERTIES_IN_HEADER | Check: warns if the property is declared in a class body but documented with a property tag inside KDoc before the constructor. | yes | no | | | 2 | 2.1.1 | KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT | Check: warns if there is a comment before the property in the constructor. | yes | no | | | 2 | 2.1.1 | COMMENTED_BY_KDOC | Check: warns if there is a kdoc comment in the code block. | yes | no | replace "/**" to "/*" | | 2 | 2.1.2 | KDOC_WITHOUT_PARAM_TAG | Check: warns if an accessible method has parameters and they are not documented in KDoc.
Fix: If accessible method has no KDoc, the KDoc template is added. | yes | no | Should also make a separate fix, even if there is already some Kdoc in place. | | 2 | 2.1.2 | KDOC_WITHOUT_RETURN_TAG | Check: warns if the accessible method has an explicit return type. Then it is not documented in KDoc.
Fix: If the accessible method has no KDoc, the KDoc template is added. | yes | no | Should also make separate fix, even if there is already some Kdoc in place. | | 2 | 2.1.2 | KDOC_WITHOUT_THROWS_TAG | Check: warns if the accessible method has the throw keyword and it is not documented in KDoc.
Fix: if the accessible method has no KDoc, KDoc template is added. | yes | no | Should also make separate fix, even if there is already some Kdoc in place. | | 2 | 2.1.3 | KDOC_EMPTY_KDOC | Check: warns if the KDoc is empty.
Fix: no | no | no | | | 2 | 2.1.3 | KDOC_WRONG_SPACES_AFTER_TAG | Check: warns if there is more than one space after the KDoc tag.
Fix: removes redundant spaces. | yes | no | | | 2 | 2.1.3 | KDOC_WRONG_TAGS_ORDER | Check: warns if the basic KDoc tags are not ordered properly.
Fix: reorders tags (`@receiver`, `@param`, `@property`, `@return`, `@throws` or `@exception`, `@constructor`). | yes | no | Ensure basic tags are at the end of KDoc. | | 2 | 2.1.3 | KDOC_NEWLINES_BEFORE_BASIC_TAGS | Check: warns if the block of tags (@param, @return, @throws) is not separated from the previous part of KDoc by exactly one empty line.
Fix: adds an empty line or removes a redundant one. | yes | no | | | 2 | 2.1.3 | KDOC_NO_NEWLINES_BETWEEN_BASIC_TAGS | Check: if there is a newline of an empty KDoc line (with a leading asterisk) between `@param`, `@return`, `@throws` tags.
Fix: removes the line. | yes | no | | | 2 | 2.1.3 | KDOC_NO_NEWLINE_AFTER_SPECIAL_TAGS | Check: warns if special tags `@apiNote`, `@implNote`, `@implSpec` don't have exactly one empty line after.
Fix: removes redundant lines or adds one. | yes | no | Handle empty lines without a leading asterisk. | | 2 | 2.1.3 | KDOC_NO_DEPRECATED_TAG | Check: warns if `@deprecated` is used in KDoc.
Fix: adds `@Deprecated` annotation with a message; removes the tag. | yes | no | The `replaceWith` field in the annotation can also be filled with a meaningful value. | | 2 | 2.2.1 | KDOC_NO_EMPTY_TAGS | Check: warns if the KDoc tags have an empty content. | no | no | | | 2 | 2.2.1 | KDOC_CONTAINS_DATE_OR_AUTHOR | Check: warns if the KDoc header contains the `@author` tag.
Also warns if the `@since` tag is present but contains anything but version (e.g.: a date). | no | no | Detect the author by other patterns (e.g. 'created by' etc.) | | 2 | 2.2.1 | HEADER_WRONG_FORMAT | Checks: warns if there is no newline after the KDoc header.
Fix: adds a newline | yes | no | Check if the header is on the very top of the file. It is hard to determine when it is not. | | 2 | 2.2.1 | HEADER_MISSING_OR_WRONG_COPYRIGHT | Checks: copyright exists on top of the file and is properly formatted (as a block comment).
Fix: adds copyright if it is missing and required. | yes | isCopyrightMandatory, copyrightText (sets the copyright pattern for your project, you also can use `;@currYear;` the key word in your text in aim to indicate the current year, instead of explicitly specifying). | | | 2 | 2.2.1 | WRONG_COPYRIGHT_YEAR | Checks: copyright has a valid year.
Fix: makes the year valid. | yes | no | - | | 2 | 2.2.1 | HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE | Check: warns if a file with zero or >1 classes doesn't have a KDoc header. | no | no | | | 2 | 2.2.1 | HEADER_NOT_BEFORE_PACKAGE | Check: warns if a KDoc file header is located not before a package directive.
Fix: moves this KDoc | yes | no | | | 2 | 2.3.1 | KDOC_TRIVIAL_KDOC_ON_FUNCTION | Check: warns if KDoc contains a single line with words 'return', 'get' or 'set'. | no | no | | | 2 | 2.4.1 | COMMENT_WHITE_SPACE | Check: warns if there is no space between // and comment, and if there is no space between the code and the comment
.Fix: adds a white space. | yes | maxSpaces | - | | 2 | 2.4.1 | WRONG_NEWLINES_AROUND_KDOC | Check: warns if there is no new line above and under the comment. Exception on the first comment.
Fix: adds a new line. | yes | no | - | | 2 | 2.4.1 | FIRST_COMMENT_NO_BLANK_LINE | Check: warns if there is a new line before the first comment.
Fix: deletes a new line. | yes | no | - | | 2 | 2.4.1 | IF_ELSE_COMMENTS | Check: warns if there is a comment outside of the else block.
Fix: adds the comment to the first line in else block. | yes | no | - | | 2 | 2.4.2 | COMMENTED_OUT_CODE | Check: warns if the commented code is detected (when uncommented, can be parsed). | no | no | Offset is lost when the joined EOL comments are split again. | | 3 | 3.1.1 | FILE_IS_TOO_LONG | Check: warns if the file has too many lines.
Fix: no | no | maxSize | - | | 1 | 3.1.2 | FILE_NAME_MATCH_CLASS | Check: warns
Fix: no | no | no | Probably, it can be agressively autofixed. | | 3 | 3.1.2 | FILE_CONTAINS_ONLY_COMMENTS | Check: warns if the file contains only comments, imports, and package directive. | no | no | - | | 3 | 3.1.2 | FILE_INCORRECT_BLOCKS_ORDER | Check: warns if the general order of code parts is wrong.
Fix: rearranges parts of code. | yes | no | handle other elements that could be present before the package directive (other comments). | | 3 | 3.1.2 | FILE_NO_BLANK_LINE_BETWEEN_BLOCKS | Check: warns if there is not exactly one blank line between the code parts.
Fix: leaves a single empty line. | yes | no | - | | 3 | 3.1.2 | FILE_UNORDERED_IMPORTS | Check: warns if the imports are not sorted alphabetically, or there are empty lines among them.
Fix: reorders imports. | yes | no | - | | 3 | 3.1.2 | FILE_WILDCARD_IMPORTS | Check: warns if the wildcard imports are used except the cases when they are allowed. | no | allowedWildcards | - | | 3 | 3.1.2 | UNUSED_IMPORT | Check: warns if an import is not used. | no | deleteUnusedImport | - | | 3 | 3.1.4 | WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES | Check: warns if the declaration part of a class, such as a code structure (class, interface, etc.) is not in the proper order.
Fix: restores the order according to the code style guide. | yes | no | - | | 3 | 3.1.4 | BLANK_LINE_BETWEEN_PROPERTIES | Check: warns if properties with comments are not separated by a blank line.
Fix: fixes the number of blank lines. | yes | no | - | | 3 | 3.1.4 | WRONG_DECLARATIONS_ORDER | Check: if the order of enum values or constant properties inside the companion is not correct. | yes | no | - | | 3 | 3.1.5 | TOP_LEVEL_ORDER | Check: warns if the top level order is incorrect. | yes | no | - | | 3 | 3.2.1 | NO_BRACES_IN_CONDITIONALS_AND_LOOPS | Check: warns if braces are not used in `if`, `else`, `when`, `for`, `do`, and `while` statements. Exception: single line if statement (ternary operator).
Fix: adds missing braces. | yes | no | - | | 3 | 3.2.2 | BRACES_BLOCK_STRUCTURE_ERROR | Check: warns if non-empty code blocks with braces do not follow the K&R style (1TBS or OTBS style). | yes | openBraceNewline closeBraceNewline | - | | 3 | 3.3.1 | WRONG_INDENTATION | Check: warns if an indentation is incorrect.
Fix: corrects the indentation.

Basic cases are covered currently. | yes | extendedIndentOfParameters
alignedParameters
extendedIndentForExpressionBodies
extendedIndentAfterOperators
extendedIndentBeforeDot
indentationSize | - | | 3 | 3.4.1 | EMPTY_BLOCK_STRUCTURE_ERROR | Check: warns if an empty block exists or if its style is incorrect. | yes | allowEmptyBlocks styleEmptyBlockWithNewline | - | | 3 | 3.5.1 | LONG_LINE | Check: warns if the length doesn't exceed the specified length. | no | lineLength | Handle json method in KDoc. | | 3 | 3.6.1 | MORE_THAN_ONE_STATEMENT_PER_LINE | Check: warns if there is more than one statement per line. | yes | no | - | | 3 | 3.6.2 | REDUNDANT_SEMICOLON | Check: warns if semicolons are used at the end of a line.
Fix: removes the semicolon. | yes | no | - | | 3 | 3.6.2 | WRONG_NEWLINES | Check: warns if line breaks do not follow the code style guide.
Fix: fixes incorrect line breaks. | yes | no | - | | 3 | 3.6.2 | COMPLEX_EXPRESSION | Check: warns if a long dot-qualified expression is used in a condition or as an argument. | no | no | - | | 3 | 3.6.2 | TRAILING_COMMA | Check: warns if a trailing comma is missing. | yes | valueArgument valueParameter indices whenConditions collectionLiteral typeArgument typeParameter destructuringDeclaration | - | | 3 | 3.7.1 | TOO_MANY_BLANK_LINES | Check: warns if blank lines are used or placed incorrectly.
Fix: removes redundant blank lines. | yes | no | | | 3 | 3.8.1 | WRONG_WHITESPACE | Check: warns if the usage of horizontal spaces violates the code style guide.
Fix: fixes incorrect whitespaces. | yes | no | - | | 3 | 3.8.1 | TOO_MANY_CONSECUTIVE_SPACES | Check: warns if there are too many consecutive spaces in a line. Exception: in an enum if there is a configured and single eol comment.
Fix: squeezes spaces to 1. | yes | maxSpaces saveInitialFormattingForEnums | - | | 3 | 3.9.1 | ENUMS_SEPARATED | Check: warns if an enum structure is incorrect: the enum entries should be separated by a comma and a line break, and the last entry should have a semicolon in the end. | yes | no | Replace variable to enum if it possible. | | 3 | 3.10.2 | LOCAL_VARIABLE_EARLY_DECLARATION | Check: warns if a local variable is declared not immediately before its usage.
Fix (not implemented yet): moves the variable declaration. | no | no | add auto fix | | 3 | 3.11.1 | WHEN_WITHOUT_ELSE | Check: warns if a `when` statement does not have `else` in the end.
Fix: adds `else` when a statement doesn't have it. | yes | no | - | If a `when` statement of the enum or sealed type contains all values of the enum, there is no need to have the "else" branch. | 3 | 3.12.1 | ANNOTATION_NEW_LINE | Check: warns if an annotation is not on a new single line. | yes | no | - | | 3 | 3.12.2 | PREVIEW_ANNOTATION | Check: warns if method, annotated with `@Preview` is not private or has no `Preview` suffix. | yes | no | - | | 3 | 3.14.1 | WRONG_MULTIPLE_MODIFIERS_ORDER | Check: warns if the multiple modifiers in the sequence are in the wrong order. Value identifier supported in Kotlin 1.5 | yes | no | - | | 3 | 3.14.2 | LONG_NUMERICAL_VALUES_SEPARATED | Check: warns if the value of the integer or float constant is too big. | no | maxNumberLength maxBlockLength | - | | 3 | 3.14.3 | MAGIC_NUMBER | Check: warns if there are magic numbers in the code. | no | ignoreNumbers, ignoreHashCodeFunction, ignorePropertyDeclaration, ignoreLocalVariableDeclaration, ignoreConstantDeclaration, ignoreCompanionObjectPropertyDeclaration, ignoreEnums, ignoreRanges, ignoreExtensionFunctions | no | | 3 | 3.15.1 | STRING_CONCATENATION | Check: warns if the concatenation of strings is used in a single line. | yes | no | - | | 3 | 3.15.2 | STRING_TEMPLATE_CURLY_BRACES | Check: warns if there are redundant curly braces in the string template.
Fix: deletes the curly braces. | yes | no | + | | 3 | 3.15.2 | STRING_TEMPLATE_QUOTES | Check: warns if there are redundant quotes in the string template.
Fix: deletes the quotes and $ symbol. | yes | no | + | | 3 | 3.16.1 | COLLAPSE_IF_STATEMENTS | Check: warns if there are redundant nested if-statements, which could be collapsed into a single statement by concatenating their conditions. | yes | no | - | | 3 | 3.16.2 | COMPLEX_BOOLEAN_EXPRESSION | Check: warns if the boolean expression is complex and can be simplified.
Fix: replaces the boolean expression with a simpler one. | yes | no | + | | 3 | 3.17.1 | CONVENTIONAL_RANGE | Check: warns if it is possible to replace the range with `until` or replace the `rangeTo` function with a range.
Fix: replace range with `until` or replace the `rangeTo` function with a range. | yes | no | - | | 3 | 3.18.1 | DEBUG_PRINT | Check: warns if there is a printing to console (assumption that it's a debug logging). | no | no | - | | 4 | 4.1.1 | FLOAT_IN_ACCURATE_CALCULATIONS | Checks that floating-point values are not used in the arithmetic expressions.
Fix: no | no | no | Current implementation detects only floating-point constants. | | 4 | 4.1.3 | SAY_NO_TO_VAR | Check: warns if the `var` modifier is used for a local variable (not in a class or at file level), and this var is not used in accumulators. | no | no | no | Several fixmes related to the search mechanism (VariablesSearch) and fixme for checking reassinment of this var | 4 | 4.2.1 | SMART_CAST_NEEDED | Check: warns if the casting can be omitted.
Fix: Deletes casting. | yes | no | - | | 4 | 4.2.2 | TYPE_ALIAS | Check: if the type reference of a property is longer than expected. | yes | typeReferenceLength | - | | | 4 | 4.3.1 | NULLABLE_PROPERTY_TYPE | Check: warns if an immutable property is initialized with null, or if the immutable property can have non-nullable type instead of nullable.
Fix: suggests the initial value instead of null or changes in the immutable property type. | yes | no | - | | 4 | 4.3.2 | GENERIC_VARIABLE_WRONG_DECLARATION | Check: warns if variables of generic types don't have an explicit type declaration.
Fix: fixes only the variables that have a generic declaration on both sides. | yes | no | + | | 4 | 4.3.3 | AVOID_NULL_CHECKS | Check: warns if the null-check is used explicitly (for example: if (a == null)). | yes | no | Fix if\else conditions on null. | | 5 | 5.1.1 | TOO_LONG_FUNCTION | Check: warns if the length of a function is too long. | no | maxFunctionLength isIncludeHeader | | | 5 | 5.1.2 | NESTED_BLOCK | Warns if a function has more nested blocks than expected. | no | maxNestedBlockQuantit | | | 5 | 5.1.3 | AVOID_NESTED_FUNCTIONS | Check: Warns if there are nested functions.
Fix: declare the function in the outer scope. | yes | no | + | | 5 | 5.1.4 | INVERSE_FUNCTION_PREFERRED | Check: Warns if a function call with "!" can be rewritten to the inverse function (!isEmpty() -> isNotEmpty()).
Fix: Rewrites the function call. | yes | no | - | | 5 | 5.2.1 | LAMBDA_IS_NOT_LAST_PARAMETER | Checks that the lambda inside function parameters block is not at the end. | no | no | | | 5 | 5.2.2 | TOO_MANY_PARAMETERS | Check: Warns if a function contains more parameters than allowed. | no | maxParameterListSize | | | 5 | 5.2.3 | WRONG_OVERLOADING_FUNCTION_ARGUMENTS | Check: Warns if a function has overloading instead of using default arguments. | no | no | | | 5 | 5.2.4 | RUN_BLOCKING_INSIDE_ASYNC | Check: Warns if the runBlocking is used inside the async block code. | no | no | - | | 5 | 5.2.5 | TOO_MANY_LINES_IN_LAMBDA | Checks that the long lambda has parameters. | no | maxLambdaLength | | | 5 | 5.2.6 | CUSTOM_LABEL | Check: Warns if using an unnecessary custom label. | no | no | - | | 5 | 5.2.7 | PARAMETER_NAME_IN_OUTER_LAMBDA | Check: warns if the outer lambda uses an implicit parameter `it`. | no | no | - | | 6 | 6.1.1 | SINGLE_CONSTRUCTOR_SHOULD_BE_PRIMARY | Check: warns if there is only one secondary constructor in a class.
Fix: converts it to a primary constructor. | yes | no | Support the more complicated logic of the constructor conversion. | | 6 | 6.1.2 | USE_DATA_CLASS | Check: if the class can be made as a data class. | no | no | yes | | 6 | 6.1.3 | EMPTY_PRIMARY_CONSTRUCTOR | Check: warns if there is an empty primary constructor. | yes | no | yes | | 6 | 6.1.4 | MULTIPLE_INIT_BLOCKS | Checks that the classes have only one init block. | yes | no | - | | 6 | 6.1.5 | USELESS_SUPERTYPE | Checks if the override function can be removed. | yes | no | | | 6 | 6.1.6 | CLASS_SHOULD_NOT_BE_ABSTRACT | Checks if the abstract class has any abstract method. If not, it warns that the class must not be abstract.
Fix: deletes the abstract modifier. | yes | no | - | | 6 | 6.1.7 | NO_CORRESPONDING_PROPERTY | Checks: warns if with using "backing property" scheme, the name of a real and a back property are the same. | no | no | - | | 6 | 6.1.8 | CUSTOM_GETTERS_SETTERS | Check that no custom getters and setters are used for the properties. | no | no | - | | 6 | 6.1.9 | WRONG_NAME_OF_VARIABLE_INSIDE_ACCESSOR | Checks if the name of a variable is used in the custom getter or setter. | no | no | | | 6 | 6.1.10 | TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED | Checks if there are trivial getters or setters.
Fix: Delete the trivial getter or setter. | yes | no | - | | 6 | 6.1.11 | COMPACT_OBJECT_INITIALIZATION | Checks if the class instantiation can be wrapped in `apply` for better readability. | yes | no | | | 6 | 6.1.12 | INLINE_CLASS_CAN_BE_USED | Check: warns if the class can be transferred to the inline class. | no | no | yes | | 6 | 6.2.2 | EXTENSION_FUNCTION_SAME_SIGNATURE | Checks if an extension function has the same signature as another extension function, and their classes are related. | no | no | + | | 6 | 6.2.3 | EXTENSION_FUNCTION_WITH_CLASS | Check: if the file contains a class, then it can not have extension functions for the same class. | no | no | - | | 6 | 6.2.4 | USE_LAST_INDEX | Check: change a property length - 1 to the property lastIndex. | no | no | - | | 6 | 6.4.1 | AVOID_USING_UTILITY_CLASS | Checks if there is a class/object that can be replaced with the extension function. | no | no | - | | 6 | 6.4.2 | OBJECT_IS_PREFERRED | Check: if class is stateless, then it is preferred to use `object.` | yes | no | + | | 6 | 6.5.1 | RUN_IN_SCRIPT | Checks : if the kts script contains other functions except the run code. | yes | no | - | ================================================ FILE: info/build.gradle.kts ================================================ import com.saveourtool.diktat.generation.docs.generateAvailableRules import com.saveourtool.diktat.generation.docs.generateCodeStyle import com.saveourtool.diktat.generation.docs.generateFullDoc import com.saveourtool.diktat.generation.docs.generateRulesMapping tasks.register("generateRulesMapping") { group = "documentation" description = "Generates a table (rules-mapping.md), which maps warning names to chapters in code style" doFirst { generateRulesMapping() } } tasks.register("generateFullDoc") { group = "documentation" description = "Compile individual chapters into a single markdown document" doFirst { generateFullDoc(file("$rootDir/guide"), "diktat-coding-convention.md") } } tasks.register("generateAvailableRules") { group = "documentation" description = "Generate table for White paper based on available-rules.md and rules-mapping.md" dependsOn("generateRulesMapping") doFirst { generateAvailableRules(rootDir, file("$rootDir/../wp")) } } tasks.register("generateCodeStyle") { group = "documentation" description = "Adds/updates diktat code style in white paper document" dependsOn("generateFullDoc") doFirst { generateCodeStyle(file("$rootDir/guide"), file("$rootDir/../wp")) } } tasks.register("updateMarkdownDocumentation") { group = "documentation" description = "Task that aggregates all documentation updates without white paper updates" dependsOn( "generateRulesMapping", "generateFullDoc" ) } tasks.register("updateDocumentation") { group = "documentation" description = "Task that aggregates all documentation updates" dependsOn( "generateRulesMapping", "generateAvailableRules", "generateFullDoc", "generateCodeStyle" ) } ================================================ FILE: info/buildSrc/build.gradle.kts ================================================ plugins { `kotlin-dsl` } repositories { mavenCentral() mavenLocal { content { includeGroup("com.saveourtool.diktat") } } flatDir { dirs( "$rootDir/../../diktat-rules/target", "$rootDir/../../diktat-common/target" ) } } dependencies { implementation("com.saveourtool.diktat:diktat-rules:$version") } ================================================ FILE: info/buildSrc/gradle.properties ================================================ version=1.2.6-SNAPSHOT ================================================ FILE: info/buildSrc/src/main/kotlin/com/saveourtool/diktat/generation/docs/FullDocGenerator.kt ================================================ package com.saveourtool.diktat.generation.docs import java.io.File /** * Compiles individual chapters of code style into a single document. */ fun generateFullDoc(guideDir: File, fullDocFileName: String) { val GUIDE_PATTERN = "guide-chapter-" val codeGuideParts = guideDir .listFiles()!! .filter { it.name.contains(GUIDE_PATTERN) } val allChapters = codeGuideParts .sortedBy { it.nameWithoutExtension.replace(GUIDE_PATTERN, "").toInt() } .map { it.readLines() } .flatten() .joinToString("\n") val tableOfContent = File(guideDir, "guide-TOC.md") .readText() File(guideDir, fullDocFileName).writeText("$tableOfContent\n\n$allChapters") } ================================================ FILE: info/buildSrc/src/main/kotlin/com/saveourtool/diktat/generation/docs/GenerationAvailableRules.kt ================================================ package com.saveourtool.diktat.generation.docs import java.io.File /** * This function parses available-rules.md and rules-mapping.md files to automatically generate table for White paper */ @Suppress("MagicNumber", "UnsafeCallOnNullableType") fun generateAvailableRules(rootDir: File, wpDir: File) { val ruleMap = File(rootDir, "rules-mapping.md").readLines() .drop(2) .map { it.drop(1).dropLast(1).split("|") } .map { RuleDescription(it[0].replace("\\s+".toRegex(), ""), it[1], it[2]) } .associateBy { it.ruleName } File(rootDir, "available-rules.md").readLines() .drop(2) .map { it.drop(1).dropLast(1).split("|") } .map { it[2].replace("\\s+".toRegex(), "") to it[5] } .forEach { ruleMap[it.first]!!.config = it.second} val newText = File(wpDir, "sections/appendix.tex").readLines().toMutableList() newText.removeAll(newText.subList(newText.indexOf("\\section*{Available Rules}") + 1, newText.indexOf("%CodeStyle"))) var index = newText.indexOf("\\section*{Available Rules}") + 1 AUTO_TABLE.trimIndent().lines().forEach { newText.add(index++, it) } ruleMap.map { it.value } .map { "${it.correctRuleName} & ${it.correctCodeStyle} & ${it.autoFix} & ${it.config.replace("
", " ")}\\\\" } .forEach { newText.add(index++, it) } AUTO_END.trimIndent().split("\n").forEach { newText.add(index++, it) } File(wpDir, "sections/appendix.tex").writeText(newText.joinToString(separator = "\n")) } /** * Data class for rule and it's description * * @property ruleName rule's name * @property codeStyle rule's description * @property autoFix is rule can fix */ @Suppress("UnsafeCallOnNullableType") data class RuleDescription(val ruleName: String, val codeStyle: String, val autoFix: String) { /** * Remove square brackets from code style */ val correctCodeStyle = codeStyle.substring(codeStyle.indexOf("[") + 1, codeStyle.indexOf("]")).run { "\\hyperref[sec:$this]{$this}" } /** * Replace space between words with underline for Latex */ val correctRuleName = ruleName.replace("_", "\\underline{ }") /** * Parse correctly configuration for Latex */ lateinit var config: String } ================================================ FILE: info/buildSrc/src/main/kotlin/com/saveourtool/diktat/generation/docs/GenerationDocs.kt ================================================ @file:Suppress("FILE_NAME_MATCH_CLASS") package com.saveourtool.diktat.generation.docs import java.io.File import java.io.PrintWriter /** * Adds/updates diktat code style in white paper document. */ @Suppress( "LoopWithTooManyJumpStatements", "LongMethod", "MagicNumber", "ComplexMethod", "NestedBlockDepth", "TOO_LONG_FUNCTION") fun generateCodeStyle(guideDir: File, wpDir: File) { val file = File(guideDir, "diktat-coding-convention.md") val tempFile = File(wpDir, "convention.tex") val lines = file.readLines().toMutableList().drop(1) tempFile.printWriter().use { writer -> val iterator = lines.iterator() writer.writeWithoutApostrophe("%CodeStyle") while (iterator.hasNext()) { val line = iterator.next() if (line.contains("##
Preface")) break when { line.startsWith("#") -> { writer.writeWithoutApostrophe("\\section*{${line.removePrefix("#").trim()}}") } line.startsWith("*") -> { writeTableContentLine(writer, line, 0.5) } line.startsWith(" ") -> { writeTableContentLine(writer, line, 1.0) } else -> { writeTableContentLine(writer, line, 0.0) } } } while (iterator.hasNext()) { var line = iterator.next() if (line.contains(" ### Purpose of this document The purpose of this document is to provide a specification that software developers could reference to enhance their ability to write consistent, easy-to-read, and high-quality code. Such a specification will ultimately improve software development efficiency and product competitiveness. For the code to be considered high-quality, it must entail the following characteristics: 1. Simplicity 2. Maintainability 3. Reliability 4. Testability 5. Efficiency 6. Portability 7. Reusability ### General principles Like other modern programming languages, Kotlin is an advanced programming language that complies with the following general principles: 1. Clarity — a necessary feature of programs that are easy to maintain and refactor. 2. Simplicity — a code is easy to understand and implement. 3. Consistency — enables a code to be easily modified, reviewed, and understood by the team members. Unification is particularly important when the same team works on the same project, utilizing similar styles enabling a code to be easily modified, reviewed, and understood by the team members. Also, we need to consider the following factors when programming on Kotlin: 1. Writing clean and simple Kotlin code Kotlin combines two of the main programming paradigms: functional and object-oriented. Both of these paradigms are trusted and well-known software engineering practices. As a young programming language, Kotlin is built on top of well-established languages such as Java, C++, C#, and Scala. This enables Kotlin to introduce many features that help a developer write cleaner, more readable code while also reducing the number of complex code structures. For example, type and null safety, extension functions, infix syntax, immutability, val/var differentiation, expression-oriented features, "when" statements, much easier work with collections, type auto conversion, and other syntactic sugar. 2. Following Kotlin idioms The author of Kotlin, Andrey Breslav, mentioned that Kotlin is both pragmatic and practical, but not academic. Its pragmatic features enable ideas to be transformed into real working software easily. Kotlin is closer to natural languages than its predecessors, and it implements the following design principles: readability, reusability, interoperability, security, and tool-friendliness (https://blog.jetbrains.com/kotlin/2018/10/kotlinconf-2018-announcements/). 3. Using Kotlin efficiently Some Kotlin features can help you to write higher-performance code: including rich coroutine library, sequences, inline functions/classes, arrays of basic types, tailRec, and CallsInPlace of contract. ### Terminology **Rules** — conventions that should be followed when programming. **Recommendations** — conventions that should be considered when programming. **Explanation** — necessary explanations of rules and recommendations. **Valid Example** — recommended examples of rules and recommendations. **Invalid Example** — not recommended examples of rules and recommendations. Unless otherwise stated, this specification applies to versions 1.3 and later of Kotlin. ### Exceptions Even though exceptions may exist, it is essential to understand why rules and recommendations are needed. Depending on a project situation or personal habits, you can break some of the rules. However, remember that one exception may lead to many and eventually can destroy code consistency. As such, there should be very few exceptions. When modifying open-source code or third-party code, you can choose to use the code style from this open-source project (instead of using the existing specifications) to maintain consistency. Software that is directly based on the Android native operating system interface, such as the Android Framework, remains consistent with the Android style. # 1. Naming In programming, it is not always easy to meaningfully and appropriately name variables, functions, classes, etc. Using meaningful names helps to clearly express your code's main ideas and functionality and avoid misinterpretation, unnecessary coding and decoding, "magic" numbers, and inappropriate abbreviations. Note: The source file encoding format (including comments) must be UTF-8 only. The ASCII horizontal space character (0x20, that is, space) is the only permitted whitespace character. Tabs should not be used for indentation. ### 1.1 Identifiers This section describes the general rules for naming identifiers. #### 1.1.1 Identifiers naming conventions For identifiers, use the following naming conventions: 1. All identifiers should use only ASCII letters or digits, and the names should match regular expressions `\w{2,64}`. Explanation: Each valid identifier name should match the regular expression `\w{2,64}`. `{2,64}` means that the name length is 2 to 64 characters, and the length of the variable name should be proportional to its life range, functionality, and responsibility. Name lengths of less than 31 characters are generally recommended. However, this depends on the project. Otherwise, a class declaration with generics or inheritance from a superclass can cause line breaking. No special prefix or suffix should be used in names. The following examples are inappropriate names: name_, mName, s_name, and kName. 2. Choose file names that would describe the content. Use camel case (PascalCase) and `.kt` extension. 3. Typical examples of naming: | Meaning | Correct |Incorrect| | ---- | ---- | ---- | | "XML Http Request" | XmlHttpRequest | XMLHTTPRequest | | "new customer ID" | newCustomerId | newCustomerID | | "inner stopwatch" | innerStopwatch | innerStopWatch | | "supports IPv6 on iOS" | supportsIpv6OnIos | supportsIPv6OnIOS | | "YouTube importer" | YouTubeImporter | YoutubeImporter | 4. The usage of (``) and free naming for functions and identifiers are prohibited. For example, the following code is not recommended: ```kotlin val `my dummy name-with-minus` = "value" ``` The only exception is function names in `Unit tests.` 5. Backticks (``) should not be used for identifiers, except the names of test methods (marked with @Test annotation): ```kotlin @Test fun `my test`() { /*...*/ } ``` 6. The following table contains some characters that may cause confusion. Be careful when using them as identifiers. To avoid issues, use other names instead. | Expected | Confusing name | Suggested name | | ------------- | ------------------------ | ---------------- | | 0 (zero) | O, D | obj, dgt | | 1 (one) | I, l | it, ln, line | | 2 (two) | Z | n1, n2 | | 5 (five) | S | xs, str | | 6 (six) | e | ex, elm | | 8 (eight) | B | bt, nxt | | n,h | h,n | nr, head, height | | rn, m | m,rn | mbr, item | **Exceptions:** - The i,j,k variables used in loops are part of the industry standard. One symbol can be used for such variables. - The `e` variable can be used to catch exceptions in catch block: `catch (e: Exception) {}` - The Java community generally does not recommend the use of prefixes. However, when developing Android code, you can use the s and m prefixes for static and non-public non-static fields, respectively. Note that prefixing can also negatively affect the style and the auto-generation of getters and setters. | Type | Naming style | | ---- | ---- | | Interfaces, classes, annotations, enumerated types, and object type names | Camel case, starting with a capital letter. Test classes have a Test suffix. The filename is 'TopClassName'.kt. | | Class fields, local variables, methods, and method parameters | Camel case starting with a low case letter. Test methods can be underlined with '_'; the only exception is [backing properties](#r6.1.7). | Static constants and enumerated values | Only uppercase underlined with '_' | | Generic type variable | Single capital letter, which can be followed by a number, for example: `E, T, U, X, T2` | | Exceptions | Same as class names, but with a suffix Exception, for example: `AccessException` and `NullPointerException`| ### 1.2 Packages #### Rule 1.2.1 Package names dots Package names are in lower case and separated by dots. Code developed within your company should start with `your.company.domain.` Numbers are permitted in package names. Each file should have a `package` directive. Package names are all written in lowercase, and consecutive words are concatenated together (no underscores). Package names should contain both the product or module names and the department (or team) name to prevent conflicts with other teams. Numbers are not permitted. For example: `org.apache.commons.lang3`, `xxx.yyy.v2`. **Exceptions:** - In certain cases, such as open-source projects or commercial cooperation, package names should not start with `your.company.domain.` - If the package name starts with a number or other character that cannot be used at the beginning of the Java/Kotlin package name, then underscores are allowed. For example: `com.example._123name`. - Underscores are sometimes permitted if the package name contains reserved Java/Kotlin keywords, such as `org.example.hyphenated_name`, `int_.example`. **Valid example**: ```kotlin package your.company.domain.mobilecontrol.views ``` ### 1.3 Classes, enumerations, typealias, interfaces This section describes the general rules for naming classes, enumerations, and interfaces. ### 1.3.1 Classes, enumerations, typealias, interface names use Camel case Classes, enumerations, and interface names use `UpperCamelCase` nomenclature. Follow the naming rules described below: 1. A class name is usually a noun (or a noun phrase) denoted using the camel case nomenclature, such as UpperCamelCase. For example: `Character` or `ImmutableList`. An interface name can also be a noun or noun phrase (such as `List`) or an adjective or adjective phrase (such as `Readable`). Note that verbs are not used to name classes. However, nouns (such as `Customer`, `WikiPage`, and `Account`) can be used. Try to avoid using vague words such as `Manager` and `Process`. 2. Test classes start with the name of the class they are testing and end with 'Test'. For example, `HashTest` or `HashIntegrationTest`. **Invalid example**: ```kotlin class marcoPolo {} class XMLService {} interface TAPromotion {} class info {} ``` **Valid example**: ```kotlin class MarcoPolo {} class XmlService {} interface TaPromotion {} class Order {} ``` ### 1.4 Functions This section describes the general rules for naming functions. ### 1.4.1 Function names should be in camel case Function names should use `lowerCamelCase` nomenclature. Follow the naming rules described below: 1. Function names are usually verbs or verb phrases denoted with the camel case nomenclature (`lowerCamelCase`). For example: `sendMessage`, `stopProcess`, or `calculateValue`. To name functions, use the following formatting rules: a) To get, modify, or calculate a certain value: get + non-boolean field(). Note that the Kotlin compiler automatically generates getters for some classes, applying the special syntax preferred for the 'get' fields: kotlin private val field: String get() { }. kotlin private val field: String get() { }. ```kotlin private val field: String get() { } ``` Note: The calling property access syntax is preferred to call getter directly. In this case, the Kotlin compiler automatically calls the corresponding getter. b) `is` + boolean variable name() c) `set` + field/attribute name(). However, note that the syntax and code generation for Kotlin are completely the same as those described for the getters in point a. d) `has` + Noun / adjective () e) verb() Note: Note: Verb are primarily used for the action objects, such as `document.print ()` f) verb + noun() g) The Callback function allows the names that use the preposition + verb format, such as: `onCreate()`, `onDestroy()`, `toString()`. **Invalid example**: ```kotlin fun type(): String fun Finished(): Boolean fun visible(boolean) fun DRAW() fun KeyListener(Listener) ``` **Valid example**: ```kotlin fun getType(): String fun isFinished(): Boolean fun setVisible(boolean) fun draw() fun addKeyListener(Listener) ``` 2. An underscore (`_`) can be included in the JUnit test function name and should be used as a separator. Each logical part is denoted in `lowerCamelCase`, for example, a typical pattern of using underscore: `pop_emptyStack`. ### 1.5 Constants This section describes the general rules for naming constraints. ### 1.5.1 Using UPPER case and underscore characters in a constraint name Constant names should be in UPPER case, words separated by underscore. The general constant naming conventions are listed below: 1. Constants are attributes created with the `const` keyword or top-level/`val` local variables of an object that holds immutable data. In most cases, constants can be identified as a `const val` property from the `object`/`companion object`/file top level. These variables contain fixed constant values that typically should never be changed by programmers. This includes basic types, strings, immutable types, and immutable collections of immutable types. The value is not constant for the object, which state can be changed. 2. Constant names should contain only uppercase letters separated by an underscores. They should have a val or const val modifier to make them final explicitly. In most cases, if you need to specify a constant value, then you need to create it with the "const val" modifier. Note that not all `val` variables are constants. 3. Objects with immutable content, such as `Logger` and `Lock`, can be in uppercase as constants or have camel case as regular variables. 4. Use meaningful constants instead of `magic numbers`. SQL or logging strings should not be treated as magic numbers, nor should they be defined as string constants. Magic constants, such as `NUM_FIVE = 5` or `NUM_5 = 5` should not be treated as constants. This is because mistakes will easily be made if they are changed to `NUM_5 = 50` or 55. These constants typically represent business logic values, such as measures, capacity, scope, location, tax rate, promotional discounts, and power base multiples in algorithms. You can avoid using magic numbers with the following method: - Using library functions and APIs. For example, instead of checking that `size == 0`, use `isEmpty()` function. To work with `time`, use built-ins from `java.time API`. - Enumerations can be used to name patterns. Refer to [Recommended usage scenario for enumeration in 3.9](#c3.9). **Invalid example**: ```kotlin var int MAXUSERNUM = 200; val String sL = "Launcher"; ``` **Valid example**: ```kotlin const val int MAX_USER_NUM = 200; const val String APPLICATION_NAME = "Launcher"; ``` ### 1.6 Non-constant fields (variables) This section describes the general rules for naming variables. ### 1.6.1 Non-constant field name Non-constant field names should use camel case and start with a lowercase letter. A local variable cannot be treated as constant even if it is final and immutable. Therefore, it should not use the preceding rules. Names of collection type variables (sets, lists, etc.) should contain plural nouns. For example: `var namesList: List` Names of non-constant variables should use `lowerCamelCase`. The name of the final immutable field used to store the singleton object can use the same camel case notation. **Invalid example**: ```kotlin customername: String user: List = listof() ``` **Valid example**: ```kotlin var customerName: String val users: List = listOf(); val mutableCollection: MutableSet = HashSet() ``` ### 1.6.2 Boolean variable names with negative meaning Avoid using Boolean variable names with a negative meaning. When using a logical operator and name with a negative meaning, the code may be difficult to understand, which is referred to as the "double negative". For instance, it is not easy to understand the meaning of !isNotError. The JavaBeans specification automatically generates isXxx() getters for attributes of Boolean classes. However, not all methods returning Boolean type have this notation. For Boolean local variables or methods, it is highly recommended that you add non-meaningful prefixes, including is (commonly used by JavaBeans), has, can, should, and must. Modern integrated development environments (IDEs) such as Intellij are already capable of doing this for you when you generate getters in Java. For Kotlin, this process is even more straightforward as everything is on the byte-code level under the hood. **Invalid example**: ```kotlin val isNoError: Boolean val isNotFound: Boolean fun empty() fun next(); ``` **Valid example**: ```kotlin val isError: Boolean val isFound: Boolean val hasLicense: Boolean val canEvaluate: Boolean val shouldAbort: Boolean fun isEmpty() fun hasNext() ``` # 2. Comments The best practice is to begin your code with a summary, which can be one sentence. Try to balance between writing no comments at all and obvious commentary statements for each line of code. Comments should be accurately and clearly expressed, without repeating the name of the class, interface, or method. Comments are not a solution to the wrong code. Instead, you should fix the code as soon as you notice an issue or plan to fix it (by entering a TODO comment, including a Jira number). Comments should accurately reflect the code's design ideas and logic and further describe its business logic. As a result, other programmers will be able to save time when trying to understand the code. Imagine that you are writing the comments to help yourself to understand the original ideas behind the code in the future. ### 2.1 General form of Kdoc KDoc is a combination of JavaDoc's block tags syntax (extended to support specific constructions of Kotlin) and Markdown's inline markup. The basic format of KDoc is shown in the following example: ```kotlin /** * There are multiple lines of KDoc text, * Other ... */ fun method(arg: String) { // ... } ``` It is also shown in the following single-line form: ```kotlin /** Short form of KDoc. */ ``` Use a single-line form when you store the entire KDoc block in one line (and there is no KDoc mark @XXX). For detailed instructions on how to use KDoc, refer to [Official Document](https://docs.oracle.com/en/Kotlin/Kotlinse/11/tools/KDoc.html). #### 2.1.1 Using KDoc for the public, protected, or internal code elements At a minimum, KDoc should be used for every public, protected, or internal decorated class, interface, enumeration, method, and member field (property). Other code blocks can also have KDocs if needed. Instead of using comments or KDocs before properties in the primary constructor of a class - use `@property` tag in a KDoc of a class. All properties of the primary constructor should also be documented in a KDoc with a `@property` tag. **Incorrect example:** ```kotlin /** * Class description */ class Example( /** * property description */ val foo: Foo, // another property description val bar: Bar ) ``` **Correct example:** ```kotlin /** * Class description * @property foo property description * @property bar another property description */ class Example( val foo: Foo, val bar: Bar ) ``` - Don't use Kdoc comments inside code blocks as block comments **Incorrect Example:** ```kotlin class Example { fun doGood() { /** * wrong place for kdoc */ 1 + 2 } } ``` **Correct Example:** ```kotlin class Example { fun doGood() { /* * right place for block comment */ 1 + 2 } } ``` **Exceptions:** * For setters/getters of properties, obvious comments (like `this getter returns field`) are optional. Note that Kotlin generates simple `get/set` methods under the hood. * It is optional to add comments for simple one-line methods, such as shown in the example below: ```kotlin val isEmpty: Boolean get() = this.size == 0 ``` or ```kotlin fun isEmptyList(list: List) = list.size == 0 ``` **Note:** You can skip KDocs for a method's override if it is almost the same as the superclass method. #### 2.1.2 Describing methods that have arguments, a return value, or can throw an exception in the KDoc block When the method has such details as arguments, return value, or can throw exceptions, it must be described in the KDoc block (with @param, @return, @throws, etc.). **Valid examples:** ```kotlin /** * This is the short overview comment for the example interface. * / * Add a blank line between the comment text and each KDoc tag underneath * / * @since 1.6 */ protected abstract class Sample { /** * This is a long comment with whitespace that should be split in * comments on multiple lines if the line comment formatting is enabled. * / * Add a blank line between the comment text and each KDoc tag underneath * / * @param fox A quick brown fox jumps over the lazy dog * @return battle between fox and dog */ protected abstract fun foo(Fox fox) /** * These possibilities include: Formatting of header comments * / * Add a blank line between the comment text and each KDoc tag underneath * / * @return battle between fox and dog * @throws ProblemException if lazy dog wins */ protected fun bar() throws ProblemException { // Some comments / * No need to add a blank line here * / var aVar = ... // Some comments / * Add a blank line before the comment * / fun doSome() } } ``` #### 2.1.3 Only one space between the Kdoc tag and content. Tags are arranged in the order. There should be only one space between the Kdoc tag and content. Tags are arranged in the following order: @param, @return, and @throws. Therefore, Kdoc should contain the following: - Functional and technical description, explaining the principles, intentions, contracts, API, etc. - The function description and @tags (`implSpec`, `apiNote`, and `implNote`) require an empty line after them. - `@implSpec`: A specification related to API implementation, and it should let the implementer decide whether to override it. - `@apiNote`: Explain the API precautions, including whether to allow null and whether the method is thread-safe, as well as the algorithm complexity, input, and output range, exceptions, etc. - `@implNote`: A note related to API implementation, which implementers should keep in mind. - One empty line, followed by regular `@param`, `@return`, `@throws`, and other comments. - The conventional standard "block labels" are arranged in the following order: `@param`, `@return`, `@throws`. Kdoc should not contain: - Empty descriptions in tag blocks. It is better not to write Kdoc than waste code line space. - There should be no empty lines between the method/class declaration and the end of Kdoc (`*/` symbols). - `@author` tag. It doesn't matter who originally created a class when you can use `git blame` or VCS of your choice to look through the changes history. Important notes: - KDoc does not support the `@deprecated` tag. Instead, use the `@Deprecated` annotation. - The `@since` tag should be used for versions only. Do not use dates in `@since` tag, it's confusing and less accurate. If a tag block cannot be described in one line, indent the content of the new line by *four spaces* from the `@` position to achieve alignment (`@` counts as one + three spaces). **Exception:** When the descriptive text in a tag block is too long to wrap, you can indent the alignment with the descriptive text in the last line. The descriptive text of multiple tags does not need to be aligned. See [3.8 Horizontal space](#c3.8). In Kotlin, compared to Java, you can put several classes inside one file, so each class should have a Kdoc formatted comment (as stated in rule 2.1). This comment should contain @since tag. The right style is to write the application version when its functionality is released. It should be entered after the `@since` tag. **Examples:** ```kotlin /** * Description of functionality * * @since 1.6 */ ``` Other KDoc tags (such as @param type parameters and @see.) can be added as follows: ```kotlin /** * Description of functionality * * @apiNote: Important information about API * * @since 1.6 */ ``` ### 2.2 Adding comments on the file header This section describes the general rules of adding comments on the file header. ### 2.2.1 Formatting of comments in the file header Comments on the file header should be placed before the package name and imports. If you need to add more content to the comment, subsequently add it in the same format. Comments on the file header must include copyright information, without the creation date and author's name (use VCS for history management). Also, describe the content inside files that contain multiple or no classes. The following examples for Huawei describe the format of the *copyright license*: \ Chinese version: `版权所有 (c) 华为技术有限公司 2012-2020` \ English version: `Copyright (c) Huawei Technologies Co., Ltd. 2012-2020. All rights reserved.` `2012` and `2020` are the years the file was first created and the current year, respectively. Do not place **release notes** in header, use VCS to keep track of changes in file. Notable changes can be marked in individual KDocs using `@since` tag with version. Invalid example: ```kotlin /** * Release notes: * 2019-10-11: added class Foo */ class Foo ``` Valid example: ```kotlin /** * @since 2.4.0 */ class Foo ``` - The **copyright statement** can use your company's subsidiaries, as shown in the below examples: \ Chinese version: `版权所有 (c) 海思半导体 2012-2020` \ English version: `Copyright (c) Hisilicon Technologies Co., Ltd. 2012-2020. All rights reserved.` - The copyright information should not be written in KDoc style or use single-line comments. It must start from the beginning of the file. The following example is a copyright statement for Huawei, without other functional comments: ```kotlin /* * Copyright (c) Huawei Technologies Co., Ltd. 2012-2020. All rights reserved. */ ``` The following factors should be considered when writing the file header or comments for top-level classes: - File header comments must start from the top of the file. If it is a top-level file comment, there should be a blank line after the last Kdoc `*/` symbol. If it is a comment for a top-level class, the class declaration should start immediately without using a newline. - Maintain a unified format. The specific format can be formulated by the project (for example, if you use an existing opensource project), and you need to follow it. - A top-level file-Kdoc must include a copyright and functional description, especially if there is more than one top-level class. - Do not include empty comment blocks. If there is no content after the option `@apiNote`, the entire tag block should be deleted. - The industry practice is not to include historical information in the comments. The corresponding history can be found in VCS (git, svn, etc.). Therefore, it is not recommended to include historical data in the comments of the Kotlin source code. ### 2.3 Comments on the function header Comments on the function header are placed above function declarations or definitions. A newline should not exist between a function declaration and its Kdoc. Use the preceding <> style rules. As stated in Chapter 1, the function name should reflect its functionality as much as possible. Therefore, in the Kdoc, try to describe the functionality that is not mentioned in the function name. Avoid unnecessary comments on dummy coding. The function header comment's content is optional, but not limited to function description, return value, performance constraints, usage, memory conventions, algorithm implementation, reentrant requirements, etc. ### 2.4 Code comments This section describes the general rules of adding code comments. #### 2.4.1 Add a blank line between the body of the comment and Kdoc tag-blocks. It is a good practice to add a blank line between the body of the comment and Kdoc tag-blocks. Also, consider the following rules: - There must be one space between the comment character and the content of the comment. - There must be a newline between a Kdoc and the presiding code. - An empty line should not exist between a Kdoc and the code it is describing. You do not need to add a blank line before the first comment in a particular namespace (code block) (for example, between the function declaration and first comment in a function body). **Valid Examples:** ```kotlin /** * This is the short overview comment for the example interface. * * @since 1.6 */ public interface Example { // Some comments /* Since it is the first member definition in this code block, there is no need to add a blank line here */ val aField: String = ... /* Add a blank line above the comment */ // Some comments val bField: String = ... /* Add a blank line above the comment */ /** * This is a long comment with whitespace that should be split in * multiple line comments in case the line comment formatting is enabled. * /* blank line between description and Kdoc tag */ * @param fox A quick brown fox jumps over the lazy dog * @return the rounds of battle of fox and dog */ fun foo(Fox fox) /* Add a blank line above the comment */ /** * These possibilities include: Formatting of header comments * * @return the rounds of battle of fox and dog * @throws ProblemException if lazy dog wins */ fun bar() throws ProblemException { // Some comments /* Since it is the first member definition in this range, there is no need to add a blank line here */ var aVar = ... // Some comments /* Add a blank line above the comment */ fun doSome() } } ``` - Leave one single space between the comment on the right side of the code and the code. If you use conditional comments in the `if-else-if` scenario, put the comments inside the `else-if` branch or in the conditional block, but not before the `else-if`. This makes the code more understandable. When the if-block is used with curly braces, the comment should be placed on the next line after opening the curly braces. Compared to Java, the `if` statement in Kotlin statements returns a value. For this reason, a comment block can describe a whole `if-statement`. **Valid examples:** ```kotlin val foo = 100 // right-side comment val bar = 200 /* right-side comment */ // general comment for the value and whole if-else condition val someVal = if (nr % 15 == 0) { // when nr is a multiple of both 3 and 5 println("fizzbuzz") } else if (nr % 3 == 0) { // when nr is a multiple of 3, but not 5 // We print "fizz", only. println("fizz") } else if (nr % 5 == 0) { // when nr is a multiple of 5, but not 3 // we print "buzz" only. println("buzz") } else { // otherwise, we print the number. println(x) } ``` - Start all comments (including KDoc) with a space after the first symbol (`//`, `/*`, `/**` and `*`) **Valid example:** ```kotlin val x = 0 // this is a comment ``` #### 2.4.2 Do not comment on unused code blocks Do not comment on unused code blocks, including imports. Delete these code blocks immediately. A code is not used to store history. Git, svn, or other VCS tools should be used for this purpose. Unused imports increase the coupling of the code and are not conducive to maintenance. The commented out code cannot be appropriately maintained. In an attempt to reuse the code, there is a high probability that you will introduce defects that are easily missed. The correct approach is to delete the unnecessary code directly and immediately when it is not used anymore. If you need the code again, consider porting or rewriting it as changes could have occurred since you first commented on the code. #### 2.4.3 Code delivered to the client should not contain TODO/FIXME comments The code officially delivered to the client typically should not contain TODO/FIXME comments. `TODO` comments are typically used to describe modification points that need to be improved and added. For example, refactoring FIXME comments are typically used to describe known defects and bugs that will be subsequently fixed and are not critical for an application. They should all have a unified style to facilitate unified text search processing. **Example:** ```kotlin // TODO(): Jira-XXX - support new json format // FIXME: Jira-XXX - fix NPE in this code block ``` At a version development stage, these annotations can be used to highlight the issues in the code, but all of them should be fixed before a new product version is released. # 3. General formatting (typesetting) ### 3.1 File-related rules This section describes the rules related to using files in your code. #### 3.1.1 Avoid files that are too long If the file is too long and complicated, it should be split into smaller files, functions, or modules. Files should not exceed 2000 lines (non-empty and non-commented lines). It is recommended to horizontally or vertically split the file according to responsibilities or hierarchy of its parts. The only exception to this rule is code generation - the auto-generated files that are not manually modified can be longer. #### 3.1.2 Code blocks in the source file should be separated by one blank line A source file contains code blocks in the following order: copyright, package name, imports, and top-level classes. They should be separated by one blank line. a) Code blocks should be in the following order: 1. Kdoc for licensed or copyrighted files 2. `@file` annotation 3. Package name 4. Import statements 5. Top-class header and top-function header comments 6. Top-level classes or functions b) Each of the preceding code blocks should be separated by a blank line. c) Import statements are alphabetically arranged, without using line breaks and wildcards ( wildcard imports - `*`). d) **Recommendation**: One `.kt` source file should contain only one class declaration, and its name should match the filename e) Avoid empty files that do not contain the code or contain only imports/comments/package name f) Unused imports should be removed #### 3.1.3 Import statements order From top to bottom, the order is the following: 1. Android 2. Imports of packages used internally in your organization 3. Imports from other non-core dependencies 4. Java core packages 5. kotlin stdlib Each category should be alphabetically arranged. Each group should be separated by a blank line. This style is compatible with [Android import order](https://source.android.com/setup/contribute/code-style#order-import-statements). **Valid example**: ```kotlin import android.* // android import androidx.* // android import com.android.* // android import com.your.company.* // your company's libs import your.company.* // your company's libs import com.fasterxml.jackson.databind.ObjectMapper // other third-party dependencies import org.junit.jupiter.api.Assertions import java.io.IOException // java core packages import java.net.URL import kotlin.system.exitProcess // kotlin standard library import kotlinx.coroutines.* // official kotlin extension library ``` #### 3.1.4 Order of declaration parts of class-like code structures The declaration parts of class-like code structures (class, interface, etc.) should be in the following order: compile-time constants (for objects), class properties, late-init class properties, init-blocks, constructors, public methods, internal methods, protected methods, private methods, and companion object. Blank lines should separate their declaration. Notes: 1. There should be no blank lines between properties with the following **exceptions**: when there is a comment before a property on a separate line or annotations on a separate line. 2. Properties with comments/Kdoc should be separated by a newline before the comment/Kdoc. 3. Enum entries and constant properties (`const val`) in companion objects should be alphabetically arranged. The declaration part of a class or interface should be in the following order: - Compile-time constants (for objects) - Properties - Late-init class properties - Init-blocks - Constructors - Methods or nested classes. Put nested classes next to the code they are used by. If the classes are meant to be used externally, and are not referenced inside the class, put them after the companion object. - Companion object **Exception:** All variants of a `private val` logger should be placed at the beginning of the class (`private val log`, `LOG`, `logger`, etc.). #### 3.1.5 Order of declaration of top-level code structures Kotlin allows several top-level declaration types: classes, objects, interfaces, properties and functions. When declaring more than one class or zero classes (e.g. only functions), as per rule [2.2.1](#r2.2.1), you should document the whole file in the header KDoc. When declaring top-level structures, keep the following order: 1. Top-level constants and properties (following same order as properties inside a class: `const val`,`val`, `lateinit var`, `var`) 2. typealiases (grouped by their visibility modifiers) 2. Interfaces, classes and objects (grouped by their visibility modifiers) 3. Extension functions 4. Other functions **Note**: Extension functions shouldn't have receivers declared in the same file according to [rule 6.2.3](#r6.2.3) Valid example: ```kotlin package com.saveourtool.diktat.example const val CONSTANT = 42 val topLevelProperty = "String constant" internal typealias ExamplesHandler = (IExample) -> Unit interface IExample class Example : IExample private class Internal fun Other.asExample(): Example { /* ... */ } private fun Other.asInternal(): Internal { /* ... */ } fun doStuff() { /* ... */ } ``` **Note**: kotlin scripts (.kts) allow arbitrary code to be placed on the top level. When writing kotlin scripts, you should first declare all properties, classes and functions. Only then you should execute functions on top level. It is still recommended wrapping logic inside functions and avoid using top-level statements for function calls or wrapping blocks of code in top-level scope functions like `run`. Example: ```kotlin /* class declarations */ /* function declarations */ run { // call functions here } ``` ### 3.2 Braces This section describes the general rules of using braces in your code. #### 3.2.1 Using braces in conditional statements and loop blocks Braces should always be used in `if`, `else`, `for`, `do`, and `while` statements, even if the program body is empty or contains only one statement. In special Kotlin `when` statements, you do not need to use braces for single-line statements. **Valid example:** ```kotlin when (node.elementType) { FILE -> { checkTopLevelDoc(node) checkSomething() } CLASS -> checkClassElements(node) } ``` **Exception:** The only exception is ternary operator in Kotlin (a single line `if () <> else <>` ) **Invalid example:** ```kotlin val value = if (string.isEmpty()) // WRONG! 0 else 1 ``` **Valid example**: ```kotlin val value = if (string.isEmpty()) 0 else 1 // Okay ``` ```kotlin if (condition) { println("test") } else { println(0) } ``` #### 3.2.2 Opening braces are placed at the end of the line in *non-empty* blocks and block structures For *non-empty* blocks and block structures, the opening brace is placed at the end of the line. Follow the K&R style (1TBS or OTBS) for *non-empty* code blocks with braces: - The opening brace and first line of the code block are on the same line. - The closing brace is on its own new line. - The closing brace can be followed by a newline character. The only exceptions are `else`, `finally`, and `while` (from `do-while` statement), or `catch` keywords. These keywords should not be split from the closing brace by a newline character. **Exception cases**: 1) For lambdas, there is no need to put a newline character after the first (function-related) opening brace. A newline character should appear only after an arrow (`->`) (see [point 5 of Rule 3.6.2](#r3.6.2)). ```kotlin arg.map { value -> foo(value) } ``` 2) for `else`/`catch`/`finally`/`while` (from `do-while` statement) keywords closing brace should stay on the same line: ```kotlin do { if (true) { x++ } else { x-- } } while (x > 0) ``` **Valid example:** ```kotlin return arg.map { value -> while (condition()) { method() } value } return MyClass() { @Override fun method() { if (condition()) { try { something() } catch (e: ProblemException) { recover() } } else if (otherCondition()) { somethingElse() } else { lastThing() } } } ``` ### 3.3 Indentation Only spaces are permitted for indentation, and each indentation should equal `four spaces` (tabs are not permitted). If you prefer using tabs, simply configure them to change to spaces in your IDE automatically. These code blocks should be indented if they are placed on the new line, and the following conditions are met: - The code block is placed immediately after an opening brace. - The code block is placed after each operator, including the assignment operator (`+`/`-`/`&&`/`=`/etc.) - The code block is a call chain of methods: ```kotlin someObject .map() .filter() ``` - The code block is placed immediately after the opening parenthesis. - The code block is placed immediately after an arrow in lambda: ```kotlin arg.map { value -> foo(value) } ``` **Exceptions**: 1. Argument lists: \ a) Eight spaces are used to indent argument lists (both in declarations and at call sites). \ b) Arguments in argument lists can be aligned if they are on different lines. 2. Eight spaces are used if there is a newline after any binary operator. 3. Eight spaces are used for functional-like styles when the newline is placed before the dot. 4. Supertype lists: \ a) Four spaces are used if the colon before the supertype list is on a new line. \ b) Four spaces are used before each supertype, and eight spaces are used if the colon is on a new line. **Note:** there should be an indentation after all statements such as `if`, `for`, etc. However, according to this code style, such statements require braces. ```kotlin if (condition) foo() ``` **Exceptions**: - When breaking the parameter list of a method/class constructor, it can be aligned with `8 spaces`. A parameter that was moved to a new line can be on the same level as the previous argument: ```kotlin fun visit( node: ASTNode, autoCorrect: Boolean, params: KtLint.ExperimentalParams, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit ) { } ``` - Such operators as `+`/`-`/`*` can be indented with `8 spaces`: ```kotlin val abcdef = "my splitted" + " string" ``` - Opening and closing quotes in multiline string should have same indentation ```kotlin lintMethod( """ |val q = 1 | """.trimMargin() ) ``` - A list of supertypes should be indented with `4 spaces` if they are on different lines or with `8 spaces` if the leading colon is also on a separate line ```kotlin class A : B() class A : B() ``` ### 3.4 Empty blocks Avoid empty blocks, and ensure braces start on a new line. An empty code block can be closed immediately on the same line and the next line. However, a newline is recommended between opening and closing braces `{}` (see the examples below.) Generally, empty code blocks are prohibited; using them is considered a bad practice (especially for catch block). They are appropriate for overridden functions, when the base class's functionality is not needed in the class-inheritor, for lambdas used as a function and for empty function in implementation of functional interface. ```kotlin override fun foo() { } ``` **Valid examples** (note once again that generally empty blocks are prohibited): ```kotlin fun doNothing() {} fun doNothingElse() { } fun foo(bar: () -> Unit = {}) ``` **Invalid examples:** ```kotlin try { doSomething() } catch (e: Some) {} ``` Use the following valid code instead: ```kotlin try { doSomething() } catch (e: Some) { } ``` ### 3.5 Line length Line length should be less than 120 symbols. Otherwise, it should be split. If `complex property` initializing is too long, It should be split into priorities: \ 1. Logic Binary Expression (&& ||) \ 2. Comparison Binary Expression (> < == >= <= !=) \ 3. Other types (Arithmetical and Bit operation) (+ - * / % >> << *= += -= /= %= ++ -- ! in !in etc) **Invalid example:** ```kotlin val complexProperty = 1 + 2 + 3 + 4 ``` **Valid example:** ```kotlin val complexProperty = 1 + 2 + 3 + 4 ``` **Invalid example:** ```kotlin val complexProperty = (1 + 2 + 3 > 0) && ( 23 * 4 > 10 * 6) ``` **Valid example:** ```kotlin val complexProperty = (1 + 2 + 3 > 0) && (23 * 4 > 10 * 6) ``` If long line should be split in `Elvis Operator` (?:), it`s done like this **Invalid example:** ```kotlin val value = first ?: second ``` **Valid example:** ```kotlin val value = first ?: second ``` If long line in `Dot Qualified Expression` or `Safe Access Expression`, it`s done like this: **Invalid example:** ```kotlin val value = This.Is.Very.Long.Dot.Qualified.Expression ``` **Valid example:** ```kotlin val value = This.Is.Very.Long .Dot.Qualified.Expression ``` **Invalid example:** ```kotlin val value = This.Is?.Very?.Long?.Safe?.Access?.Expression ``` **Valid example:** ```kotlin val value = This.Is?.Very?.Long ?.Safe?.Access?.Expression ``` if `value arguments list` is too long, it also should be split: **Invalid example:** ```kotlin val result1 = ManyParamInFunction(firstArgument, secondArgument, thirdArgument, fourthArgument, fifthArguments) ``` **Valid example:** ```kotlin val result1 = ManyParamInFunction(firstArgument, secondArgument, thirdArgument, fourthArgument, fifthArguments) ``` If `annotation` is too long, it also should be split: **Invalid example:** ```kotlin @Query(value = "select * from table where age = 10", nativeQuery = true) fun foo() {} ``` **Valid example:** ```kotlin @Query( value = "select * from table where age = 10", nativeQuery = true) fun foo() {} ``` Long one line `function` should be split: **Invalid example:** ```kotlin fun foo() = goo().write("TooLong") ``` **Valid example:** ```kotlin fun foo() = goo().write("TooLong") ``` Long `binary expression` should be split into priorities: \ 1. Logic Binary Expression (**&&** **||**) \ 2. Comparison Binary Expression (**>** **<** **==** **>=** **<=** **!=**) \ 3. Other types (Arithmetical and Bit operation) (**+** **-** * **/** **%** **>>** **<<** **/*=** **+=** **-=** **/=** **%=** **++** **--** **!** **in** **!in** etc) **Invalid example:** ```kotlin if (( x > 100) || y < 100 && !isFoo()) {} ``` **Valid example:** ```kotlin if (( x > 100) || y < 100 && !isFoo()) {} ``` `String template` also can be split in white space in string text **Invalid example:** ```kotlin val nameString = "This is very long string template" ``` **Valid example:** ```kotlin val nameString = "This is very long" + " string template" ``` Long `Lambda argument` should be split: **Invalid example:** ```kotlin val variable = a?.filter { it.elementType == true } ?: null ``` **Valid example:** ```kotlin val variable = a?.filter { it.elementType == true } ?: null ``` Long one line `When Entry` should be split: **Invalid example:** ```kotlin when(elem) { true -> long.argument.whenEntry } ``` **Valid example:** ```kotlin when(elem) { true -> { long.argument.whenEntry } } ``` If the examples above do not fit, but the line needs to be split and this in `property`, this is fixed like thisЖ **Invalid example:** ```kotlin val element = veryLongNameFunction(firstParam) ``` **Valid example:** ```kotlin val element = varyLongNameFunction(firstParam) ``` `Eol comment` also can be split, but it depends on comment location. If this comment is on the same line with code it should be on line before: **Invalid example:** ```kotlin fun foo() { val name = "Nick" // this comment is too long } ``` **Valid example:** ```kotlin fun foo() { // this comment is too long val name = "Nick" } ``` But if this comment is on new line - it should be split to several lines: **Invalid example:** ```kotlin // This comment is too long. It should be on two lines. fun foo() {} ``` **Valid example:** ```kotlin // This comment is too long. // It should be on two lines. fun foo() {} ``` The international code style prohibits `non-Latin` (`non-ASCII`) symbols. (See [Identifiers](#r1.1.1)) However, if you still intend on using them, follow the following convention: - One wide character occupies the width of two narrow characters. The "wide" and "narrow" parts of a character are defined by its [east Asian width Unicode attribute](https://unicode.org/reports/tr11/). Typically, narrow characters are also called "half-width" characters. All characters in the ASCII character set include letters (such as `a, A`), numbers (such as `0, 3`), and punctuation spaces (such as `,` , `{`), all of which are narrow characters. Wide characters are also called "full-width" characters. Chinese characters (such as `中, 文`), Chinese punctuation (`,` , `;` ), full-width letters and numbers (such as `A、3`) are "full-width" characters. Each one of these characters represents two narrow characters. - Any line that exceeds this limit (`120 narrow symbols`) should be wrapped, as described in the [Newline section](#c3.5). **Exceptions:** 1. The long URL or long JSON method reference in KDoc. 2. The `package` and `import` statements. 3. The command line in the comment, enabling it to be cut and pasted into the shell for use. ### 3.6 Line breaks (newlines) This section contains the rules and recommendations on using line breaks. #### 3.6.1 Each line can have a maximum of one statement Each line can have a maximum of one code statement. This recommendation prohibits the use of code with `;` because it decreases code visibility. **Invalid example:** ```kotlin val a = ""; val b = "" ``` **Valid example:** ```kotlin val a = "" val b = "" ``` #### 3.6.2 Rules for line-breaking 1) Unlike Java, Kotlin allows you not to put a semicolon (`;`) after each statement separated by a newline character. There should be no redundant semicolon at the end of the lines. When a newline character is needed to split the line, it should be placed after such operators as `&&`/`||`/`+`/etc. and all infix functions (for example, `xor`). However, the newline character should be placed before operators such as `.`, `?.`, `?:`, and `::`. Note that all comparison operators, such as `==`, `>`, `<`, should not be split. **Invalid example**: ```kotlin if (node != null && test != null) {} ``` **Valid example**: ```kotlin if (node != null && test != null) { } ``` **Note:** You need to follow the functional style, meaning each function call in a chain with `.` should start at a new line if the chain of functions contains more than one call: ```kotlin val value = otherValue!! .map { x -> x } .filter { val a = true true } .size ``` **Note:** The parser prohibits the separation of the `!!` operator from the value it is checking. **Exception**: If a functional chain is used inside the branches of a ternary operator, it does not need to be split with newlines. **Valid example**: ```kotlin if (condition) list.map { foo(it) }.filter { bar(it) } else list.drop(1) ``` **Note:** If dot qualified expression is inside condition or passed as an argument - it should be replaced with new variable. **Invalid example**: ```kotlin if (node.treeParent.treeParent?.treeParent.findChildByType(IDENTIFIER) != null) {} ``` **Valid example**: ```kotlin val grandIdentifier = node .treeParent .treeParent ?.treeParent .findChildByType(IDENTIFIER) if (grandIdentifier != null) {} ``` **Second valid example**: ```kotlin val grandIdentifier = node.treeParent .treeParent ?.treeParent .findChildByType(IDENTIFIER) if (grandIdentifier != null) {} ``` 2) Newlines should be placed after the assignment operator (`=`). 3) In function or class declarations, the name of a function or constructor should not be split by a newline from the opening brace `(`. A brace should be placed immediately after the name without any spaces in declarations or at call sites. 4) Newlines should be placed right after the comma (`,`). 5) If a lambda statement contains more than one line in its body, a newline should be placed after an arrow if the lambda statement has explicit parameters. If it uses an implicit parameter (`it`), the newline character should be placed after the opening brace (`{`). The following examples illustrate this rule: **Invalid example:** ```kotlin value.map { name -> foo() bar() } ``` **Valid example:** ```kotlin value.map { name -> foo() bar() } val someValue = { node:String -> node } ``` 6) When the function contains only a single expression, it can be written as [expression function](https://kotlinlang.org/docs/reference/functions.html#single-expression-functions). The below example shows the style that should not be used. Instead of: ```kotlin override fun toString(): String { return "hi" } ``` use: ```kotlin override fun toString() = "hi" ``` 7) If an argument list in a function declaration (including constructors) or function call contains more than two arguments, these arguments should be split by newlines in the following style. **Valid example:** ```kotlin class Foo(val a: String, b: String, val c: String) { } fun foo( a: String, b: String, c: String ) { } ``` If and only if the first parameter is on the same line as an opening parenthesis, all parameters can be horizontally aligned by the first parameter. Otherwise, there should be a line break after an opening parenthesis. Kotlin 1.4 introduced a trailing comma as an optional feature, so it is generally recommended to place all parameters on a separate line and append [trailing comma](https://kotlinlang.org/docs/reference/whatsnew14.html#trailing-comma). It makes the resolving of merge conflicts easier. **Valid example:** ```kotlin fun foo( a: String, b: String, ) { } ``` same should be done for function calls/constructor arguments/e.t.c Kotlin supports trailing commas in the following cases: Enumerations Value arguments Class properties and parameters Function value parameters Parameters with optional type (including setters) Indexing suffix Lambda parameters when entry Collection literals (in annotations) Type arguments Type parameters Destructuring declarations 8) If the supertype list has more than two elements, they should be separated by newlines. **Valid example:** ```kotlin class MyFavouriteVeryLongClassHolder : MyLongHolder(), SomeOtherInterface, AndAnotherOne { } ``` ### 3.7 Using blank lines Reduce unnecessary blank lines and maintain a compact code size. By reducing unnecessary blank lines, you can display more code on one screen, which improves code readability. - Blank lines should separate content based on relevance and should be placed between groups of fields, constructors, methods, nested classes, `init` blocks, and objects (see [3.1.2](#r3.1.2)). - Do not use more than one line inside methods, type definitions, and initialization expressions. - Generally, do not use more than two consecutive blank lines in a row. - Do not put newlines in the beginning or end of code blocks with curly braces. **Valid example:** ```kotlin fun baz() { doSomething() // No need to add blank lines at the beginning and end of the code block // ... } ``` ### 3.8 Horizontal space This section describes general rules and recommendations for using spaces in the code. #### 3.8.1: Usage of whitespace for code separation Follow the recommendations below for using space to separate keywords: **Note:** These recommendations are for cases where symbols are located on the same line. However, in some cases, a line break could be used instead of a space. 1. Separate keywords (such as `if`, `when`, `for`) from the opening parenthesis with single whitespace. The only exception is the `constructor` keyword, which should not be separated from the opening parenthesis. 2. Separate keywords like `else` or `try` from the opening brace (`{`) with single whitespace. If `else` is used in a ternary-style statement without braces, there should be a single space between `else` and the statement after: `if (condition) foo() else bar()` 3. Use a **single** whitespace before all opening braces (`{`). The only exception is the passing of a lambda as a parameter inside parentheses: ```kotlin private fun foo(a: (Int) -> Int, b: Int) {} foo({x: Int -> x}, 5) // no space before '{' ``` 4. Single whitespace should be placed on both sides of binary operators. This also applies to operator-like symbols. For example: - A colon in generic structures with the `where` keyword: `where T : Type` - Arrow in lambdas: `(str: String) -> str.length()` **Exceptions:** - Two colons (`::`) are written without spaces:\ `Object::toString` - The dot separator (`.`) that stays on the same line with an object name:\ `object.toString()` - Safe access modifiers `?.` and `!!` that stay on the same line with an object name:\ `object?.toString()` - Operator `..` for creating ranges:\ `1..100` 5. Use spaces after (`,`), (`:`), and (`;`), except when the symbol is at the end of the line. However, note that this code style prohibits the use of (`;`) in the middle of a line ([see 3.3.2](#r3.2.2)). There should be no whitespaces at the end of a line. The only scenario where there should be no space after a colon is when the colon is used in the annotation to specify a use-site target (for example, `@param:JsonProperty`). There should be no spaces before `,` , `:` and `;`. **Exceptions** for spaces and colons: - When `:` is used to separate a type and a supertype, including an anonymous object (after object keyword) - When delegating to a superclass constructor or different constructor of the same class **Valid example:** ```kotlin abstract class Foo : IFoo { } class FooImpl : Foo() { constructor(x: String) : this(x) { /*...*/ } val x = object : IFoo { /*...*/ } } ``` 6. There should be *only one space* between the identifier and its type: `list: List` If the type is nullable, there should be no space before `?`. 7. When using `[]` operator (`get/set`) there should be **no** spaces between identifier and `[` : `someList[0]`. 8. There should be no space between a method or constructor name (both at declaration and at call site) and a parenthesis: `foo() {}`. Note that this sub-rule is related only to spaces; the rules for whitespaces are described in [see 3.6.2](#r3.6.2). This rule does not prohibit, for example, the following code: ```kotlin fun foo ( a: String ) ``` 9. Never put a space after `(`, `[`, `<` (when used as a bracket in templates) or before `)`, `]`, `>` (when used as a bracket in templates). 10. There should be no spaces between a prefix/postfix operator (like `!!` or `++`) and its operand. #### 3.8.2: No spaces for horizontal alignment *Horizontal alignment* refers to aligning code blocks by adding space to the code. Horizontal alignment should not be used because: - When modifying code, it takes much time for new developers to format, support, and fix alignment issues. - Long identifier names will break the alignment and lead to less presentable code. - There are more disadvantages than advantages in alignment. To reduce maintenance costs, misalignment (???) is the best choice. Recommendation: Alignment only looks suitable for `enum class`, where it can be used in table format to improve code readability: ```kotlin enum class Warnings(private val id: Int, private val canBeAutoCorrected: Boolean, private val warn: String) : Rule { PACKAGE_NAME_MISSING (1, true, "no package name declared in a file"), PACKAGE_NAME_INCORRECT_CASE (2, true, "package name should be completely in a lower case"), PACKAGE_NAME_INCORRECT_PREFIX(3, false, "package name should start from the company's domain") ; } ``` **Valid example:** ```kotlin private val nr: Int // no alignment, but looks fine private var color: Color // no alignment ``` **Invalid example**: ```kotlin private val nr: Int // aligned comment with extra spaces private val color: Color // alignment for a comment and alignment for identifier name ``` ### 3.9 Enumerations Enum values are separated by a comma and line break, with ';' placed on the new line. 1) The comma and line break characters separate enum values. Put `;` on the new line: ```kotlin enum class Warnings { A, B, C, ; } ``` This will help to resolve conflicts and reduce the number of conflicts during merging pull requests. Also, use [trailing comma](https://kotlinlang.org/docs/reference/whatsnew14.html#trailing-comma). 2) If the enum is simple (no properties, methods, and comments inside), you can declare it in a single line: ```kotlin enum class Suit { CLUBS, HEARTS, SPADES, DIAMONDS } ``` 3) Enum classes take preference (if it is possible to use it). For example, instead of two boolean properties: ```kotlin val isCelsius = true val isFahrenheit = false ``` use enum class: ```kotlin enum class TemperatureScale { CELSIUS, FAHRENHEIT } ``` - The variable value only changes within a fixed range and is defined with the enum type. - Avoid comparison with magic numbers of `-1, 0, and 1`; use enums instead. ```kotlin enum class ComparisonResult { ORDERED_ASCENDING, ORDERED_SAME, ORDERED_DESCENDING, ; } ``` ### 3.10 Variable declaration This section describes rules for the declaration of variables. #### 3.10.1 Declare one variable per line Each property or variable must be declared on a separate line. **Invalid example**: ```kotlin val n1: Int; val n2: Int ``` #### 3.10.2 Variables should be declared near the line where they are first used Declare local variables close to the point where they are first used to minimize their scope. This will also increase the readability of the code. Local variables are usually initialized during their declaration or immediately after. The member fields of the class should be declared collectively (see [Rule 3.1.2](#r3.1.2) for details on the class structure). ### 3.11 'When' expression The `when` statement must have an 'else' branch unless the condition variable is enumerated or a sealed type. Each `when` statement should contain an `else` statement group, even if it does not contain any code. **Exception:** If 'when' statement of the `enum or sealed` type contains all enum values, there is no need to have an "else" branch. The compiler can issue a warning when it is missing. ### 3.12 Annotations Each annotation applied to a class, method or constructor should be placed on its own line. Consider the following examples: 1. Annotations applied to the class, method or constructor are placed on separate lines (one annotation per line). **Valid example**: ```kotlin @MustBeDocumented @CustomAnnotation fun getNameIfPresent() { /* ... */ } ``` 2. A single annotation should be on the same line as the code it is annotating. **Valid example**: ```kotlin @CustomAnnotation class Foo {} ``` 3. Multiple annotations applied to a field or property can appear on the same line as the corresponding field. **Valid example**: ```kotlin @MustBeDocumented @CustomAnnotation val loader: DataLoader ``` ### 3.13 Block comments Block comments should be placed at the same indentation level as the surrounding code. See examples below. **Valid example**: ```kotlin class SomeClass { /* * This is * okay */ fun foo() {} } ``` **Note**: Use `/*...*/` block comments to enable automatic formatting by IDEs. ### 3.14 Modifiers and constant values This section contains recommendations regarding modifiers and constant values. #### 3.14.1 Declaration with multiple modifiers If a declaration has multiple modifiers, always follow the proper sequence. **Valid sequence:** ```kotlin public / internal / protected / private expect / actual final / open / abstract / sealed / const external override lateinit tailrec crossinline vararg suspend inner out enum / annotation companion inline / noinline reified infix operator data ``` #### 3.14.2: Separate long numerical values with an underscore An underscore character should separate long numerical values. **Note:** Using underscores simplifies reading and helps to find errors in numeric constants. ```kotlin val oneMillion = 1_000_000 val creditCardNumber = 1234_5678_9012_3456L val socialSecurityNumber = 999_99_9999L val hexBytes = 0xFF_EC_DE_5E val bytes = 0b11010010_01101001_10010100_10010010 ``` #### 3.14.3: Magic number Prefer defining constants with clear names describing what the magic number means. **Valid example**: ```kotlin class Person() { fun isAdult(age: Int): Boolean = age >= majority companion object { private const val majority = 18 } } ``` **Invalid example**: ```kotlin class Person() { fun isAdult(age: Int): Boolean = age >= 18 } ``` ### 3.15 Strings This section describes the general rules of using strings. #### 3.15.1 Concatenation of Strings String concatenation is prohibited if the string can fit on one line. Use raw strings and string templates instead. Kotlin has significantly improved the use of Strings: [String templates](https://kotlinlang.org/docs/reference/basic-types.html#string-templates), [Raw strings](https://kotlinlang.org/docs/reference/basic-types.html#string-literals). Therefore, compared to using explicit concatenation, code looks much better when proper Kotlin strings are used for short lines, and you do not need to split them with newline characters. **Invalid example**: ```kotlin val myStr = "Super string" val value = myStr + " concatenated" ``` **Valid example**: ```kotlin val myStr = "Super string" val value = "$myStr concatenated" ``` #### 3.15.2 String template format **Redundant curly braces in string templates** If there is only one variable in a string template, there is no need to use such a template. Use this variable directly. **Invalid example**: ```kotlin val someString = "${myArgument} ${myArgument.foo()}" ``` **Valid example**: ```kotlin val someString = "$myArgument ${myArgument.foo()}" ``` **Redundant string template** In case a string template contains only one variable - there is no need to use the string template. Use this variable directly. **Invalid example**: ```kotlin val someString = "$myArgument" ``` **Valid example**: ```kotlin val someString = myArgument ``` ### 3.16 Conditional Statements This section describes the general rules related to the сonditional statements. #### 3.16.1 Collapsing redundant nested if-statements The nested if-statements, when possible, should be collapsed into a single one by concatenating their conditions with the infix operator &&. This improves the readability by reducing the number of the nested language constructs. #### Simple collapse **Invalid example**: ```kotlin if (cond1) { if (cond2) { doSomething() } } ``` **Valid example**: ```kotlin if (cond1 && cond2) { doSomething() } ``` #### Compound conditions **Invalid example**: ```kotlin if (cond1) { if (cond2 || cond3) { doSomething() } } ``` **Valid example**: ```kotlin if (cond1 && (cond2 || cond3)) { doSomething() } ``` #### 3.16.2 Too complex conditions Too complex conditions should be simplified according to boolean algebra rules, if it is possible. The following rules are considered when simplifying an expression: * boolean literals are removed (e.g. `foo() || false` -> `foo()`) * double negation is removed (e.g. `!(!a)` -> `a`) * expression with the same variable are simplified (e.g. `a && b && a` -> `a && b`) * remove expression from disjunction, if they are subset of other expression (e.g. `a || (a && b)` -> `a`) * remove expression from conjunction, if they are more broad than other expression (e.g. `a && (a || b)` -> `a`) * de Morgan's rule (negation is moved inside parentheses, i.e. `!(a || b)` -> `!a && !b`) **Valid example** ```kotlin if (condition1 && condition2) { foo() } ``` **Invalid example** ```kotlin if (condition1 && condition2 && condition1) { foo() } ``` # 4. Variables and types This section is dedicated to the rules and recommendations for using variables and types in your code. ### 4.1 Variables The rules of using variables are explained in the below topics. #### 4.1.1 Do not use Float and Double types when accurate calculations are needed Floating-point numbers provide a good approximation over a wide range of values, but they cannot produce accurate results in some cases. Binary floating-point numbers are unsuitable for precise calculations because it is impossible to represent 0.1 or any other negative power of 10 in a `binary representation` with a finite length. The following code example seems to be obvious: ```kotlin val myValue = 2.0 - 1.1 println(myValue) ``` However, it will print the following value: `0.8999999999999999` Therefore, for precise calculations (for example, in finance or exact sciences), using such types as `Int`, `Long`, `BigDecimal`are recommended. The `BigDecimal` type should serve as a good choice. **Invalid example**: Float values containing more than six or seven decimal numbers will be rounded. ```kotlin val eFloat = 2.7182818284f // Float, will be rounded to 2.7182817 ``` **Valid example**: (when precise calculations are needed): ```kotlin val income = BigDecimal("2.0") val expense = BigDecimal("1.1") println(income.subtract(expense)) // you will obtain 0.9 here ``` #### 4.1.2: Comparing numeric float type values Numeric float type values should not be directly compared with the equality operator (==) or other methods, such as `compareTo()` and `equals()`. Since floating-point numbers involve precision problems in computer representation, it is better to use `BigDecimal` as recommended in [Rule 4.1.1](#r4.1.1) to make accurate computations and comparisons. The following code describes these problems. **Invalid example**: ```kotlin val f1 = 1.0f - 0.9f val f2 = 0.9f - 0.8f if (f1 == f2) { println("Expected to enter here") } else { println("But this block will be reached") } val flt1 = f1; val flt2 = f2; if (flt1.equals(flt2)) { println("Expected to enter here") } else { println("But this block will be reached") } ``` **Valid example**: ```kotlin val foo = 1.03f val bar = 0.42f if (abs(foo - bar) > 1e-6f) { println("Ok") } else { println("Not") } ``` #### 4.1.3 Try to use 'val' instead of 'var' for variable declaration [SAY_NO_TO_VAR] Variables with the `val` modifier are immutable (read-only). Using `val` variables instead of `var` variables increases code robustness and readability. This is because `var` variables can be reassigned several times in the business logic. However, in some scenarios with loops or accumulators, only `var`s are permitted. ### 4.2 Types This section provides recommendations for using types. #### 4.2.1: Use Contracts and smart cast as much as possible The Kotlin compiler has introduced [Smart Casts](https://kotlinlang.org/docs/reference/typecasts.html#smart-casts) that help reduce the size of code. **Invalid example**: ```kotlin if (x is String) { print((x as String).length) // x was already automatically cast to String - no need to use 'as' keyword here } ``` **Valid example**: ```kotlin if (x is String) { print(x.length) // x was already automatically cast to String - no need to use 'as' keyword here } ``` Also, Kotlin 1.3 introduced [Contracts](https://kotlinlang.org/docs/reference/whatsnew13.html#contracts) that provide enhanced logic for smart-cast. Contracts are used and are very stable in `stdlib`, for example: ```kotlin fun bar(x: String?) { if (!x.isNullOrEmpty()) { println("length of '$x' is ${x.length}") // smartcasted to not-null } } ``` Smart cast and contracts are a better choice because they reduce boilerplate code and features forced type conversion. **Invalid example**: ```kotlin fun String?.isNotNull(): Boolean = this != null fun foo(s: String?) { if (s.isNotNull()) s!!.length // No smartcast here and !! operator is used } ``` **Valid example**: ```kotlin fun foo(s: String?) { if (s.isNotNull()) s.length // We have used a method with contract from stdlib that helped compiler to execute smart cast } ``` #### 4.2.2: Try to use type alias to represent types making code more readable Type aliases provide alternative names for existing types. If the type name is too long, you can replace it with a shorter name, which helps to shorten long generic types. For example, code looks much more readable if you introduce a `typealias` instead of a long chain of nested generic types. We recommend using a `typealias` if the type contains **more than two** nested generic types and is longer than **25 chars**. **Invalid example**: ```kotlin val b: MutableMap> ``` **Valid example**: ```kotlin typealias FileTable = MutableMap> val b: FileTable ``` You can also provide additional aliases for function (lambda-like) types: ```kotlin typealias MyHandler = (Int, String, Any) -> Unit typealias Predicate = (T) -> Boolean ``` ### 4.3 Null safety and variable declarations Kotlin is declared as a null-safe programming language. However, to achieve compatibility with Java, it still supports nullable types. #### 4.3.1: Avoid declaring variables with nullable types, especially from Kotlin stdlib To avoid `NullPointerException` and help the compiler prevent Null Pointer Exceptions, avoid using nullable types (with `?` symbol). **Invalid example**: ```kotlin val a: Int? = 0 ``` **Valid example**: ```kotlin val a: Int = 0 ``` Nevertheless, when using Java libraries extensively, you have to use nullable types and enrich the code with `!!` and `?` symbols. Avoid using nullable types for Kotlin stdlib (declared in [official documentation](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/)). Try to use initializers for empty collections. For example, if you want to initialize a list instead of `null`, use `emptyList()`. **Invalid example**: ```kotlin val a: List? = null ``` **Valid example**: ```kotlin val a: List = emptyList() ``` #### 4.3.2: Variables of generic types should have an explicit type declaration Like in Java, classes in Kotlin may have type parameters. To create an instance of such a class, we typically need to provide type arguments: ```kotlin val myVariable: Map = emptyMap() ``` However, the compiler can inherit type parameters from the r-value (value assigned to a variable). Therefore, it will not force users to declare the type explicitly. These declarations are not recommended because programmers would need to find the return value and understand the variable type by looking at the method. **Invalid example**: ```kotlin val myVariable = emptyMap() ``` **Valid example**: ```kotlin val myVariable: Map = emptyMap() ``` #### 4.3.3 Null-safety Try to avoid explicit null checks (explicit comparison with `null`) Kotlin is declared as [Null-safe](https://kotlinlang.org/docs/reference/null-safety.html) language. However, Kotlin architects wanted Kotlin to be fully compatible with Java; that's why the `null` keyword was also introduced in Kotlin. There are several code-structures that can be used in Kotlin to avoid null-checks. For example: `?:`, `.let {}`, `.also {}`, e.t.c **Invalid example:** ```kotlin // example 1 var myVar: Int? = null if (myVar == null) { println("null") return } // example 2 if (myVar != null) { println("not null") return } // example 3 val anotherVal = if (myVar != null) { println("not null") 1 } else { 2 } // example 4 if (myVar == null) { println("null") } else { println("not null") } ``` **Valid example:** ```kotlin // example 1 var myVar: Int? = null myVar?: run { println("null") return } // example 2 myVar?.let { println("not null") return } // example 3 val anotherVal = myVar?.also { println("not null") 1 } ?: 2 // example 4 myVar?.let { println("not null") } ?: run { println("null") } ``` **Exceptions:** In the case of complex expressions, such as multiple `else-if` structures or long conditional statements, there is common sense to use explicit comparison with `null`. **Valid examples:** ```kotlin if (myVar != null) { println("not null") } else if (anotherCondition) { println("Other condition") } ``` ```kotlin if (myVar == null || otherValue == 5 && isValid) {} ``` Please also note, that instead of using `require(a != null)` with a not null check - you should use a special Kotlin function called `requireNotNull(a)`. # 5. Functions This section describes the rules of using functions in your code. ### 5.1 Function design Developers can write clean code by gaining knowledge of how to build design patterns and avoid code smells. You should utilize this approach, along with functional style, when writing Kotlin code. The concepts behind functional style are as follows: Functions are the smallest unit of combinable and reusable code. They should have clean logic, **high cohesion**, and **low coupling** to organize the code effectively. The code in functions should be simple and not conceal the author's original intentions. Additionally, it should have a clean abstraction, and control statements should be used straightforwardly. The side effects (code that does not affect a function's return value but affects global/object instance variables) should not be used for state changes of an object. The only exceptions to this are state machines. Kotlin is [designed](https://www.slideshare.net/abreslav/whos-more-functional-kotlin-groovy-scala-or-java) to support and encourage functional programming, featuring the corresponding built-in mechanisms. Also, it supports standard collections and sequences feature methods that enable functional programming (for example, `apply`, `with`, `let`, and `run`), Kotlin Higher-Order functions, function types, lambdas, and default function arguments. As [previously discussed](#r4.1.3), Kotlin supports and encourages the use of immutable types, which in turn motivates programmers to write pure functions that avoid side effects and have a corresponding output for specific input. The pipeline data flow for the pure function comprises a functional paradigm. It is easy to implement concurrent programming when you have chains of function calls, where each step features the following characteristics: 1. Simplicity 2. Verifiability 3. Testability 4. Replaceability 5. Pluggability 6. Extensibility 7. Immutable results There can be only one side effect in this data stream, which can be placed only at the end of the execution queue. #### 5.1.1 Avoid functions that are too long The function should be displayable on one screen and only implement one certain logic. If a function is too long, it often means complex and could be split or simplified. Functions should consist of 30 lines (non-empty and non-comment) in total. **Exception:** Some functions that implement complex algorithms may exceed 30 lines due to aggregation and comprehensiveness. Linter warnings for such functions **can be suppressed**. Even if a long function works well, new problems or bugs may appear due to the function's complex logic once it is modified by someone else. Therefore, it is recommended to split such functions into several separate and shorter functions that are easier to manage. This approach will enable other programmers to read and modify the code properly. #### 5.1.2 Avoid deep nesting of function code blocks, limiting to four levels The nesting depth of a function's code block is the depth of mutual inclusion between the code control blocks in the function (for example: if, for, while, and when). Each nesting level will increase the amount of effort needed to read the code because you need to remember the current "stack" (for example, entering conditional statements and loops). **Exception:** The nesting levels of the lambda expressions, local classes, and anonymous classes in functions are calculated based on the innermost function. The nesting levels of enclosing methods are not accumulated. Functional decomposition should be implemented to avoid confusion for the developer who reads the code. This will help the reader switch between contexts. #### 5.1.3 Avoid using nested functions Nested functions create a more complex function context, thereby confusing readers. With nested functions, the visibility context may not be evident to the code reader. **Invalid example**: ```kotlin fun foo() { fun nested():String { return "String from nested function" } println("Nested Output: ${nested()}") } ``` #### 5.1.4 Negated function calls Don't use negated function calls if it can be replaced with negated version of this function **Invalid example**: ```kotlin fun foo() { val list = listOf(1, 2, 3) if (!list.isEmpty()) { // Some cool logic } } ``` **Valid example**: ```kotlin fun foo() { val list = listOf(1, 2, 3) if (list.isNotEmpty()) { // Some cool logic } } ``` ### 5.2 Function arguments The rules for using function arguments are described in the below topics. #### 5.2.1 The lambda parameter of the function should be placed at the end of the argument list With such notation, it is easier to use curly brackets, leading to better code readability. **Valid example**: ```kotlin // declaration fun myFoo(someArg: Int, myLambda: () -> Unit) { // ... } // usage myFoo(1) { println("hey") } ``` #### 5.2.2 Number of function parameters should be limited to five A long argument list is a [code smell](https://en.wikipedia.org/wiki/Code_smell) that leads to less reliable code. It is recommended to reduce the number of parameters. Having **more than five** parameters leads to difficulties in maintenance and conflicts merging. If parameter groups appear in different functions multiple times, these parameters are closely related and can be encapsulated into a single Data Class. It is recommended that you use Data Classes and Maps to unify these function arguments. #### 5.2.3 Use default values for function arguments instead of overloading them In Java, default values for function arguments are prohibited. That is why the function should be overloaded when you need to create a function with fewer arguments. In Kotlin, you can use default arguments instead. **Invalid example**: ```kotlin private fun foo(arg: Int) { // ... } private fun foo() { // ... } ``` **Valid example**: ```kotlin private fun foo(arg: Int = 0) { // ... } ``` #### 5.2.4 Synchronizing code inside asynchronous code Try to avoid using `runBlocking` in asynchronous code **Invalid example**: ```kotlin GlobalScope.async { runBlocking { count++ } } ``` #### 5.2.5 Long lambdas should have explicit parameters The lambda without parameters shouldn't be too long. If a lambda is too long, it can confuse the user. Lambda without parameters should consist of 10 lines (non-empty and non-comment) in total. #### 5.2.6 Avoid using unnecessary, custom label Expressions with unnecessary, custom labels generally increase complexity and worsen the maintainability of the code. **Invalid example**: ```kotlin run lab@ { list.forEach { return@lab } } ``` **Valid example**: ```kotlin list.forEachIndexed { index, i -> return@forEachIndexed } lab@ for(i: Int in q) { for (j: Int in q) { println(i) break@lab } } ``` # 6. Classes, interfaces, and extension functions ### 6.1 Classes This section describes the rules of denoting classes in your code. #### 6.1.1 Denoting a class with a single constructor When a class has a single constructor, it should be defined as a primary constructor in the declaration of the class. If the class contains only one explicit constructor, it should be converted to a primary constructor. **Invalid example**: ```kotlin class Test { var a: Int constructor(a: Int) { this.a = a } } ``` **Valid example**: ```kotlin class Test(var a: Int) { // ... } // in case of any annotations or modifiers used on a constructor: class Test private constructor(var a: Int) { // ... } ``` #### 6.1.2 Prefer data classes instead of classes without any functional logic Some people say that the data class is a code smell. However, if you need to use it (which makes your code more simple), you can utilize the Kotlin `data class`. The main purpose of this class is to hold data, but also `data class` will automatically generate several useful methods: - equals()/hashCode() pair; - toString() - componentN() functions corresponding to the properties in their order of declaration; - copy() function Therefore, instead of using `normal` classes: ```kotlin class Test { var a: Int = 0 get() = field set(value: Int) { field = value} } class Test { var a: Int = 0 var b: Int = 0 constructor(a:Int, b: Int) { this.a = a this.b = b } } // or class Test(var a: Int = 0, var b: Int = 0) // or class Test() { var a: Int = 0 var b: Int = 0 } ``` **prefer data classes:** ```kotlin data class Test1(var a: Int = 0, var b: Int = 0) ``` **Exception 1**: Note that data classes cannot be abstract, open, sealed, or inner; that is why these types of classes cannot be changed to a data class. **Exception 2**: No need to convert a class to a data class if this class extends some other class or implements an interface. #### 6.1.3 Do not use the primary constructor if it is empty or useless The primary constructor is a part of the class header; it is placed after the class name and type parameters (optional) but can be omitted if it is not used. **Invalid example**: ```kotlin // simple case that does not need a primary constructor class Test() { var a: Int = 0 var b: Int = 0 } // empty primary constructor is not needed here // it can be replaced with a primary contructor with one argument or removed class Test() { var a = "Property" init { println("some init") } constructor(a: String): this() { this.a = a } } ``` **Valid example**: ```kotlin // the good example here is a data class; this example also shows that you should get rid of braces for the primary constructor class Test { var a: Int = 0 var b: Int = 0 } ``` #### 6.1.4 Do not use redundant init blocks in your class Several init blocks are redundant and generally should not be used in your class. The primary constructor cannot contain any code. That is why Kotlin has introduced `init` blocks. These blocks store the code to be run during the class initialization. Kotlin allows writing multiple initialization blocks executed in the same order as they appear in the class body. Even when you follow (rule 3.2)[#r3.2], this makes your code less readable as the programmer needs to keep in mind all init blocks and trace the execution of the code. Therefore, you should try to use a single `init` block to reduce the code's complexity. If you need to do some logging or make some calculations before the class property assignment, you can use powerful functional programming. This will reduce the possibility of the error if your `init` blocks' order is accidentally changed and make the code logic more coupled. It is always enough to use one `init` block to implement your idea in Kotlin. **Invalid example**: ```kotlin class YourClass(var name: String) { init { println("First initializer block that prints ${name}") } val property = "Property: ${name.length}".also(::println) init { println("Second initializer block that prints ${name.length}") } } ``` **Valid example**: ```kotlin class YourClass(var name: String) { init { println("First initializer block that prints ${name}") } val property = "Property: ${name.length}".also { prop -> println(prop) println("Second initializer block that prints ${name.length}") } } ``` The `init` block was not added to Kotlin to help you initialize your properties; it is needed for more complex tasks. Therefore if the `init` block contains only assignments of variables - move it directly to properties to be correctly initialized near the declaration. In some cases, this rule can be in clash with [6.1.1](#r6.1.1), but that should not stop you. **Invalid example**: ```kotlin class A(baseUrl: String) { private val customUrl: String init { customUrl = "$baseUrl/myUrl" } } ``` **Valid example**: ```kotlin class A(baseUrl: String) { private val customUrl = "$baseUrl/myUrl" } ``` #### 6.1.5 Explicit supertype qualification The explicit supertype qualification should not be used if there is no clash between called methods. This rule is applicable to both interfaces and classes. **Invalid example**: ```kotlin open class Rectangle { open fun draw() { /* ... */ } } class Square() : Rectangle() { override fun draw() { super.draw() // no need in super here } } ``` #### 6.1.6 Abstract class should have at least one abstract method Abstract classes are used to force a developer to implement some of its parts in their inheritors. When the abstract class has no abstract methods, it was set `abstract` incorrectly and can be converted to a regular class. **Invalid example**: ```kotlin abstract class NotAbstract { fun foo() {} fun test() {} } ``` **Valid example**: ```kotlin abstract class NotAbstract { abstract fun foo() fun test() {} } // OR class NotAbstract { fun foo() {} fun test() {} } ``` #### 6.1.7 When using the "implicit backing property" scheme, the name of real and back property should be the same Kotlin has a mechanism of [backing properties](https://kotlinlang.org/docs/reference/properties.html#backing-properties). In some cases, implicit backing is not enough and it should be done explicitly: ```kotlin private var _table: Map? = null val table: Map get() { if (_table == null) { _table = HashMap() // Type parameters are inferred } return _table ?: throw AssertionError("Set to null by another thread") } ``` In this case, the name of the backing property (`_table`) should be the same as the name of the real property (`table`) but should have an underscore (`_`) prefix. It is one of the exceptions from the [identifier names rule](#r1.2) #### 6.1.8 Avoid using custom getters and setters Kotlin has a perfect mechanism of [properties](https://kotlinlang.org/docs/reference/properties.html#properties-and-fields). Kotlin compiler automatically generates `get` and `set` methods for properties and can override them. **Invalid example:** ```kotlin class A { var size: Int = 0 set(value) { println("Side effect") field = value } // user of this class does not expect calling A.size receive size * 2 get() = field * 2 } ``` From the callee code, these methods look like access to this property: `A().isEmpty = true` for setter and `A().isEmpty` for getter. However, when `get` and `set` are overridden, it isn't very clear for a developer who uses this particular class. The developer expects to get the property value but receives some unknown value and some extra side-effect hidden by the custom getter/setter. Use extra functions instead to avoid confusion. **Valid example**: ```kotlin class A { var size: Int = 0 fun initSize(value: Int) { // some custom logic } // this will not confuse developer and he will get exactly what he expects fun goodNameThatDescribesThisGetter() = this.size * 2 } ``` **Exception:** `Private setters` are only exceptions that are not prohibited by this rule. #### 6.1.9 Never use the name of a variable in the custom getter or setter (possible_bug) If you ignored [recommendation 6.1.8](#r6.1.8), be careful with using the name of the property in your custom getter/setter as it can accidentally cause a recursive call and a `StackOverflow Error`. Use the `field` keyword instead. **Invalid example (very bad)**: ```kotlin var isEmpty: Boolean set(value) { println("Side effect") isEmpty = value } get() = isEmpty ``` #### 6.1.10 No trivial getters and setters are allowed in the code In Java, trivial getters - are the getters that are just returning the field value. Trivial setters - are merely setting the field with a value without any transformation. However, in Kotlin, trivial getters/setters are generated by default. There is no need to use it explicitly for all types of data structures in Kotlin. **Invalid example**: ```kotlin class A { var a: Int = 0 get() = field set(value: Int) { field = value } // } ``` **Valid example**: ```kotlin class A { var a: Int = 0 get() = field set(value: Int) { field = value } // } ``` #### 6.1.11 Use 'apply' for grouping object initialization In Java, before functional programming became popular, many classes from common libraries used the configuration paradigm. To use these classes, you had to create an object with the constructor with 0-2 arguments and set the fields needed to run the object. In Kotlin, to reduce the number of dummy code line and to group objects [`apply` extension](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/apply.html) was added: **Invalid example**: ```kotlin class HttpClient(var name: String) { var url: String = "" var port: String = "" var timeout = 0 fun doRequest() {} } fun main() { val httpClient = HttpClient("myConnection") httpClient.url = "http://example.com" httpClient.port = "8080" httpClient.timeout = 100 httpCLient.doRequest() } ``` **Valid example**: ```kotlin class HttpClient(var name: String) { var url: String = "" var port: String = "" var timeout = 0 fun doRequest() {} } fun main() { val httpClient = HttpClient("myConnection") .apply { url = "http://example.com" port = "8080" timeout = 100 } httpClient.doRequest() } ``` ### 6.1.12 Prefer Inline classes when a class has a single property If a class has only one immutable property, then it can be converted to the inline class. Sometimes it is necessary for business logic to create a wrapper around some type. However, it introduces runtime overhead due to additional heap allocations. Moreover, if the wrapped type is primitive, the performance hit is terrible, because primitive types are usually heavily optimized by the runtime, while their wrappers don't get any special treatment. **Invalid example**: ```kotlin class Password { val value: String } ``` **Valid example**: ```kotlin inline class Password(val value: String) ``` ### 6.2 Extension functions This section describes the rules of using extension functions in your code. [Extension functions](https://kotlinlang.org/docs/reference/extensions.html) is a killer-feature in Kotlin. It gives you a chance to extend classes that were already implemented in external libraries and helps you to make classes less heavy. Extension functions are resolved statically. #### 6.2.1 Use extension functions for making logic of classes less coupled It is recommended that for classes, the non-tightly coupled functions, which are rarely used in the class, should be implemented as extension functions where possible. They should be implemented in the same class/file where they are used. This is a non-deterministic rule, so the code cannot be checked or fixed automatically by a static analyzer. #### 6.2.2 No extension functions with the same name and signature if they extend base and inheritor classes (possible_bug) You should avoid declaring extension functions with the same name and signature if their receivers are base and inheritor classes (possible_bug), as extension functions are resolved statically. There could be a situation when a developer implements two extension functions: one is for the base class and another for the inheritor. This can lead to an issue when an incorrect method is used. **Invalid example**: ```kotlin open class A class B: A() // two extension functions with the same signature fun A.foo() = "A" fun B.foo() = "B" fun printClassName(s: A) { println(s.foo()) } // this call will run foo() method from the base class A, but // programmer can expect to run foo() from the class inheritor B fun main() { printClassName(B()) } ``` #### 6.2.3 Don't use extension functions for the class in the same file You should not use extension functions for the class in the same file, where it is defined. **Invalid example**: ```kotlin class SomeClass { } fun SomeClass.deleteAllSpaces() { } ``` #### 6.2.4 Use 'lastIndex' in case you need to get latest element of a collection You should not use property length with operation - 1, you can change this to lastIndex **Invalid example**: ```kotlin val A = "name" val B = A.length - 1 val C = A[A.length - 1] ``` **Valid example**: ```kotlin val A = "name" val B = A.lastIndex val C = A[A.lastIndex] ``` ### 6.3 Interfaces An `Interface` in Kotlin can contain declarations of abstract methods, as well as method implementations. What makes them different from abstract classes is that interfaces cannot store state. They can have properties, but these need to be abstract or to provide accessor implementations. Kotlin's interfaces can define attributes and functions. In Kotlin and Java, the interface is the main presentation means of application programming interface (API) design and should take precedence over the use of (abstract) classes. ### 6.4 Objects This section describes the rules of using objects in code. #### 6.4.1 Instead of using utility classes/objects, use extensions Avoid using utility classes/objects; use extensions instead. As described in [6.2 Extension functions](#c6.2), using extension functions is a powerful method. This enables you to avoid unnecessary complexity and class/object wrapping and use top-level functions instead. **Invalid example**: ```kotlin object StringUtil { fun stringInfo(myString: String): Int { return myString.count{ "something".contains(it) } } } StringUtil.stringInfo("myStr") ``` **Valid example**: ```kotlin fun String.stringInfo(): Int { return this.count{ "something".contains(it) } } "myStr".stringInfo() ``` #### 6.4.2 Objects should be used for Stateless Interfaces Kotlin’s objects are extremely useful when you need to implement some interface from an external library that does not have any state. There is no need to use classes for such structures. **Valid example**: ``` interface I { fun foo() } object O: I { override fun foo() {} } ``` ### 6.5 Kts Files This section describes general rules for `.kts` files #### 6.5.1 kts files should wrap logic into top-level scope It is still recommended wrapping logic inside functions and avoid using top-level statements for function calls or wrapping blocks of code in top-level scope functions like `run`. **Valid example**: ``` run { // some code } fun foo() { } ``` ================================================ FILE: info/guide/guide-TOC.md ================================================ # Kotlin Coding Style Guide (Diktat Code Style), v.1.0.0 I [Preface](#c0) * [I.I Purpose of this document](#c0.1) * [I.II General principles](#c0.2) * [I.III Terminology](#c0.3) * [I.IV Exceptions](#c0.4) [1. Naming](#c1) * [1.1 Identifiers](#c1.1) * [1.2 Packages](#c1.2) * [1.3 Classes, enumerations, interfaces](#c1.3) * [1.4 Functions](#c1.4) * [1.5 Constants](#c1.5) * [1.6 Non-constant fields (variables)](#c1.6) * [1.6.1 Non-constant field name](#r1.6.1) * [1.6.2 Boolean variable names with negative meaning](#r1.6.2) [2. Comments](#c2) * [2.1 General form of Kdoc](#c2.1) * [2.1.1 Using KDoc for the public, protected, or internal code elements](#r2.1.1) * [2.1.2 Describing methods that have arguments, a return value, or can throw an exception in the KDoc block](#r2.1.2) * [2.1.3 Only one space between the Kdoc tag and content. Tags are arranged in the order.](#r2.1.3) * [2.2 Adding comments on the file header](#c2.2) * [2.3 Comments on the function header](#c2.3) * [2.4 Code comments](#c2.4) * [2.4.1 Adding a blank line between the body of the comment and Kdoc tag-blocks](#r2.4.1) * [2.4.2 Do not comment on unused code blocks](#r2.4.2) * [2.4.3 Code delivered to the client must not contain TODO/FIXME comments](#r2.4.3) [3. General formatting (typesetting)](#c3) * [3.1 File-related rules](#c3.1) * [3.1.1 Avoid files that are too long](#r3.1.1) * [3.1.2 Code blocks in the source file must be separated by one blank line](#r3.1.2) * [3.1.3 Import statements order](#r3.1.3) * [3.1.4 Order of declaration parts of class-like code structures](#r3.1.4) * [3.1.5 Order of declaration of top-level code structures](#r3.1.5) * [3.2 Braces](#c3.2) * [3.2.1 Using braces in conditional statements and loop blocks](#r3.2.1) * [3.2.2 Opening braces are placed at the end of the line in *non-empty* blocks and block structures](#r3.2.2) * [3.3 Indentation](#c3.3) * [3.4 Empty blocks](#c3.4) * [3.5 Line length](#c3.5) * [3.6 Line breaks (newlines)](#c3.6) * [3.6.1 Each line can have a maximum of one statement](#r3.6.1) * [3.6.2 Rules for line-breaking](#r3.6.2) * [3.7 Using blank lines](#c3.7) * [3.8 Horizontal space](#c3.8) * [3.8.1 Using whitespace for code separation](#r3.8.1) * [3.8.2 No spaces for horizontal alignment](#r3.8.2) * [3.9 Enumerations](#c3.9) * [3.10 Variable declaration](#c3.10) * [3.10.1 Declare one variable per line](#r3.10.1) * [3.10.2 Variables should be declared near the line where they are first used](#r3.10.2) * [3.11 'When' expression](#c3.11) * [3.12 Annotations](#c3.12) * [3.13 Block comments](#c3.13) * [3.14 Modifiers and constant values](#c3.14) * [3.14.1 Declaration with multiple modifiers](#r3.14.1) * [3.14.2 Separating long numerical values with an underscore](#r3.14.2) * [3.15 Strings](#c3.15) * [3.15.1 Concatenation of Strings](#r3.15.1) * [3.15.2 String template format](#r3.15.2) * [3.16 Conditional statements](#c3.16) * [3.16.1 Collapsing redundant nested if-statements](#r3.16.1) * [3.16.2 Too complex conditions](#r3.16.2) [4. Variables and types](#c4) * [4.1 Variables](#c4.1) * [4.1.1 Do not use Float and Double types when accurate calculations are needed](#r4.1.1) * [4.1.2 Comparing numeric float type values](#r4.1.2) * [4.1.3 Using 'val' instead of 'var' for variable declaration [SAY_NO_TO_VAR]](#r4.1.3) * [4.2 Types](#c4.2) * [4.2.1 Using Contracts and smart cast as much as possible](#r4.2.1) * [4.2.2 Trying to use type alias to represent types making code more readable](#r4.2.2) * [4.3 Null safety and variable declarations](#c4.3) * [4.3.1 Avoid declaring variables with nullable types, especially from Kotlin stdlib](#r4.3.1) * [4.3.2 Variables of generic types should have an explicit type declaration](#r4.3.2) * [4.3.3 Null-safety](#r4.3.3) [5. Functions](#c5) * [5.1 Function design](#c5.1) * [5.1.1 Avoid functions that are too long ](#r5.1.1) * [5.1.2 Avoid deep nesting of function code blocks, limiting to four levels](#r5.1.2) * [5.1.3 Avoid using nested functions](#r5.1.3) * [5.1.4 Negated function calls](#r5.1.4) * [5.2 Function arguments](#c5.2) * [5.2.1 The lambda parameter of the function should be placed at the end of the argument list](#r5.2.1) * [5.2.2 Number of function parameters should be limited to five](#r5.2.2) * [5.2.3 Use default values for function arguments instead of overloading them](#r5.2.3) * [5.2.4 Synchronizing code inside asynchronous code](#r5.2.4) * [5.2.5 Long lambdas must have explicit parameters](#r5.2.5) * [5.2.6 Avoid using unnecessary, custom label](#r5.2.6) [6. Classes, interfaces, and extension functions](#c6) * [6.1 Classes](#c6.1) * [6.1.1 Denoting a class with a single constructor](#r6.1.1) * [6.1.2 Prefer data classes instead of classes without any functional logic](#r6.1.2) * [6.1.3 Do not use the primary constructor if it is empty or useless](#r6.1.3) * [6.1.4 Do not use redundant init blocks in your class](#r6.1.4) * [6.1.5 Explicit supertype qualification](#r6.1.5) * [6.1.6 Abstract class must have at least one abstract method](#r6.1.6) * [6.1.7 When using the "implicit backing property" scheme, the name of real and back property should be the same](#r6.1.7) * [6.1.8 Avoid using custom getters and setters](#r6.1.8) * [6.1.9 Never use the name of a variable in the custom getter or setter (possible_bug)](#r6.1.9) * [6.1.10 No trivial getters and setters are allowed in the code](#r6.1.10) * [6.1.11 Use 'apply' for grouping object initialization](#r6.1.11) * [6.1.12 Prefer Inline classes when the class has a single property](#r6.1.12) * [6.2 Extension functions](#c6.2) * [6.2.1 Use extension functions for making logic of classes less coupled](#r6.2.1) * [6.2.2 No extension functions with the same name and signature if they extend base and inheritor classes (possible_bug)](#r6.2.2) * [6.2.3 Don't use extension functions for the class in the same file](#r6.2.3) * [6.3 Interfaces](#c6.3) * [6.4 Objects](#c6.4) * [6.4.1 Instead of using utility classes/objects, use extensions](#r6.4.1) * [6.4.2 Objects must be used for Stateless Interfaces](#r6.4.2) * [6.5 Kts Files](#c6.5) * [6.5.1 kts files should wrap logic into top-level scope](#r6.5.1) ================================================ FILE: info/guide/guide-chapter-0.md ================================================ ## Preface ### Purpose of this document The purpose of this document is to provide a specification that software developers could reference to enhance their ability to write consistent, easy-to-read, and high-quality code. Such a specification will ultimately improve software development efficiency and product competitiveness. For the code to be considered high-quality, it must entail the following characteristics: 1. Simplicity 2. Maintainability 3. Reliability 4. Testability 5. Efficiency 6. Portability 7. Reusability ### General principles Like other modern programming languages, Kotlin is an advanced programming language that complies with the following general principles: 1. Clarity — a necessary feature of programs that are easy to maintain and refactor. 2. Simplicity — a code is easy to understand and implement. 3. Consistency — enables a code to be easily modified, reviewed, and understood by the team members. Unification is particularly important when the same team works on the same project, utilizing similar styles enabling a code to be easily modified, reviewed, and understood by the team members. Also, we need to consider the following factors when programming on Kotlin: 1. Writing a clean and simple Kotlin code Kotlin combines two of the main programming paradigms: functional and object-oriented. Both of these paradigms are trusted and well-known software engineering practices. As a young programming language, Kotlin is built on top of well-established languages such as Java, C++, C#, and Scala. This enables Kotlin to introduce many features that help a developer write cleaner, more readable code while also reducing the number of complex code structures. For example, type and null safety, extension functions, infix syntax, immutability, val/var differentiation, expression-oriented features, "when" statements, much easier work with collections, type auto conversion, and other syntactic sugar. 2. Following Kotlin idioms The author of Kotlin, Andrey Breslav, mentioned that Kotlin is both pragmatic and practical, but not academic. Its pragmatic features enable ideas to be transformed into real working software easily. Kotlin is closer to natural languages than its predecessors, and it implements the following design principles: readability, reusability, interoperability, security, and tool-friendliness (https://blog.jetbrains.com/kotlin/2018/10/kotlinconf-2018-announcements/). 3. Using Kotlin efficiently Some Kotlin features can help you write high-performance code. Such features include: rich coroutine library, sequences, inline functions/classes, arrays of basic types, tailRec, and CallsInPlace of contract. ### Terminology **Rules** — conventions that should be followed when programming. **Recommendations** — conventions that should be considered when programming. **Explanation** — necessary explanations of rules and recommendations. **Valid Example** — recommended examples of rules and recommendations. **Invalid Example** — not recommended examples of rules and recommendations. Unless otherwise stated, this specification applies to versions 1.3 and later of Kotlin. ### Exceptions Even though exceptions may exist, it is essential to understand why rules and recommendations are needed. Depending on a project situation or personal habits, you can break some of the rules. However, remember that one exception may lead to many and eventually can destroy code consistency. As such, there should be very few exceptions. When modifying open-source code or third-party code, you can choose to use the code style from this open-source project (instead of using the existing specifications) to maintain consistency. The software that is directly based on the Android native operating system interface, such as the Android Framework, remains consistent with the Android style. ================================================ FILE: info/guide/guide-chapter-1.md ================================================ # 1. Naming It is not always easy to meaningfully and appropriately name variables, functions, classes, and so on. Using meaningful names in programming helps to clearly express the main ideas and functionality of your code and avoid its misinterpretation, unnecessary coding and decoding, "magic" numbers, and inappropriate abbreviations. Note: Source code files (incl. comments) must be UTF-8 encoded, no exceptions. The ASCII horizontal space (`0x20`, `U+0020`) is the only permitted whitespace character. Tab character (`0x09`, `U+0009`) should never be used for indentation. ### 1.1 Identifiers This section describes the general rules for naming identifiers. #### 1.1.1 Identifiers naming conventions For identifiers, use the following naming conventions: 1. All identifiers should use only ASCII letters or digits, and the names should match regular expressions `\w{2,64}`. Explanation: Each valid identifier name should match the regular expression `\w{2,64}`, which means that the name length is 2 to 64 characters, and the length of the variable name should be proportional to its life range and describe its functionality and responsibility. Length of names depends on the project. Nevertheless, name lengths of less than 31 characters are generally recommended. Otherwise, a class declaration with generics or inheritance from a superclass can cause line breaking. No special prefix or suffix should be used in names. The examples of inappropriate names are: name_, mName, s_name, and kName. 2. Choose file names that would describe the content. Use camel case (PascalCase) and `.kt` extension. 3. Typical examples of naming: | Meaning | Correct |Incorrect| | ---- | ---- | ---- | | "XML Http Request" | XmlHttpRequest | XMLHTTPRequest | | "new customer ID" | newCustomerId | newCustomerID | | "inner stopwatch" | innerStopwatch | innerStopWatch | | "supports IPv6 on iOS" | supportsIpv6OnIos | supportsIPv6OnIOS | | "YouTube importer" | YouTubeImporter | YoutubeImporter | 4. The usage of (``) and free naming for functions and identifiers are prohibited. For example, the following code is not recommended: ```kotlin val `my dummy name-with-minus` = "value" ``` The only exceptions are function names in `Unit tests.` 5. Backticks (``) must not be used for identifiers, except for the names of test methods (marked with @Test annotation): ```kotlin @Test fun `my test`() { /*...*/ } ``` 6. The following table contains some characters that may cause confusion. Be careful when using them as identifiers. To avoid issues, use other names instead. | Expected | Confusing name | Suggested name | | ------------- | ------------------------ | ---------------- | | 0 (zero) | O, D | obj, dgt | | 1 (one) | I, l | it, ln, line | | 2 (two) | Z | n1, n2 | | 5 (five) | S | xs, str | | 6 (six) | e | ex, elm | | 8 (eight) | B | bt, nxt | | n,h | h,n | nr, head, height | | rn, m | m,rn | mbr, item | **Exceptions:** - The `i`,`j`,`k` variables used in loops are part of the industry standard. A single character can be used for such variables. - The `e` variable can be used to catch exceptions in the catch block: `catch (e: Exception) {}` - The Java community normally does not recommend the use of prefixes. However, when developing the Android code, you can use the "s" and "m" prefixes for static and non-public non-static fields, respectively. Note that prefixing can also negatively affect the style and the auto-generation of getters and setters. | Type | Naming style | | ---- | ---- | | Interfaces, classes, annotations, enumerated types, and object type names | Camel case, starting with a capital letter. Test classes have a Test suffix. The filename is 'TopClassName'.kt. | | Class fields, local variables, methods, and method parameters | Camel case, starting with a low case letter. Test methods can be underlined with '_'; the only exception is [backing properties](#r6.1.7). | Static constants and enumerated values | Only the uppercase underlined with '_' | | Generic type variable | Single capital letter, which can be followed by a number, for example: `E, T, U, X, T2` | | Exceptions | Same as class names, but with a suffix Exception, for example: `AccessException` and `NullPointerException`| ### 1.2 Packages #### Rule 1.2.1 Package names dots Package names are in the lower case and separated by dots. The code developed within your company should start with `your.company.domain.` Numbers are permitted in package names. Each file should have a `package` directive. Package names are all written in lowercase, and consecutive words are concatenated (no underscores). Package names should contain both the product or module names and the department (or team) name to prevent conflicts with other teams. Numbers are not permitted. For example: `org.apache.commons.lang3`, `xxx.yyy.v2`. **Exceptions:** - In certain cases, such as open-source projects or commercial cooperation, package names should not start with `your.company.domain.` - If the package name starts with a number or other character that cannot be used at the beginning of the Java/Kotlin package name, then underscores are allowed. For example: `com.example._123name`. - Underscores are sometimes permitted if the package name contains reserved Java/Kotlin keywords, such as `org.example.hyphenated_name`, `int_.example`. **Valid example**: ```kotlin package your.company.domain.mobilecontrol.views ``` ### 1.3 Classes, enumerations, typealias, interfaces This section describes the general rules for naming classes, enumerations, and interfaces. ### 1.3.1 Classes, enumerations, typealias, interface names use Camel case Classes, enumerations, and interface names use `UpperCamelCase` nomenclature. Follow the naming rules described below: 1. A class name is usually a noun (or a noun phrase) denoted using the camel case nomenclature, such as the UpperCamelCase. For example: `Character` or `ImmutableList`. An interface name can also be a noun or noun phrase (such as `List`) or an adjective or adjective phrase (such as `Readable`). Note that verbs are not used to name classes. However, nouns (such as `Customer`, `WikiPage`, and `Account`) can be used. Try to avoid using vague words such as `Manager` and `Process`. 2. Test classes start with the name of the class they are testing and end with 'Test'. For example, `HashTest` or `HashIntegrationTest`. **Invalid example**: ```kotlin class marcoPolo {} class XMLService {} interface TAPromotion {} class info {} ``` **Valid example**: ```kotlin class MarcoPolo {} class XmlService {} interface TaPromotion {} class Order {} ``` ### 1.4 Functions This section describes the general rules for naming functions. ### 1.4.1 Function names should be in camel case Function names should use `lowerCamelCase` nomenclature. Follow the naming rules described below: 1. Function names are usually verbs or verb phrases denoted with the camel case nomenclature (`lowerCamelCase`). For example: `sendMessage`, `stopProcess`, or `calculateValue`. To name functions, use the following formatting rules: a) To get, modify, or calculate a certain value: get + non-boolean field(). Note that the Kotlin compiler automatically generates getters for some classes, applying the special syntax preferred for the 'get' fields: kotlin private val field: String get() { }. kotlin private val field: String get() { }. ```kotlin private val field: String get() { } ``` Note: The calling property access syntax is preferred to call getter directly. In this case, the Kotlin compiler automatically calls the corresponding getter. b) `is` + boolean variable name() c) `set` + field/attribute name(). However, note that the syntax and code generation for Kotlin are completely the same as those described for the getters in point a. d) `has` + Noun / adjective () e) verb() Note: Verb are primarily used for the action objects, such as `document.print ()` f) verb + noun() g) The Callback function allows the names that use the preposition + verb format, such as `onCreate()`, `onDestroy()`, `toString()`. **Invalid example**: ```kotlin fun type(): String fun Finished(): Boolean fun visible(boolean) fun DRAW() fun KeyListener(Listener) ``` **Valid example**: ```kotlin fun getType(): String fun isFinished(): Boolean fun setVisible(boolean) fun draw() fun addKeyListener(Listener) ``` 2. An underscore (`_`) can be included in the JUnit test function name and should be used as a separator. Each logical part is denoted in `lowerCamelCase`, for example, a typical pattern of using underscore: `pop_emptyStack`. ### 1.5 Constants This section describes the general rules for naming constraints. ### 1.5.1 Using UPPER case and underscore characters in a constraint name Constant names should be in the UPPER case, words separated by an underscore. The general constant naming conventions are listed below: 1. Constants are attributes created with the `const` keyword or top-level/`val` local variables of an object that holds immutable data. In most cases, constants can be identified as a `const val` property from the `object`/`companion object`/file top level. These variables contain fixed constant values that typically should never be changed by programmers. This includes basic types, strings, immutable types, and immutable collections of immutable types. The value is not constant for the object, which state can be changed. 2. Constant names should contain only uppercase letters separated by underscores. They should have a val or const val modifier to make them final explicitly. In most cases, if you need to specify a constant value, then you need to create it with the "const val" modifier. Note that not all `val` variables are constants. 3. Objects with immutable content, such as `Logger` and `Lock`, they can be in the uppercase as constants or have the Camel case as regular variables. 4. Use meaningful constants instead of `magic numbers`. SQL or logging strings should not be treated as magic numbers, nor should they be defined as string constants. Magic constants, such as `NUM_FIVE = 5` or `NUM_5 = 5`, should not be treated as constants. This is because mistakes will easily be made if they are changed to `NUM_5 = 50` or 55. These constants typically represent business logic values, such as measures, capacity, scope, location, tax rate, promotional discounts, and power base multiples in algorithms. You can avoid using magic numbers with the following method: - Using library functions and APIs. For example, instead of checking that `size == 0`, use `isEmpty()` function. To work with `time`, use built-ins from `java.time API`. - Enumerations can be used to name patterns. Refer to [Recommended usage scenario for enumeration in 3.9](#c3.9). **Invalid example**: ```kotlin var int MAXUSERNUM = 200; val String sL = "Launcher"; ``` **Valid example**: ```kotlin const val int MAX_USER_NUM = 200; const val String APPLICATION_NAME = "Launcher"; ``` ### 1.6 Non-constant fields (variables) This section describes the general rules for naming variables. ### 1.6.1 Non-constant field name Non-constant field names should use the Camel case and start with a lowercase letter. A local variable cannot be treated as constant, even if it is final and immutable. Therefore, it should not use the preceding rules. Names of collection type variables (sets, lists, etc.) should contain plural nouns. For example: `var namesList: List` Names of non-constant variables should use `lowerCamelCase`. The name of the final immutable field used to store the singleton object can use the same camel case notation. **Invalid example**: ```kotlin customername: String user: List = listof() ``` **Valid example**: ```kotlin var customerName: String val users: List = listOf(); val mutableCollection: MutableSet = HashSet() ``` ### 1.6.2 Boolean variable names with negative meaning Avoid using Boolean variable names with a negative meaning. When using a logical operator and name with a negative meaning, the code may be difficult to understand, which is referred to as the "double negative". For instance, it is not easy to understand the meaning of !isNotError. The JavaBeans specification automatically generates isXxx() getters for attributes of Boolean classes. However, not all methods returning the Boolean type have this notation. For Boolean local variables or methods, it is highly recommended that you add non-meaningful prefixes, including is (commonly used by JavaBeans), has, can, should, and must. Modern integrated development environments (IDEs), such as Intellij, can doing this when you generate getters in Java. For Kotlin, this process is even more straightforward as everything is on the byte-code level under the hood. **Invalid example**: ```kotlin val isNoError: Boolean val isNotFound: Boolean fun empty() fun next(); ``` **Valid example**: ```kotlin val isError: Boolean val isFound: Boolean val hasLicense: Boolean val canEvaluate: Boolean val shouldAbort: Boolean fun isEmpty() fun hasNext() ``` ================================================ FILE: info/guide/guide-chapter-2.md ================================================ # 2. Comments The best practice is to begin your code with a summary, which can be one sentence. Try to balance between writing no comments at all and obvious commentary statements for each line of code. Comments should be accurately and clearly expressed, without repeating the name of the class, interface, or method. Comments are not a solution to the wrong code. Instead, you should fix the code as soon as you notice an issue or plan to fix it (by entering a TODO comment, including a Jira number). Comments should accurately reflect the code's design ideas and logic and further describe its business logic. As a result, other programmers will be able to save time when trying to understand the code. Imagine that you are writing the comments to help yourself to understand the original ideas behind the code in the future. ### 2.1 General form of Kdoc KDoc is a combination of JavaDoc's block tags syntax (extended to support specific constructions of Kotlin) and Markdown's inline markup. The basic format of KDoc is shown in the following example: ```kotlin /** * There are multiple lines of KDoc text, * Other ... */ fun method(arg: String) { // ... } ``` It is also shown in the following single-line form: ```kotlin /** Short form of KDoc. */ ``` Use a single-line form when you store the entire KDoc block in one line (and there is no KDoc mark @XXX). For detailed instructions on how to use KDoc, refer to [Official Document](https://docs.oracle.com/en/Kotlin/Kotlinse/11/tools/KDoc.html). #### 2.1.1 Using KDoc for the public, protected, or internal code elements At a minimum, KDoc should be used for every public, protected, or internal decorated class, interface, enumeration, method, and member field (property). Instead of using comments or KDocs before properties in the primary constructor of a class - use `@property` tag in a KDoc of a class. All properties of the primary constructor should also be documented in a KDoc with a `@property` tag. **Incorrect example:** ```kotlin /** * Class description */ class Example( /** * property description */ val foo: Foo, // another property description val bar: Bar ) ``` **Correct example:** ```kotlin /** * Class description * @property foo property description * @property bar another property description */ class Example( val foo: Foo, val bar: Bar ) ``` **Exceptions:** * For setters/getters of properties, obvious comments (like `this getter returns field`) are optional. Note that Kotlin generates simple `get/set` methods under the hood. * It is optional to add comments for simple one-line methods, such as shown in the example below: ```kotlin val isEmpty: Boolean get() = this.size == 0 ``` or ```kotlin fun isEmptyList(list: List) = list.size == 0 ``` **Note:** You can skip KDocs for a method's override if it is almost the same as the superclass method. - - Don't use Kdoc comments inside code blocks as block comments **Incorrect Example:** ```kotlin class Example { fun doGood() { /** * wrong place for kdoc */ 1 + 2 } } ``` **Correct Example:** ```kotlin class Example { fun doGood() { /* * right place for block comment */ 1 + 2 } } ``` #### 2.1.2 Describing methods that have arguments, a return value, or can throw an exception in the KDoc block When the method has such details as arguments, return value, or can throw exceptions, it must be described in the KDoc block (with @param, @return, @throws, etc.). **Valid examples:** ```kotlin /** * This is the short overview comment for the example interface. * / * Add a blank line between the comment text and each KDoc tag underneath * / * @since 1.6 */ protected abstract class Sample { /** * This is a long comment with whitespace that should be split in * comments on multiple lines if the line comment formatting is enabled. * / * Add a blank line between the comment text and each KDoc tag underneath * / * @param fox A quick brown fox jumps over the lazy dog * @return battle between fox and dog */ protected abstract fun foo(Fox fox) /** * These possibilities include: Formatting of header comments * / * Add a blank line between the comment text and each KDoc tag underneath * / * @return battle between fox and dog * @throws ProblemException if lazy dog wins */ protected fun bar() throws ProblemException { // Some comments / * No need to add a blank line here * / var aVar = ... // Some comments / * Add a blank line before the comment * / fun doSome() } } ``` #### 2.1.3 Only one space between the Kdoc tag and content. Tags are arranged in the order. There should be only one space between the Kdoc tag and content. Tags are arranged in the following order: @param, @return, and @throws. Therefore, Kdoc should contain the following: - Functional and technical description, explaining the principles, intentions, contracts, API, etc. - The function description and @tags (`implSpec`, `apiNote`, and `implNote`) require an empty line after them. - `@implSpec`: A specification related to API implementation, and it should let the implementer decide whether to override it. - `@apiNote`: Explain the API precautions, including whether to allow null and whether the method is thread-safe, as well as the algorithm complexity, input, and output range, exceptions, etc. - `@implNote`: A note related to API implementation, which implementers should keep in mind. - One empty line, followed by regular `@param`, `@return`, `@throws`, and other comments. - The conventional standard "block labels" are arranged in the following order: `@param`, `@return`, `@throws`. Kdoc should not contain: - Empty descriptions in tag blocks. It is better not to write Kdoc than waste code line space. - There should be no empty lines between the method/class declaration and the end of Kdoc (`*/` symbols). - `@author` tag. It doesn't matter who originally created a class when you can use `git blame` or VCS of your choice to look through the changes history. Important notes: - KDoc does not support the `@deprecated` tag. Instead, use the `@Deprecated` annotation. - The `@since` tag should be used for versions only. Do not use dates in `@since` tag, it's confusing and less accurate. If a tag block cannot be described in one line, indent the content of the new line by *four spaces* from the `@` position to achieve alignment (`@` counts as one + three spaces). **Exception:** When the descriptive text in a tag block is too long to wrap, you can indent the alignment with the descriptive text in the last line. The descriptive text of multiple tags does not need to be aligned. See [3.8 Horizontal space](#c3.8). In Kotlin, compared to Java, you can put several classes inside one file, so each class should have a Kdoc formatted comment (as stated in rule 2.1). This comment should contain @since tag. The right style is to write the application version when its functionality is released. It should be entered after the `@since` tag. **Examples:** ```kotlin /** * Description of functionality * * @since 1.6 */ ``` Other KDoc tags (such as @param type parameters and @see.) can be added as follows: ```kotlin /** * Description of functionality * * @apiNote: Important information about API * * @since 1.6 */ ``` ### 2.2 Adding comments on the file header This section describes the general rules of adding comments on the file header. ### 2.2.1 Formatting of comments in the file header Comments on the file header should be placed before the package name and imports. If you need to add more content to the comment, subsequently add it in the same format. Comments on the file header must include copyright information, without the creation date and author's name (use VCS for history management). Also, describe the content inside files that contain multiple or no classes. The following examples for Huawei describe the format of the *copyright license*: \ Chinese version: `版权所有 (c) 华为技术有限公司 2012-2020` \ English version: `Copyright (c) Huawei Technologies Co., Ltd. 2012-2020. All rights reserved.` `2012` and `2020` are the years the file was first created and the current year, respectively. Do not place **release notes** in header, use VCS to keep track of changes in file. Notable changes can be marked in individual KDocs using `@since` tag with version. Invalid example: ```kotlin /** * Release notes: * 2019-10-11: added class Foo */ class Foo ``` Valid example: ```kotlin /** * @since 2.4.0 */ class Foo ``` - The **copyright statement** can use your company's subsidiaries, as shown in the below examples: \ Chinese version: `版权所有 (c) 海思半导体 2012-2020` \ English version: `Copyright (c) Hisilicon Technologies Co., Ltd. 2012-2020. All rights reserved.` - The copyright information should not be written in KDoc style or use single-line comments. It must start from the beginning of the file. The following example is a copyright statement for Huawei, without other functional comments: ```kotlin /* * Copyright (c) Huawei Technologies Co., Ltd. 2012-2020. All rights reserved. */ ``` The following factors should be considered when writing the file header or comments for top-level classes: - File header comments must start from the top of the file. If it is a top-level file comment, there should be a blank line after the last Kdoc `*/` symbol. If it is a comment for a top-level class, the class declaration should start immediately without using a newline. - Maintain a unified format. The specific format can be formulated by the project (for example, if you use an existing opensource project), and you need to follow it. - A top-level file-Kdoc must include a copyright and functional description, especially if there is more than one top-level class. - Do not include empty comment blocks. If there is no content after the option `@apiNote`, the entire tag block should be deleted. - The industry practice is not to include historical information in the comments. The corresponding history can be found in VCS (git, svn, etc.). Therefore, it is not recommended to include historical data in the comments of the Kotlin source code. ### 2.3 Comments on the function header Comments on the function header are placed above function declarations or definitions. A newline should not exist between a function declaration and its Kdoc. Use the preceding <> style rules. As stated in Chapter 1, the function name should reflect its functionality as much as possible. Therefore, in the Kdoc, try to describe the functionality that is not mentioned in the function name. Avoid unnecessary comments on dummy coding. The function header comment's content is optional, but not limited to function description, return value, performance constraints, usage, memory conventions, algorithm implementation, reentrant requirements, etc. ### 2.4 Code comments This section describes the general rules of adding code comments. #### 2.4.1 Add a blank line between the body of the comment and Kdoc tag-blocks. It is a good practice to add a blank line between the body of the comment and Kdoc tag-blocks. Also, consider the following rules: - There must be one space between the comment character and the content of the comment. - There must be a newline between a Kdoc and the presiding code. - An empty line should not exist between a Kdoc and the code it is describing. You do not need to add a blank line before the first comment in a particular namespace (code block) (for example, between the function declaration and first comment in a function body). **Valid Examples:** ```kotlin /** * This is the short overview comment for the example interface. * * @since 1.6 */ public interface Example { // Some comments /* Since it is the first member definition in this code block, there is no need to add a blank line here */ val aField: String = ... /* Add a blank line above the comment */ // Some comments val bField: String = ... /* Add a blank line above the comment */ /** * This is a long comment with whitespace that should be split in * multiple line comments in case the line comment formatting is enabled. * /* blank line between description and Kdoc tag */ * @param fox A quick brown fox jumps over the lazy dog * @return the rounds of battle of fox and dog */ fun foo(Fox fox) /* Add a blank line above the comment */ /** * These possibilities include: Formatting of header comments * * @return the rounds of battle of fox and dog * @throws ProblemException if lazy dog wins */ fun bar() throws ProblemException { // Some comments /* Since it is the first member definition in this range, there is no need to add a blank line here */ var aVar = ... // Some comments /* Add a blank line above the comment */ fun doSome() } } ``` - Leave one single space between the comment on the right side of the code and the code. If you use conditional comments in the `if-else-if` scenario, put the comments inside the `else-if` branch or in the conditional block, but not before the `else-if`. This makes the code more understandable. When the if-block is used with curly braces, the comment should be placed on the next line after opening the curly braces. Compared to Java, the `if` statement in Kotlin statements returns a value. For this reason, a comment block can describe a whole `if-statement`. **Valid examples:** ```kotlin val foo = 100 // right-side comment val bar = 200 /* right-side comment */ // general comment for the value and whole if-else condition val someVal = if (nr % 15 == 0) { // when nr is a multiple of both 3 and 5 println("fizzbuzz") } else if (nr % 3 == 0) { // when nr is a multiple of 3, but not 5 // We print "fizz", only. println("fizz") } else if (nr % 5 == 0) { // when nr is a multiple of 5, but not 3 // we print "buzz" only. println("buzz") } else { // otherwise, we print the number. println(x) } ``` - Start all comments (including KDoc) with a space after the first symbol (`//`, `/*`, `/**` and `*`) **Valid example:** ```kotlin val x = 0 // this is a comment ``` #### 2.4.2 Do not comment on unused code blocks Do not comment on unused code blocks, including imports. Delete these code blocks immediately. A code is not used to store history. Git, svn, or other VCS tools should be used for this purpose. Unused imports increase the coupling of the code and are not conducive to maintenance. The commented out code cannot be appropriately maintained. In an attempt to reuse the code, there is a high probability that you will introduce defects that are easily missed. The correct approach is to delete the unnecessary code directly and immediately when it is not used anymore. If you need the code again, consider porting or rewriting it as changes could have occurred since you first commented on the code. #### 2.4.3 Code delivered to the client should not contain TODO/FIXME comments The code officially delivered to the client typically should not contain TODO/FIXME comments. `TODO` comments are typically used to describe modification points that need to be improved and added. For example, refactoring FIXME comments are typically used to describe known defects and bugs that will be subsequently fixed and are not critical for an application. They should all have a unified style to facilitate unified text search processing. **Example:** ```kotlin // TODO(): Jira-XXX - support new json format // FIXME: Jira-XXX - fix NPE in this code block ``` At a version development stage, these annotations can be used to highlight the issues in the code, but all of them should be fixed before a new product version is released. ================================================ FILE: info/guide/guide-chapter-3.md ================================================ # 3. General formatting (typesetting) ### 3.1 File-related rules This section describes the rules related to using files in your code. #### 3.1.1 Avoid files that are too long If the file is too long and complicated, it should be split into smaller files, functions, or modules. Files should not exceed 2000 lines (non-empty and non-commented lines). It is recommended to horizontally or vertically split the file according to responsibilities or hierarchy of its parts. The only exception to this rule is code generation - the auto-generated files that are not manually modified can be longer. #### 3.1.2 Code blocks in the source file should be separated by one blank line A source file contains code blocks in the following order: copyright, package name, imports, and top-level classes. They should be separated by one blank line. a) Code blocks should be in the following order: 1. Kdoc for licensed or copyrighted files 2. `@file` annotation 3. Package name 4. Import statements 5. Top-class header and top-function header comments 6. Top-level classes or functions b) Each of the preceding code blocks should be separated by a blank line. c) Import statements are alphabetically arranged, without using line breaks and wildcards ( wildcard imports - `*`). d) **Recommendation**: One `.kt` source file should contain only one class declaration, and its name should match the filename e) Avoid empty files that do not contain the code or contain only imports/comments/package name f) Unused imports should be removed #### 3.1.3 Import statements order From top to bottom, the order is the following: 1. Android 2. Imports of packages used internally in your organization 3. Imports from other non-core dependencies 4. Java core packages 5. kotlin stdlib Each category should be alphabetically arranged. Each group should be separated by a blank line. This style is compatible with [Android import order](https://source.android.com/setup/contribute/code-style#order-import-statements). **Valid example**: ```kotlin import android.* // android import androidx.* // android import com.android.* // android import com.your.company.* // your company's libs import your.company.* // your company's libs import com.fasterxml.jackson.databind.ObjectMapper // other third-party dependencies import org.junit.jupiter.api.Assertions import java.io.IOException // java core packages import java.net.URL import kotlin.system.exitProcess // kotlin standard library import kotlinx.coroutines.* // official kotlin extension library ``` #### 3.1.4 Order of declaration parts of class-like code structures The declaration parts of class-like code structures (class, interface, etc.) should be in the following order: compile-time constants (for objects), class properties, late-init class properties, init-blocks, constructors, public methods, internal methods, protected methods, private methods, and companion object. Blank lines should separate their declaration. Notes: 1. There should be no blank lines between properties with the following **exceptions**: when there is a comment before a property on a separate line or annotations on a separate line. 2. Properties with comments/Kdoc should be separated by a newline before the comment/Kdoc. 3. Enum entries and constant properties (`const val`) in companion objects should be alphabetically arranged. The declaration part of a class or interface should be in the following order: - Compile-time constants (for objects) - Properties - Late-init class properties - Init-blocks - Constructors - Methods or nested classes. Put nested classes next to the code they are used by. If the classes are meant to be used externally, and are not referenced inside the class, put them after the companion object. - Companion object **Exception:** All variants of a `private val` logger should be placed at the beginning of the class (`private val log`, `LOG`, `logger`, etc.). #### 3.1.5 Order of declaration of top-level code structures Kotlin allows several top-level declaration types: classes, objects, interfaces, properties and functions. When declaring more than one class or zero classes (e.g. only functions), as per rule [2.2.1](#r2.2.1), you should document the whole file in the header KDoc. When declaring top-level structures, keep the following order: 1. Top-level constants and properties (following same order as properties inside a class: `const val`,`val`, `lateinit var`, `var`) 2. Type aliases (grouped by their visibility modifiers) 2. Interfaces, classes and objects (grouped by their visibility modifiers) 3. Extension functions 4. Other functions **Note**: Extension functions shouldn't have receivers declared in the same file according to [rule 6.2.3](#r6.2.3) Valid example: ```kotlin package com.saveourtool.diktat.example const val CONSTANT = 42 val topLevelProperty = "String constant" internal typealias ExamplesHandler = (IExample) -> Unit interface IExample class Example : IExample private class Internal fun Other.asExample(): Example { /* ... */ } private fun Other.asInternal(): Internal { /* ... */ } fun doStuff() { /* ... */ } ``` **Note**: kotlin scripts (.kts) allow arbitrary code to be placed on the top level. When writing kotlin scripts, you should first declare all properties, classes and functions. Only then you should execute functions on top level. It is still recommended wrapping logic inside functions and avoid using top-level statements for function calls or wrapping blocks of code in top-level scope functions like `run`. Example: ```kotlin /* class declarations */ /* function declarations */ run { // call functions here } ``` ### 3.2 Braces This section describes the general rules of using braces in your code. #### 3.2.1 Using braces in conditional statements and loop blocks Braces should always be used in `if`, `else`, `for`, `do`, and `while` statements, even if the program body is empty or contains only one statement. In special Kotlin `when` statements, you do not need to use braces for single-line statements. **Valid example:** ```kotlin when (node.elementType) { FILE -> { checkTopLevelDoc(node) checkSomething() } CLASS -> checkClassElements(node) } ``` **Exception:** The only exception is ternary operator in Kotlin (a single line `if () <> else <>` ) **Invalid example:** ```kotlin val value = if (string.isEmpty()) // WRONG! 0 else 1 ``` **Valid example**: ```kotlin val value = if (string.isEmpty()) 0 else 1 // Okay ``` ```kotlin if (condition) { println("test") } else { println(0) } ``` #### 3.2.2 Opening braces are placed at the end of the line in *non-empty* blocks and block structures For *non-empty* blocks and block structures, the opening brace is placed at the end of the line. Follow the K&R style (1TBS or OTBS) for *non-empty* code blocks with braces: - The opening brace and first line of the code block are on the same line. - The closing brace is on its own new line. - The closing brace can be followed by a newline character. The only exceptions are `else`, `finally`, and `while` (from `do-while` statement), or `catch` keywords. These keywords should not be split from the closing brace by a newline character. **Exception cases**: 1) For lambdas, there is no need to put a newline character after the first (function-related) opening brace. A newline character should appear only after an arrow (`->`) (see [point 5 of Rule 3.6.2](#r3.6.2)). ```kotlin arg.map { value -> foo(value) } ``` 2) for `else`/`catch`/`finally`/`while` (from `do-while` statement) keywords closing brace should stay on the same line: ```kotlin do { if (true) { x++ } else { x-- } } while (x > 0) ``` **Valid example:** ```kotlin return arg.map { value -> while (condition()) { method() } value } return MyClass() { @Override fun method() { if (condition()) { try { something() } catch (e: ProblemException) { recover() } } else if (otherCondition()) { somethingElse() } else { lastThing() } } } ``` ### 3.3 Indentation Only spaces are permitted for indentation, and each indentation should equal `four spaces` (tabs are not permitted). If you prefer using tabs, simply configure them to change to spaces in your IDE automatically. These code blocks should be indented if they are placed on the new line, and the following conditions are met: - The code block is placed immediately after an opening brace. - The code block is placed after each operator, including the assignment operator (`+`/`-`/`&&`/`=`/etc.) - The code block is a call chain of methods: ```kotlin someObject .map() .filter() ``` - The code block is placed immediately after the opening parenthesis. - The code block is placed immediately after an arrow in lambda: ```kotlin arg.map { value -> foo(value) } ``` **Exceptions**: 1. Argument lists: \ a) Eight spaces are used to indent argument lists (both in declarations and at call sites). \ b) Arguments in argument lists can be aligned if they are on different lines. 2. Eight spaces are used if there is a newline after any binary operator. 3. Eight spaces are used for functional-like styles when the newline is placed before the dot. 4. Supertype lists: \ a) Four spaces are used if the colon before the supertype list is on a new line. \ b) Four spaces are used before each supertype, and eight spaces are used if the colon is on a new line. **Note:** there should be an indentation after all statements such as `if`, `for`, etc. However, according to this code style, such statements require braces. ```kotlin if (condition) foo() ``` **Exceptions**: - When breaking the parameter list of a method/class constructor, it can be aligned with `8 spaces`. A parameter that was moved to a new line can be on the same level as the previous argument: ```kotlin fun visit( node: ASTNode, autoCorrect: Boolean, params: KtLint.ExperimentalParams, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit ) { } ``` - Such operators as `+`/`-`/`*` can be indented with `8 spaces`: ```kotlin val abcdef = "my split" + " string" ``` - Opening and closing quotes in multiline string with [`trimMargin()`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/trim-margin.html) or [`trimIndent()`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/trim-indent.html) method should have the same indentation: ```kotlin lintMethod( """ |val q = 1 | """.trimMargin() ) ``` - A list of supertypes should be indented with `4 spaces` if they are on different lines or with `8 spaces` if the leading colon is also on a separate line ```kotlin class A : B() class A : B() ``` ### 3.4 Empty blocks Avoid empty blocks, and ensure braces start on a new line. An empty code block can be closed immediately on the same line and the next line. However, a newline is recommended between opening and closing braces `{}` (see the examples below.) Generally, empty code blocks are prohibited; using them is considered a bad practice (especially for catch block). They are appropriate for overridden functions, when the base class's functionality is not needed in the class-inheritor, for lambdas used as a function and for empty function in implementation of functional interface. ```kotlin override fun foo() { } ``` **Valid examples** (note once again that generally empty blocks are prohibited): ```kotlin fun doNothing() {} fun doNothingElse() { } fun foo(bar: () -> Unit = {}) ``` **Invalid examples:** ```kotlin try { doSomething() } catch (e: Some) {} ``` Use the following valid code instead: ```kotlin try { doSomething() } catch (e: Some) { } ``` ### 3.5 Line length Line length should be less than 120 symbols. Otherwise, it should be split. if the initializer of a `complex property` is too long, it should be split into multiple lines using the operators grouped in the following precedence order: 1. Logic Binary Expression (`&&` , `||`) 2. Comparison Binary Expression (`>` , `<` , `==` , `>=` , `<=` , `!=`, `===`, `!==`) 3. Other types (Arithmetical and Bitwise operation) (`+` , `-` , `*` , `/` , `%` , `>>` , `<<` , `&` , `|` , `~` , `^` , `>>>` , `<<<` , `*=` , `+=` , `-=` , `/=` , `%=` , `++` , `--` , `in` , `!in` etc) **Invalid example:** ```kotlin val complexProperty = 1 + 2 + 3 + 4 ``` **Valid example:** ```kotlin val complexProperty = 1 + 2 + 3 + 4 ``` **Invalid example:** ```kotlin val complexProperty = (1 + 2 + 3 > 0) && ( 23 * 4 > 10 * 6) ``` **Valid example:** ```kotlin val complexProperty = (1 + 2 + 3 > 0) && (23 * 4 > 10 * 6) ``` A long expression which should be split into two lines before the `Elvis Operator` (`?:`): **Invalid example:** ```kotlin val value = first ?: second ``` **Valid example:** ```kotlin val value = first ?: second ``` A long line in a `Dot Qualified Expression` or a `Safe Access Expression`: **Invalid example:** ```kotlin val value = This.Is.A.Very.Long.Dot.Qualified.Expression ``` **Valid example:** ```kotlin val value = This.Is.A.Very.Long .Dot.Qualified.Expression ``` **Invalid example:** ```kotlin val value = This.Is?.A?.Very?.Long?.Safe?.Access?.Expression ``` **Valid example:** ```kotlin val value = This.Is?.A?.Very?.Long ?.Safe?.Access?.Expression ``` A long list of `function call arguments`: **Invalid example:** ```kotlin val result1 = ManyParamInFunction(firstArgument, secondArgument, thirdArgument, fourthArgument, fifthArguments) ``` **Valid example:** ```kotlin val result1 = ManyParamInFunction(firstArgument, secondArgument, thirdArgument, fourthArgument, fifthArguments) ``` If an `annotation` is too long, it also should be split: **Invalid example:** ```kotlin @Query(value = "select * from table where age = 10", nativeQuery = true) fun foo() {} ``` **Valid example:** ```kotlin @Query( value = "select * from table where age = 10", nativeQuery = true) fun foo() {} ``` A long one line `function` should be split: **Invalid example:** ```kotlin fun foo() = goo().write("TooLong") ``` **Valid example:** ```kotlin fun foo() = goo().write("TooLong") ``` A `long binary expression` should be split into multiple lines using the operators grouped in the following precedence: 1. Logic Binary Expression (`&&` , `||`) 2. Comparison Binary Expression (`>` , `<` , `==` , `>=` , `<=` , `!=`, `===`, `!==`) 3. Other types (Arithmetical and Bitwise operation) (`+` , `-` , `*` , `/` , `%` , `>>` , `<<` , `&` , `|` , `~` , `^` , `>>>` , `<<<` , `*=` , `+=` , `-=` , `/=` , `%=` , `++` , `--` , `in` , `!in` etc) **Invalid example:** ```kotlin if (( x > 100) || y < 100 && !isFoo()) {} ``` **Valid example:** ```kotlin if (( x > 100) || y < 100 && !isFoo()) {} ``` A long `string template` should be split into several lines: **Invalid example:** ```kotlin val nameString = "This is a very long string template" ``` **Valid example:** ```kotlin val nameString = "This is a very long" + " string template" ``` A long `lambda argument` should be split: **Invalid example:** ```kotlin val variable = a?.filter { it.elementType == IDENTIFIER } ?: null ``` **Valid example:** ```kotlin val variable = a?.filter { it.elementType == IDENTIFIER } ?: null ``` A long `when-expression` body spanning only a single line should be split: **Invalid example:** ```kotlin when (argument) { true -> long.argument.whenEntry } ``` **Valid example:** ```kotlin when (argument) { true -> { long.argument.whenEntry } } ``` If none of the above examples are suitable but a code fragment is a property declaration which needs to be split into multiple lines, then the property initializer can be moved into a separate line: **Invalid example:** ```kotlin val element = veryLongNameFunction(firstParam) ``` **Valid example:** ```kotlin val element = veryLongNameFunction(firstParam) ``` An `eol comment` also can be split, but it depends on comment location. If the comment shares the same line with code, it should be moved to the line above: **Invalid example:** ```kotlin fun foo() { val name = "Nick" // this comment is too long } ``` **Valid example:** ```kotlin fun foo() { // this comment is too long val name = "Nick" } ``` But if this comment is on new line - it should be split to several lines: **Invalid example:** ```kotlin // This comment is too long. It should be on two lines. fun foo() {} ``` **Valid example:** ```kotlin // This comment is too long. // It should be on two lines. fun foo() {} ``` The international code style prohibits `non-Latin` (`non-ASCII`) symbols. (See [Identifiers](#r1.1.1)) However, if you still intend on using them, follow the following convention: - One wide character occupies the width of two narrow characters. The "wide" and "narrow" parts of a character are defined by its [east Asian width Unicode attribute](https://unicode.org/reports/tr11/). Typically, narrow characters are also called "half-width" characters. All characters in the ASCII character set include letters (such as `a, A`), numbers (such as `0, 3`), and punctuation spaces (such as `,` , `{`), all of which are narrow characters. Wide characters are also called "full-width" characters. Chinese characters (such as `中, 文`), Chinese punctuation (`,` , `;` ), full-width letters and numbers (such as `A、3`) are "full-width" characters. Each one of these characters represents two narrow characters. - Any line that exceeds this limit (`120 narrow symbols`) should be wrapped, as described in the [Newline section](#c3.5). **Exceptions:** 1. The long URL or long JSON method reference in KDoc. 2. The `package` and `import` statements. 3. The command line in the comment, enabling it to be cut and pasted into the shell for use. ### 3.6 Line breaks (newlines) This section contains the rules and recommendations on using line breaks. #### 3.6.1 Each line can have a maximum of one statement Each line can have a maximum of one code statement. This recommendation prohibits the use of code with `;` because it decreases code visibility. **Invalid example:** ```kotlin val a = ""; val b = "" ``` **Valid example:** ```kotlin val a = "" val b = "" ``` #### 3.6.2 Rules for line-breaking 1) Unlike Java, Kotlin allows you not to put a semicolon (`;`) after each statement separated by a newline character. There should be no redundant semicolon at the end of the lines. When a newline character is needed to split the line, it should be placed after such operators as `&&`/`||`/`+`/etc. and all infix functions (for example, `xor`). However, the newline character should be placed before operators such as `.`, `?.`, `?:`, and `::`. Note that all comparison operators, such as `==`, `>`, `<`, should not be split. **Invalid example**: ```kotlin if (node != null && test != null) {} ``` **Valid example**: ```kotlin if (node != null && test != null) { } ``` **Note:** You need to follow the functional style, meaning each function call in a chain with `.` should start at a new line if the chain of functions contains more than one call: ```kotlin val value = otherValue!! .map { x -> x } .filter { val a = true true } .size ``` **Note:** The parser prohibits the separation of the `!!` operator from the value it is checking. **Exception**: If a functional chain is used inside the branches of a ternary operator, it does not need to be split with newlines. **Valid example**: ```kotlin if (condition) list.map { foo(it) }.filter { bar(it) } else list.drop(1) ``` **Note:** If dot qualified expression is inside condition or passed as an argument - it should be replaced with new variable. **Invalid example**: ```kotlin if (node.treeParent.treeParent?.treeParent.findChildByType(IDENTIFIER) != null) {} ``` **Valid example**: ```kotlin val grandIdentifier = node .treeParent .treeParent ?.treeParent .findChildByType(IDENTIFIER) if (grandIdentifier != null) {} ``` **Another valid example**: ```kotlin val grandIdentifier = node.treeParent .treeParent ?.treeParent .findChildByType(IDENTIFIER) if (grandIdentifier != null) {} ``` 2) Newlines should be placed after the assignment operator (`=`). 3) In function or class declarations, the name of a function or constructor should not be split by a newline from the opening brace `(`. A brace should be placed immediately after the name without any spaces in declarations or at call sites. 4) Newlines should be placed right after the comma (`,`). 5) If a lambda statement contains more than one line in its body, a newline should be placed after an arrow if the lambda statement has explicit parameters. If it uses an implicit parameter (`it`), the newline character should be placed after the opening brace (`{`). The following examples illustrate this rule: **Invalid example:** ```kotlin value.map { name -> foo() bar() } ``` **Valid example:** ```kotlin value.map { name -> foo() bar() } val someValue = { node:String -> node } ``` 6) When a function body consists of only a single expression, it can be re-written as an [_expression function_](https://kotlinlang.org/docs/reference/functions.html#single-expression-functions). Instead of: ```kotlin override fun toString(): String { return "hi" } ``` use: ```kotlin override fun toString() = "hi" ``` 7) If an argument list in a function declaration (including constructors) or function call contains more than two arguments, these arguments should be split by newlines in the following style. **Valid example:** ```kotlin class Foo(val a: String, b: String, val c: String) { } fun foo( a: String, b: String, c: String ) { } ``` If and only if the first parameter is on the same line as an opening parenthesis, all parameters can be horizontally aligned by the first parameter. Otherwise, there should be a line break after an opening parenthesis. Kotlin 1.4 introduced a trailing comma as an optional feature, so it is generally recommended to place all parameters on a separate line and append [trailing comma](https://kotlinlang.org/docs/reference/whatsnew14.html#trailing-comma). It makes the resolving of merge conflicts easier. **Valid example:** ```kotlin fun foo( a: String, b: String, ) { } ``` same should be done for function calls/constructor arguments/e.t.c Kotlin supports trailing commas in the following cases: Enumerations Value arguments Class properties and parameters Function value parameters Parameters with optional type (including setters) Indexing suffix Lambda parameters when entry Collection literals (in annotations) Type arguments Type parameters Destructuring declarations 8) If the supertype list has more than two elements, they should be separated by newlines. **Valid example:** ```kotlin class MyFavouriteVeryLongClassHolder : MyLongHolder(), SomeOtherInterface, AndAnotherOne { } ``` ### 3.7 Using blank lines Reduce unnecessary blank lines and maintain a compact code size. By reducing unnecessary blank lines, you can display more code on one screen, which improves code readability. - Blank lines should separate content based on relevance and should be placed between groups of fields, constructors, methods, nested classes, `init` blocks, and objects (see [3.1.2](#r3.1.2)). - Do not use more than one line inside methods, type definitions, and initialization expressions. - Generally, do not use more than two consecutive blank lines in a row. - Do not put newlines in the beginning or end of code blocks with curly braces. **Valid example:** ```kotlin fun baz() { doSomething() // No need to add blank lines at the beginning and end of the code block // ... } ``` ### 3.8 Horizontal space This section describes general rules and recommendations for using spaces in the code. #### 3.8.1: Usage of whitespace for code separation Follow the recommendations below for using space to separate keywords: **Note:** These recommendations are for cases where symbols are located on the same line. However, in some cases, a line break could be used instead of a space. 1. Separate keywords (such as `if`, `when`, `for`) from the opening parenthesis with single whitespace. The only exception is the `constructor` keyword, which should not be separated from the opening parenthesis. 2. Separate keywords like `else` or `try` from the opening brace (`{`) with single whitespace. If `else` is used in a ternary-style statement without braces, there should be a single space between `else` and the statement after: `if (condition) foo() else bar()` 3. Use a **single** whitespace before all opening braces (`{`). The only exception is the passing of a lambda as a parameter inside parentheses: ```kotlin private fun foo(a: (Int) -> Int, b: Int) {} foo({x: Int -> x}, 5) // no space before '{' ``` 4. Single whitespace should be placed on both sides of binary operators. This also applies to operator-like symbols. For example: - A colon in generic structures with the `where` keyword: `where T : Type` - Arrow in lambdas: `(str: String) -> str.length()` **Exceptions:** - Two colons (`::`) are written without spaces:\ `Object::toString` - The dot separator (`.`) that stays on the same line with an object name:\ `object.toString()` - Safe access modifiers `?.` and `!!` that stay on the same line with an object name:\ `object?.toString()` - Operator `..` for creating ranges:\ `1..100` 5. Use spaces after (`,`), (`:`), and (`;`), except when the symbol is at the end of the line. However, note that this code style prohibits the use of (`;`) in the middle of a line ([see 3.3.2](#r3.2.2)). There should be no whitespaces at the end of a line. The only scenario where there should be no space after a colon is when the colon is used in the annotation to specify a use-site target (for example, `@param:JsonProperty`). There should be no spaces before `,` , `:` and `;`. **Exceptions** for spaces and colons: - When `:` is used to separate a type and a supertype, including an anonymous object (after object keyword) - When delegating to a superclass constructor or different constructor of the same class **Valid example:** ```kotlin abstract class Foo : IFoo { } class FooImpl : Foo() { constructor(x: String) : this(x) { /*...*/ } val x = object : IFoo { /*...*/ } } ``` 6. There should be *only one space* between the identifier and its type: `list: List` If the type is nullable, there should be no space before `?`. 7. When using `[]` operator (`get/set`) there should be **no** spaces between identifier and `[` : `someList[0]`. 8. There should be no space between a method or constructor name (both at declaration and at call site) and a parenthesis: `foo() {}`. Note that this sub-rule is related only to spaces; the rules for whitespaces are described in [see 3.6.2](#r3.6.2). This rule does not prohibit, for example, the following code: ```kotlin fun foo ( a: String ) ``` 9. Never put a space after `(`, `[`, `<` (when used as a bracket in templates) or before `)`, `]`, `>` (when used as a bracket in templates). 10. There should be no spaces between a prefix/postfix operator (like `!!` or `++`) and its operand. #### 3.8.2: No spaces for horizontal alignment *Horizontal alignment* refers to aligning code blocks by adding space to the code. Horizontal alignment should not be used because: - When modifying code, it takes much time for new developers to format, support, and fix alignment issues. - Long identifier names will break the alignment and lead to less presentable code. - There are more disadvantages than advantages in alignment. To reduce maintenance costs, misalignment (???) is the best choice. Recommendation: Alignment only looks suitable for `enum class`, where it can be used in table format to improve code readability: ```kotlin enum class Warnings(private val id: Int, private val canBeAutoCorrected: Boolean, private val warn: String) : Rule { PACKAGE_NAME_MISSING (1, true, "no package name declared in a file"), PACKAGE_NAME_INCORRECT_CASE (2, true, "package name should be completely in a lower case"), PACKAGE_NAME_INCORRECT_PREFIX(3, false, "package name should start from the company's domain") ; } ``` **Valid example:** ```kotlin private val nr: Int // no alignment, but looks fine private var color: Color // no alignment ``` **Invalid example**: ```kotlin private val nr: Int // aligned comment with extra spaces private val color: Color // alignment for a comment and alignment for identifier name ``` ### 3.9 Enumerations Enum values are separated by a comma and line break, with ';' placed on the new line. 1) The comma and line break characters separate enum values. Put `;` on the new line: ```kotlin enum class Warnings { A, B, C, ; } ``` This will help to resolve conflicts and reduce the number of conflicts during merging pull requests. Also, use [trailing comma](https://kotlinlang.org/docs/reference/whatsnew14.html#trailing-comma). 2) If the enum is simple (no properties, methods, and comments inside), you can declare it in a single line: ```kotlin enum class Suit { CLUBS, HEARTS, SPADES, DIAMONDS } ``` 3) Enum classes take preference (if it is possible to use it). For example, instead of two boolean properties: ```kotlin val isCelsius = true val isFahrenheit = false ``` use enum class: ```kotlin enum class TemperatureScale { CELSIUS, FAHRENHEIT } ``` - The variable value only changes within a fixed range and is defined with the enum type. - Avoid comparison with magic numbers of `-1, 0, and 1`; use enums instead. ```kotlin enum class ComparisonResult { ORDERED_ASCENDING, ORDERED_SAME, ORDERED_DESCENDING, ; } ``` ### 3.10 Variable declaration This section describes rules for the declaration of variables. #### 3.10.1 Declare one variable per line Each property or variable must be declared on a separate line. **Invalid example**: ```kotlin val n1: Int; val n2: Int ``` #### 3.10.2 Variables should be declared near the line where they are first used Declare local variables close to the point where they are first used to minimize their scope. This will also increase the readability of the code. Local variables are usually initialized during their declaration or immediately after. The member fields of the class should be declared collectively (see [Rule 3.1.2](#r3.1.2) for details on the class structure). ### 3.11 'When' expression The `when` statement must have an 'else' branch unless the condition variable is enumerated or a sealed type. Each `when` statement should contain an `else` statement group, even if it does not contain any code. **Exception:** If 'when' statement of the `enum or sealed` type contains all enum values, there is no need to have an "else" branch. The compiler can issue a warning when it is missing. ### 3.12 Annotations This section contains recommendations regarding annotations. #### 3.12.1 Whitespaces and newlines for annotations Each annotation applied to a class, method or constructor should be placed on its own line. Consider the following examples: 1. Annotations applied to the class, method or constructor are placed on separate lines (one annotation per line). **Valid example**: ```kotlin @MustBeDocumented @CustomAnnotation fun getNameIfPresent() { /* ... */ } ``` 2. A single annotation should be on the same line as the code it is annotating. **Valid example**: ```kotlin @CustomAnnotation class Foo {} ``` 3. Multiple annotations applied to a field or property can appear on the same line as the corresponding field. **Valid example**: ```kotlin @MustBeDocumented @CustomAnnotation val loader: DataLoader ``` #### 3.12.2 Preview annotation `@Preview` (Jetpack Compose) functions should end with 'Preview' suffix and are also be private **Valid example**: ```kotlin @Preview @Composable private fun BannerPreview() {} ``` ### 3.13 Block comments Block comments should be placed at the same indentation level as the surrounding code. See examples below. **Valid example**: ```kotlin class SomeClass { /* * This is * okay */ fun foo() {} } ``` **Note**: Use `/*...*/` block comments to enable automatic formatting by IDEs. ### 3.14 Modifiers and constant values This section contains recommendations regarding modifiers and constant values. #### 3.14.1 Declaration with multiple modifiers If a declaration has multiple modifiers, always follow the proper sequence. `Value` identifier supported in Kotlin 1.5 **Valid sequence:** ```kotlin public / internal / protected / private expect / actual final / open / abstract / sealed / const external override lateinit tailrec crossinline vararg suspend inner out enum / annotation companion value / inline / noinline reified infix operator data ``` #### 3.14.2: Separate long numerical values with an underscore An underscore character should separate long numerical values. **Note:** Using underscores simplifies reading and helps to find errors in numeric constants. ```kotlin val oneMillion = 1_000_000 val creditCardNumber = 1234_5678_9012_3456L val socialSecurityNumber = 999_99_9999L val hexBytes = 0xFF_EC_DE_5E val bytes = 0b11010010_01101001_10010100_10010010 ``` #### 3.14.3: Magic number Prefer defining constants with clear names describing what the magic number means. **Valid example**: ```kotlin class Person() { fun isAdult(age: Int): Boolean = age >= majority companion object { private const val majority = 18 } } ``` **Invalid example**: ```kotlin class Person() { fun isAdult(age: Int): Boolean = age >= 18 } ``` ### 3.15 Strings This section describes the general rules of using strings. #### 3.15.1 Concatenation of Strings String concatenation is prohibited if the string can fit on one line. Use raw strings and string templates instead. Kotlin has significantly improved the use of Strings: [String templates](https://kotlinlang.org/docs/reference/basic-types.html#string-templates), [Raw strings](https://kotlinlang.org/docs/reference/basic-types.html#string-literals). Therefore, compared to using explicit concatenation, code looks much better when proper Kotlin strings are used for short lines, and you do not need to split them with newline characters. **Invalid example**: ```kotlin val myStr = "Super string" val value = myStr + " concatenated" ``` **Valid example**: ```kotlin val myStr = "Super string" val value = "$myStr concatenated" ``` #### 3.15.2 String template format **Redundant curly braces in string templates** If there is only one variable in a string template, there is no need to use such a template. Use this variable directly. **Invalid example**: ```kotlin val someString = "${myArgument} ${myArgument.foo()}" ``` **Valid example**: ```kotlin val someString = "$myArgument ${myArgument.foo()}" ``` **Redundant string template** In case a string template contains only one variable - there is no need to use the string template. Use this variable directly. **Invalid example**: ```kotlin val someString = "$myArgument" ``` **Valid example**: ```kotlin val someString = myArgument ``` ### 3.16 Conditional Statements This section describes the general rules related to the conditional statements. #### 3.16.1 Collapsing redundant nested if-statements The nested if-statements, when possible, should be collapsed into a single one by concatenating their conditions with the infix operator &&. This improves the readability by reducing the number of the nested language constructs. #### Simple collapse **Invalid example**: ```kotlin if (cond1) { if (cond2) { doSomething() } } ``` **Valid example**: ```kotlin if (cond1 && cond2) { doSomething() } ``` #### Compound conditions **Invalid example**: ```kotlin if (cond1) { if (cond2 || cond3) { doSomething() } } ``` **Valid example**: ```kotlin if (cond1 && (cond2 || cond3)) { doSomething() } ``` #### 3.16.2 Too complex conditions Too complex conditions should be simplified according to boolean algebra rules, if it is possible. The following rules are considered when simplifying an expression: * boolean literals are removed (e.g. `foo() || false` -> `foo()`) * double negation is removed (e.g. `!(!a)` -> `a`) * expression with the same variable are simplified (e.g. `a && b && a` -> `a && b`) * remove expression from disjunction, if they are subset of other expression (e.g. `a || (a && b)` -> `a`) * remove expression from conjunction, if they are more broad than other expression (e.g. `a && (a || b)` -> `a`) * de Morgan's rule (negation is moved inside parentheses, i.e. `!(a || b)` -> `!a && !b`) **Valid example** ```kotlin if (condition1 && condition2) { foo() } ``` **Invalid example** ```kotlin if (condition1 && condition2 && condition1) { foo() } ``` ### 3.17 Ranges This section describes guidelines for working with ranges. #### 3.17.1 When creating a range with excluded upper boundary, instead of using range function with included upper boundary (`rangeTo` or `..`) it's preferred to use a range function with excluded upper boundary (`until`). Invalid example: ```kotlin 0..(a-1) ``` Valid example: ```kotlin 0 until a ``` Instead of `rangeTo` function it's preferred to use `..` operator. ### 3.18 Logging This section describes the general rules of logging. #### 3.18.1 Debug logging The dedicated logging library should be used for logging purposes. Need to try to avoid the following statements (assumption that it's a debug logging): ```kotlin print("I'M HERE") println("test") ``` Additionally, need to avoid the following statements on Kotlin JS: ```kotlin console.info("info test") console.log("test") ``` ================================================ FILE: info/guide/guide-chapter-4.md ================================================ # 4. Variables and types This section is dedicated to the rules and recommendations for using variables and types in your code. ### 4.1 Variables The rules of using variables are explained in the below topics. #### 4.1.1 Do not use Float and Double types when accurate calculations are needed Floating-point numbers provide a good approximation over a wide range of values, but they cannot produce accurate results in some cases. Binary floating-point numbers are unsuitable for precise calculations because it is impossible to represent 0.1 or any other negative power of 10 in a `binary representation` with a finite length. The following code example seems to be obvious: ```kotlin val myValue = 2.0 - 1.1 println(myValue) ``` However, it will print the following value: `0.8999999999999999` Therefore, for precise calculations (for example, in finance or exact sciences), using such types as `Int`, `Long`, `BigDecimal`are recommended. The `BigDecimal` type should serve as a good choice. **Invalid example**: Float values containing more than six or seven decimal numbers will be rounded. ```kotlin val eFloat = 2.7182818284f // Float, will be rounded to 2.7182817 ``` **Valid example**: (when precise calculations are needed): ```kotlin val income = BigDecimal("2.0") val expense = BigDecimal("1.1") println(income.subtract(expense)) // you will obtain 0.9 here ``` #### 4.1.2: Comparing numeric float type values Numeric float type values should not be directly compared with the equality operator (==) or other methods, such as `compareTo()` and `equals()`. Since floating-point numbers involve precision problems in computer representation, it is better to use `BigDecimal` as recommended in [Rule 4.1.1](#r4.1.1) to make accurate computations and comparisons. The following code describes these problems. **Invalid example**: ```kotlin val f1 = 1.0f - 0.9f val f2 = 0.9f - 0.8f if (f1 == f2) { println("Expected to enter here") } else { println("But this block will be reached") } val flt1 = f1; val flt2 = f2; if (flt1.equals(flt2)) { println("Expected to enter here") } else { println("But this block will be reached") } ``` **Valid example**: ```kotlin val foo = 1.03f val bar = 0.42f if (abs(foo - bar) > 1e-6f) { println("Ok") } else { println("Not") } ``` #### 4.1.3 Try to use 'val' instead of 'var' for variable declaration [SAY_NO_TO_VAR] Variables with the `val` modifier are immutable (read-only). Using `val` variables instead of `var` variables increases code robustness and readability. This is because `var` variables can be reassigned several times in the business logic. However, in some scenarios with loops or accumulators, only `var`s are permitted. ### 4.2 Types This section provides recommendations for using types. #### 4.2.1: Use Contracts and smart cast as much as possible The Kotlin compiler has introduced [Smart Casts](https://kotlinlang.org/docs/reference/typecasts.html#smart-casts) that help reduce the size of code. **Invalid example**: ```kotlin if (x is String) { print((x as String).length) // x was already automatically cast to String - no need to use 'as' keyword here } ``` **Valid example**: ```kotlin if (x is String) { print(x.length) // x was already automatically cast to String - no need to use 'as' keyword here } ``` Also, Kotlin 1.3 introduced [Contracts](https://kotlinlang.org/docs/reference/whatsnew13.html#contracts) that provide enhanced logic for smart-cast. Contracts are used and are very stable in `stdlib`, for example: ```kotlin fun bar(x: String?) { if (!x.isNullOrEmpty()) { println("length of '$x' is ${x.length}") // smartcasted to not-null } } ``` Smart cast and contracts are a better choice because they reduce boilerplate code and features forced type conversion. **Invalid example**: ```kotlin fun String?.isNotNull(): Boolean = this != null fun foo(s: String?) { if (s.isNotNull()) s!!.length // No smartcast here and !! operator is used } ``` **Valid example**: ```kotlin fun foo(s: String?) { if (s.isNotNull()) s.length // We have used a method with contract from stdlib that helped compiler to execute smart cast } ``` #### 4.2.2: Try to use type alias to represent types making code more readable Type aliases provide alternative names for existing types. If the type name is too long, you can replace it with a shorter name, which helps to shorten long generic types. For example, code looks much more readable if you introduce a `typealias` instead of a long chain of nested generic types. We recommend using a `typealias` if the type contains **more than two** nested generic types and is longer than **25 chars**. **Invalid example**: ```kotlin val b: MutableMap> ``` **Valid example**: ```kotlin typealias FileTable = MutableMap> val b: FileTable ``` You can also provide additional aliases for function (lambda-like) types: ```kotlin typealias MyHandler = (Int, String, Any) -> Unit typealias Predicate = (T) -> Boolean ``` ### 4.3 Null safety and variable declarations Kotlin is declared as a null-safe programming language. However, to achieve compatibility with Java, it still supports nullable types. #### 4.3.1: Avoid declaring variables with nullable types, especially from Kotlin stdlib To avoid `NullPointerException` and help the compiler prevent Null Pointer Exceptions, avoid using nullable types (with `?` symbol). **Invalid example**: ```kotlin val a: Int? = 0 ``` **Valid example**: ```kotlin val a: Int = 0 ``` Nevertheless, when using Java libraries extensively, you have to use nullable types and enrich the code with `!!` and `?` symbols. Avoid using nullable types for Kotlin stdlib (declared in [official documentation](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/)). Try to use initializers for empty collections. For example, if you want to initialize a list instead of `null`, use `emptyList()`. **Invalid example**: ```kotlin val a: List? = null ``` **Valid example**: ```kotlin val a: List = emptyList() ``` #### 4.3.2: Variables of generic types should have an explicit type declaration Like in Java, classes in Kotlin may have type parameters. To create an instance of such a class, we typically need to provide type arguments: ```kotlin val myVariable: Map = emptyMap() ``` However, the compiler can inherit type parameters from the r-value (value assigned to a variable). Therefore, it will not force users to declare the type explicitly. These declarations are not recommended because programmers would need to find the return value and understand the variable type by looking at the method. **Invalid example**: ```kotlin val myVariable = emptyMap() ``` **Valid example**: ```kotlin val myVariable: Map = emptyMap() ``` #### 4.3.3 Null-safety Try to avoid explicit null checks (explicit comparison with `null`) Kotlin is declared as [Null-safe](https://kotlinlang.org/docs/reference/null-safety.html) language. However, Kotlin architects wanted Kotlin to be fully compatible with Java; that's why the `null` keyword was also introduced in Kotlin. There are several code-structures that can be used in Kotlin to avoid null-checks. For example: `?:`, `.let {}`, `.also {}`, e.t.c **Invalid example:** ```kotlin // example 1 var myVar: Int? = null if (myVar == null) { println("null") return } // example 2 if (myVar != null) { println("not null") return } // example 3 val anotherVal = if (myVar != null) { println("not null") 1 } else { 2 } // example 4 if (myVar == null) { println("null") } else { println("not null") } ``` **Valid example:** ```kotlin // example 1 var myVar: Int? = null myVar?: run { println("null") return } // example 2 myVar?.let { println("not null") return } // example 3 val anotherVal = myVar?.also { println("not null") 1 } ?: 2 // example 4 myVar?.let { println("not null") } ?: run { println("null") } ``` **Exceptions:** In the case of complex expressions, such as multiple `else-if` structures or long conditional statements, there is common sense to use explicit comparison with `null`. **Valid examples:** ```kotlin if (myVar != null) { println("not null") } else if (anotherCondition) { println("Other condition") } ``` ```kotlin if (myVar == null || otherValue == 5 && isValid) {} ``` Please also note, that instead of using `require(a != null)` with a not null check - you should use a special Kotlin function called `requireNotNull(a)`. ================================================ FILE: info/guide/guide-chapter-5.md ================================================ # 5. Functions This section describes the rules of using functions in your code. ### 5.1 Function design Developers can write clean code by gaining knowledge of how to build design patterns and avoid code smells. You should utilize this approach, along with functional style, when writing Kotlin code. The concepts behind functional style are as follows: Functions are the smallest unit of combinable and reusable code. They should have clean logic, **high cohesion**, and **low coupling** to organize the code effectively. The code in functions should be simple and not conceal the author's original intentions. Additionally, it should have a clean abstraction, and control statements should be used straightforwardly. The side effects (code that does not affect a function's return value but affects global/object instance variables) should not be used for state changes of an object. The only exceptions to this are state machines. Kotlin is [designed](https://www.slideshare.net/abreslav/whos-more-functional-kotlin-groovy-scala-or-java) to support and encourage functional programming, featuring the corresponding built-in mechanisms. Also, it supports standard collections and sequences feature methods that enable functional programming (for example, `apply`, `with`, `let`, and `run`), Kotlin Higher-Order functions, function types, lambdas, and default function arguments. As [previously discussed](#r4.1.3), Kotlin supports and encourages the use of immutable types, which in turn motivates programmers to write pure functions that avoid side effects and have a corresponding output for specific input. The pipeline data flow for the pure function comprises a functional paradigm. It is easy to implement concurrent programming when you have chains of function calls, where each step features the following characteristics: 1. Simplicity 2. Verifiability 3. Testability 4. Replaceability 5. Pluggability 6. Extensibility 7. Immutable results There can be only one side effect in this data stream, which can be placed only at the end of the execution queue. #### 5.1.1 Avoid functions that are too long The function should be displayable on one screen and only implement one certain logic. If a function is too long, it often means complex and could be split or simplified. Functions should consist of 30 lines (non-empty and non-comment) in total. **Exception:** Some functions that implement complex algorithms may exceed 30 lines due to aggregation and comprehensiveness. Linter warnings for such functions **can be suppressed**. Even if a long function works well, new problems or bugs may appear due to the function's complex logic once it is modified by someone else. Therefore, it is recommended to split such functions into several separate and shorter functions that are easier to manage. This approach will enable other programmers to read and modify the code properly. #### 5.1.2 Avoid deep nesting of function code blocks, limiting to four levels The nesting depth of a function's code block is the depth of mutual inclusion between the code control blocks in the function (for example: if, for, while, and when). Each nesting level will increase the amount of effort needed to read the code because you need to remember the current "stack" (for example, entering conditional statements and loops). **Exception:** The nesting levels of the lambda expressions, local classes, and anonymous classes in functions are calculated based on the innermost function. The nesting levels of enclosing methods are not accumulated. Functional decomposition should be implemented to avoid confusion for the developer who reads the code. This will help the reader switch between contexts. #### 5.1.3 Avoid using nested functions Nested functions create a more complex function context, thereby confusing readers. With nested functions, the visibility context may not be evident to the code reader. **Invalid example**: ```kotlin fun foo() { fun nested():String { return "String from nested function" } println("Nested Output: ${nested()}") } ``` #### 5.1.4 Negated function calls Don't use negated function calls if it can be replaced with negated version of this function **Invalid example**: ```kotlin fun foo() { val list = listOf(1, 2, 3) if (!list.isEmpty()) { // Some cool logic } } ``` **Valid example**: ```kotlin fun foo() { val list = listOf(1, 2, 3) if (list.isNotEmpty()) { // Some cool logic } } ``` ### 5.2 Function arguments The rules for using function arguments are described in the below topics. #### 5.2.1 The lambda parameter of the function should be placed at the end of the argument list With such notation, it is easier to use curly brackets, leading to better code readability. **Valid example**: ```kotlin // declaration fun myFoo(someArg: Int, myLambda: () -> Unit) { // ... } // usage myFoo(1) { println("hey") } ``` #### 5.2.2 Number of function parameters should be limited to five A long argument list is a [code smell](https://en.wikipedia.org/wiki/Code_smell) that leads to less reliable code. It is recommended to reduce the number of parameters. Having **more than five** parameters leads to difficulties in maintenance and conflicts merging. If parameter groups appear in different functions multiple times, these parameters are closely related and can be encapsulated into a single Data Class. It is recommended that you use Data Classes and Maps to unify these function arguments. #### 5.2.3 Use default values for function arguments instead of overloading them In Java, default values for function arguments are prohibited. That is why the function should be overloaded when you need to create a function with fewer arguments. In Kotlin, you can use default arguments instead. This is useful if methods have same modifiers (private/inline/etc.). If you would like to have some different logic and code in these methods - then name them differently accordingly. **Invalid example**: ```kotlin private fun foo(arg: Int) { // ... } private fun foo() { // ... } ``` **Valid example**: ```kotlin private fun foo(arg: Int = 0) { // ... } ``` #### 5.2.4 Synchronizing code inside asynchronous code Try to avoid using `runBlocking` in asynchronous code **Invalid example**: ```kotlin GlobalScope.async { runBlocking { count++ } } ``` #### 5.2.5 Long lambdas should have explicit parameters The lambda without parameters shouldn't be too long. If a lambda is too long, it can confuse the user. Lambda without parameters should consist of 10 lines (non-empty and non-comment) in total. #### 5.2.6 Avoid using unnecessary, custom label Expressions with unnecessary, custom labels generally increase complexity and worsen the maintainability of the code. **Invalid example**: ```kotlin run lab@ { list.forEach { return@lab } } ``` **Valid example**: ```kotlin list.forEachIndexed { index, i -> return@forEachIndexed } lab@ for(i: Int in q) { for (j: Int in q) { println(i) break@lab } } ``` #### 5.2.7 Outer lambdas should have explicit parameters. The lambda without parameters shouldn't have inner lambdas. If a lambda has an inner lambda, `it` can confuse the user. Lambda without parameters should be latest. **Invalid example**: ```kotlin arrays.map { it.map { element -> element.foo() } } ``` **Valid example**: ```kotlin arrays.map { array -> array.map { it.foo() } } ``` ================================================ FILE: info/guide/guide-chapter-6.md ================================================ # 6. Classes, interfaces, and extension functions ### 6.1 Classes This section describes the rules of denoting classes in your code. #### 6.1.1 Denoting a class with a single constructor When a class has a single constructor, it should be defined as a primary constructor in the declaration of the class. If the class contains only one explicit constructor, it should be converted to a primary constructor. **Invalid example**: ```kotlin class Test { var a: Int constructor(a: Int) { this.a = a } } ``` **Valid example**: ```kotlin class Test(var a: Int) { // ... } // in case of any annotations or modifiers used on a constructor: class Test private constructor(var a: Int) { // ... } ``` #### 6.1.2 Prefer data classes instead of classes without any functional logic Some people say that the data class is a code smell. However, if you need to use it (which makes your code more simple), you can utilize the Kotlin `data class`. The main purpose of this class is to hold data, but also `data class` will automatically generate several useful methods: - equals()/hashCode() pair; - toString() - componentN() functions corresponding to the properties in their order of declaration; - copy() function Therefore, instead of using `normal` classes: ```kotlin class Test { var a: Int = 0 get() = field set(value: Int) { field = value} } class Test { var a: Int = 0 var b: Int = 0 constructor(a:Int, b: Int) { this.a = a this.b = b } } // or class Test(var a: Int = 0, var b: Int = 0) // or class Test() { var a: Int = 0 var b: Int = 0 } ``` **prefer data classes:** ```kotlin data class Test1(var a: Int = 0, var b: Int = 0) ``` **Exception 1**: Note that data classes cannot be abstract, open, sealed, or inner; that is why these types of classes cannot be changed to a data class. **Exception 2**: No need to convert a class to a data class if this class extends some other class or implements an interface. #### 6.1.3 Do not use the primary constructor if it is empty or useless The primary constructor is a part of the class header; it is placed after the class name and type parameters (optional) but can be omitted if it is not used. **Invalid example**: ```kotlin // simple case that does not need a primary constructor class Test() { var a: Int = 0 var b: Int = 0 } // empty primary constructor is not needed here // it can be replaced with a primary contructor with one argument or removed class Test() { var a = "Property" init { println("some init") } constructor(a: String): this() { this.a = a } } ``` **Valid example**: ```kotlin // the good example here is a data class; this example also shows that you should get rid of braces for the primary constructor class Test { var a: Int = 0 var b: Int = 0 } ``` #### 6.1.4 Do not use redundant init blocks in your class Several init blocks are redundant and generally should not be used in your class. The primary constructor cannot contain any code. That is why Kotlin has introduced `init` blocks. These blocks store the code to be run during the class initialization. Kotlin allows writing multiple initialization blocks executed in the same order as they appear in the class body. Even when you follow (rule 3.2)[#r3.2], this makes your code less readable as the programmer needs to keep in mind all init blocks and trace the execution of the code. Therefore, you should try to use a single `init` block to reduce the code's complexity. If you need to do some logging or make some calculations before the class property assignment, you can use powerful functional programming. This will reduce the possibility of the error if your `init` blocks' order is accidentally changed and make the code logic more coupled. It is always enough to use one `init` block to implement your idea in Kotlin. **Invalid example**: ```kotlin class YourClass(var name: String) { init { println("First initializer block that prints ${name}") } val property = "Property: ${name.length}".also(::println) init { println("Second initializer block that prints ${name.length}") } } ``` **Valid example**: ```kotlin class YourClass(var name: String) { init { println("First initializer block that prints ${name}") } val property = "Property: ${name.length}".also { prop -> println(prop) println("Second initializer block that prints ${name.length}") } } ``` The `init` block was not added to Kotlin to help you initialize your properties; it is needed for more complex tasks. Therefore if the `init` block contains only assignments of variables - move it directly to properties to be correctly initialized near the declaration. In some cases, this rule can be in clash with [6.1.1](#r6.1.1), but that should not stop you. **Invalid example**: ```kotlin class A(baseUrl: String) { private val customUrl: String init { customUrl = "$baseUrl/myUrl" } } ``` **Valid example**: ```kotlin class A(baseUrl: String) { private val customUrl = "$baseUrl/myUrl" } ``` #### 6.1.5 Explicit supertype qualification The explicit supertype qualification should not be used if there is no clash between called methods. This rule is applicable to both interfaces and classes. **Invalid example**: ```kotlin open class Rectangle { open fun draw() { /* ... */ } } class Square() : Rectangle() { override fun draw() { super.draw() // no need in super here } } ``` #### 6.1.6 Abstract class should have at least one abstract method Abstract classes are used to force a developer to implement some of its parts in their inheritors. When the abstract class has no abstract methods, it was set `abstract` incorrectly and can be converted to open class. **Invalid example**: ```kotlin abstract class NotAbstract { fun foo() {} fun test() {} } ``` **Valid example**: ```kotlin abstract class NotAbstract { abstract fun foo() fun test() {} } // OR open class NotAbstract { fun foo() {} fun test() {} } // OR class NotAbstract { fun foo() {} fun test() {} } ``` #### 6.1.7 When using the "implicit backing property" scheme, the name of real and back property should be the same Kotlin has a mechanism of [backing properties](https://kotlinlang.org/docs/reference/properties.html#backing-properties). In some cases, implicit backing is not enough and it should be done explicitly: ```kotlin private var _table: Map? = null val table: Map get() { if (_table == null) { _table = HashMap() // Type parameters are inferred } return _table ?: throw AssertionError("Set to null by another thread") } ``` In this case, the name of the backing property (`_table`) should be the same as the name of the real property (`table`) but should have an underscore (`_`) prefix. It is one of the exceptions from the [identifier names rule](#r1.2) #### 6.1.8 Avoid using custom getters and setters Kotlin has a perfect mechanism of [properties](https://kotlinlang.org/docs/reference/properties.html#properties-and-fields). Kotlin compiler automatically generates `get` and `set` methods for properties and can override them. **Invalid example:** ```kotlin class A { var size: Int = 0 set(value) { println("Side effect") field = value } // user of this class does not expect calling A.size receive size * 2 get() = field * 2 } ``` From the callee code, these methods look like access to this property: `A().isEmpty = true` for setter and `A().isEmpty` for getter. However, when `get` and `set` are overridden, it isn't very clear for a developer who uses this particular class. The developer expects to get the property value but receives some unknown value and some extra side-effect hidden by the custom getter/setter. Use extra functions instead to avoid confusion. **Valid example**: ```kotlin class A { var size: Int = 0 fun initSize(value: Int) { // some custom logic } // this will not confuse developer and he will get exactly what he expects fun goodNameThatDescribesThisGetter() = this.size * 2 } ``` **Exception:** `Private setters` are only exceptions that are not prohibited by this rule. #### 6.1.9 Never use the name of a variable in the custom getter or setter (possible_bug) If you ignored [recommendation 6.1.8](#r6.1.8), be careful with using the name of the property in your custom getter/setter as it can accidentally cause a recursive call and a `StackOverflow Error`. Use the `field` keyword instead. **Invalid example (very bad)**: ```kotlin var isEmpty: Boolean set(value) { println("Side effect") isEmpty = value } get() = isEmpty ``` #### 6.1.10 No trivial getters and setters are allowed in the code In Java, trivial getters - are the getters that are just returning the field value. Trivial setters - are merely setting the field with a value without any transformation. However, in Kotlin, trivial getters/setters are generated by default. There is no need to use it explicitly for all types of data structures in Kotlin. **Invalid example**: ```kotlin class A { var a: Int = 0 get() = field set(value: Int) { field = value } // } ``` **Valid example**: ```kotlin class A { var a: Int = 0 get() = field set(value: Int) { field = value } // } ``` #### 6.1.11 Use 'apply' for grouping object initialization In Java, before functional programming became popular, many classes from common libraries used the configuration paradigm. To use these classes, you had to create an object with the constructor with 0-2 arguments and set the fields needed to run the object. In Kotlin, to reduce the number of dummy code line and to group objects [`apply` extension](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/apply.html) was added: **Invalid example**: ```kotlin class HttpClient(var name: String) { var url: String = "" var port: String = "" var timeout = 0 fun doRequest() {} } fun main() { val httpClient = HttpClient("myConnection") httpClient.url = "http://example.com" httpClient.port = "8080" httpClient.timeout = 100 httpCLient.doRequest() } ``` **Valid example**: ```kotlin class HttpClient(var name: String) { var url: String = "" var port: String = "" var timeout = 0 fun doRequest() {} } fun main() { val httpClient = HttpClient("myConnection") .apply { url = "http://example.com" port = "8080" timeout = 100 } httpClient.doRequest() } ``` ### 6.1.12 Prefer Inline classes when a class has a single property If a class has only one immutable property, then it can be converted to the inline class. Sometimes it is necessary for business logic to create a wrapper around some type. However, it introduces runtime overhead due to additional heap allocations. Moreover, if the wrapped type is primitive, the performance hit is terrible, because primitive types are usually heavily optimized by the runtime, while their wrappers don't get any special treatment. **Invalid example**: ```kotlin class Password { val value: String } ``` **Valid example**: ```kotlin inline class Password(val value: String) ``` ### 6.2 Extension functions This section describes the rules of using extension functions in your code. [Extension functions](https://kotlinlang.org/docs/reference/extensions.html) is a killer-feature in Kotlin. It gives you a chance to extend classes that were already implemented in external libraries and helps you to make classes less heavy. Extension functions are resolved statically. #### 6.2.1 Use extension functions for making logic of classes less coupled It is recommended that for classes, the non-tightly coupled functions, which are rarely used in the class, should be implemented as extension functions where possible. They should be implemented in the same class/file where they are used. This is a non-deterministic rule, so the code cannot be checked or fixed automatically by a static analyzer. #### 6.2.2 No extension functions with the same name and signature if they extend base and inheritor classes (possible_bug) You should avoid declaring extension functions with the same name and signature if their receivers are base and inheritor classes (possible_bug), as extension functions are resolved statically. There could be a situation when a developer implements two extension functions: one is for the base class and another for the inheritor. This can lead to an issue when an incorrect method is used. **Invalid example**: ```kotlin open class A class B: A() // two extension functions with the same signature fun A.foo() = "A" fun B.foo() = "B" fun printClassName(s: A) { println(s.foo()) } // this call will run foo() method from the base class A, but // programmer can expect to run foo() from the class inheritor B fun main() { printClassName(B()) } ``` #### 6.2.3 Don't use extension functions for the class in the same file You should not use extension functions for the class in the same file, where it is defined. **Invalid example**: ```kotlin class SomeClass { } fun SomeClass.deleteAllSpaces() { } ``` #### 6.2.4 You should not use property length with operation - 1, you can change this to lastIndex You should not use property length with operation - 1, you can change this to lastIndex **Invalid example**: ```kotlin val A = "name" val B = A.length - 1 val C = A[A.length - 1] ``` **Valid example**: ```kotlin val A = "name" val B = A.lastIndex val C = A[A.lastIndex] ``` ### 6.3 Interfaces An `Interface` in Kotlin can contain declarations of abstract methods, as well as method implementations. What makes them different from abstract classes is that interfaces cannot store state. They can have properties, but these need to be abstract or to provide accessor implementations. Kotlin's interfaces can define attributes and functions. In Kotlin and Java, the interface is the main presentation means of application programming interface (API) design and should take precedence over the use of (abstract) classes. ### 6.4 Objects This section describes the rules of using objects in code. #### 6.4.1 Instead of using utility classes/objects, use extensions Avoid using utility classes/objects; use extensions instead. As described in [6.2 Extension functions](#c6.2), using extension functions is a powerful method. This enables you to avoid unnecessary complexity and class/object wrapping and use top-level functions instead. **Invalid example**: ```kotlin object StringUtil { fun stringInfo(myString: String): Int { return myString.count{ "something".contains(it) } } } StringUtil.stringInfo("myStr") ``` **Valid example**: ```kotlin fun String.stringInfo(): Int { return this.count{ "something".contains(it) } } "myStr".stringInfo() ``` #### 6.4.2 Objects should be used for Stateless Interfaces Kotlin’s objects are extremely useful when you need to implement some interface from an external library that does not have any state. There is no need to use classes for such structures. **Valid example**: ``` interface I { fun foo() } object O: I { override fun foo() {} } ``` ### 6.5 Kts Files This section describes general rules for `.kts` files #### 6.5.1 kts files should wrap logic into top-level scope It is still recommended wrapping logic inside functions and avoid using top-level statements for function calls or wrapping blocks of code in top-level scope functions like `run`. **Valid example**: ``` run { // some code } fun foo() { } ``` ================================================ FILE: info/guide/table-of-content.md ================================================ # Diktat Coding Convention ### Content | Chapter | Content | | ------------------- | ------------------------------------------------------------ | | [0. Preface](#c0.1) | [Purpose](#c0.1), [General principles](#c0.2), [Terminology](#c0.3), [Exceptions](#c0.4) | | [1. Naming](#c1) | [Identifiers](#c1.1), [Package names](#c1.2), [Classes, enumeration and interfaces](#c1.3), [Functions](#c1.4), [Constants](#c1.5), [Variables](#c1.6) | | [2. Docs](#c2) | [Kdoc](#c2.1), [File header](#c2.2), [Comments on function header ](#c2.3), [Code comments](#c2.4) | | [3. General formatting](#c3) | [File-related rules](#c3.1), [Braces](#c3.2), [Indentation](#c3.3), [Empty blocks](#c3.4), [Line length](#c3.5), [Line breaks (newlines)](#c3.6), [Blank lines](#c3.7), [Horizontal space](#c3.8), [Enumerations](#c3.9), [Variable declaration](#c3.10), [When expression](#c3.11), [Annotations](#c3.12), [Layout of comments](#c3.13), [Modifiers and constant values](#c3.14), [Strings](#c3.15)| | [4. Variables and types](#c4) | [Variables](#c4.1), [Types](#c4.2), [Null safety and variable declarations](#4.3)| | [5. Functions](#c5) | [Function design](#c5.1), [Function arguments](#c5.2)| | [6. Classes](#c6) | [Classes](#c6.1), [Extension functions](#c6.2), [Interfaces](#c6.3), [Objects](#c6.4) | | [7. Kotlin & Java](#c7) | | ================================================ FILE: info/rules-mapping.md ================================================ | Diktat Rule | Code Style | Auto-fixed? | Chapter | | ----------------------------------------- | ------ | --- | --------- | | VARIABLE_NAME_INCORRECT | [1.1.1](guide/diktat-coding-convention.md#r1.1.1) | no | Naming | | VARIABLE_HAS_PREFIX | [1.1.1](guide/diktat-coding-convention.md#r1.1.1) | yes | Naming | | IDENTIFIER_LENGTH | [1.1.1](guide/diktat-coding-convention.md#r1.1.1) | no | Naming | | GENERIC_NAME | [1.1.1](guide/diktat-coding-convention.md#r1.1.1) | yes | Naming | | BACKTICKS_PROHIBITED | [1.1.1](guide/diktat-coding-convention.md#r1.1.1) | no | Naming | | FILE_NAME_INCORRECT | [1.1.1](guide/diktat-coding-convention.md#r1.1.1) | yes | Naming | | EXCEPTION_SUFFIX | [1.1.1](guide/diktat-coding-convention.md#r1.1.1) | yes | Naming | | CONFUSING_IDENTIFIER_NAMING | [1.1.1](guide/diktat-coding-convention.md#r1.1.1) | no | Naming | | PACKAGE_NAME_MISSING | [1.2.1](guide/diktat-coding-convention.md#r1.2.1) | yes | Naming | | PACKAGE_NAME_INCORRECT_CASE | [1.2.1](guide/diktat-coding-convention.md#r1.2.1) | yes | Naming | | PACKAGE_NAME_INCORRECT_PREFIX | [1.2.1](guide/diktat-coding-convention.md#r1.2.1) | yes | Naming | | PACKAGE_NAME_INCORRECT_SYMBOLS | [1.2.1](guide/diktat-coding-convention.md#r1.2.1) | no | Naming | | PACKAGE_NAME_INCORRECT_PATH | [1.2.1](guide/diktat-coding-convention.md#r1.2.1) | yes | Naming | | INCORRECT_PACKAGE_SEPARATOR | [1.2.1](guide/diktat-coding-convention.md#r1.2.1) | yes | Naming | | CLASS_NAME_INCORRECT | [1.3.1](guide/diktat-coding-convention.md#r1.3.1) | yes | Naming | | OBJECT_NAME_INCORRECT | [1.3.1](guide/diktat-coding-convention.md#r1.3.1) | yes | Naming | | ENUM_VALUE | [1.3.1](guide/diktat-coding-convention.md#r1.3.1) | yes | Naming | | TYPEALIAS_NAME_INCORRECT_CASE | [1.3.1](guide/diktat-coding-convention.md#r1.3.1) | yes | Naming | | FUNCTION_NAME_INCORRECT_CASE | [1.4.1](guide/diktat-coding-convention.md#r1.4.1) | yes | Naming | | CONSTANT_UPPERCASE | [1.5.1](guide/diktat-coding-convention.md#r1.5.1) | yes | Naming | | VARIABLE_NAME_INCORRECT_FORMAT | [1.6.1](guide/diktat-coding-convention.md#r1.6.1) | yes | Naming | | FUNCTION_BOOLEAN_PREFIX | [1.6.2](guide/diktat-coding-convention.md#r1.6.2) | yes | Naming | | MISSING_KDOC_TOP_LEVEL | [2.1.1](guide/diktat-coding-convention.md#r2.1.1) | no | Comments | | MISSING_KDOC_CLASS_ELEMENTS | [2.1.1](guide/diktat-coding-convention.md#r2.1.1) | no | Comments | | MISSING_KDOC_ON_FUNCTION | [2.1.1](guide/diktat-coding-convention.md#r2.1.1) | yes | Comments | | KDOC_NO_CONSTRUCTOR_PROPERTY | [2.1.1](guide/diktat-coding-convention.md#r2.1.1) | yes | Comments | | KDOC_NO_CLASS_BODY_PROPERTIES_IN_HEADER | [2.1.1](guide/diktat-coding-convention.md#r2.1.1) | yes | Comments | | KDOC_EXTRA_PROPERTY | [2.1.1](guide/diktat-coding-convention.md#r2.1.1) | no | Comments | | KDOC_DUPLICATE_PROPERTY | [2.1.1](guide/diktat-coding-convention.md#r2.1.1) | no | Comments | | KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT | [2.1.1](guide/diktat-coding-convention.md#r2.1.1) | yes | Comments | | KDOC_WITHOUT_PARAM_TAG | [2.1.2](guide/diktat-coding-convention.md#r2.1.2) | yes | Comments | | KDOC_WITHOUT_RETURN_TAG | [2.1.2](guide/diktat-coding-convention.md#r2.1.2) | yes | Comments | | KDOC_WITHOUT_THROWS_TAG | [2.1.2](guide/diktat-coding-convention.md#r2.1.2) | yes | Comments | | KDOC_EMPTY_KDOC | [2.1.3](guide/diktat-coding-convention.md#r2.1.3) | no | Comments | | KDOC_WRONG_SPACES_AFTER_TAG | [2.1.3](guide/diktat-coding-convention.md#r2.1.3) | yes | Comments | | KDOC_WRONG_TAGS_ORDER | [2.1.3](guide/diktat-coding-convention.md#r2.1.3) | yes | Comments | | KDOC_NEWLINES_BEFORE_BASIC_TAGS | [2.1.3](guide/diktat-coding-convention.md#r2.1.3) | yes | Comments | | KDOC_NO_NEWLINES_BETWEEN_BASIC_TAGS | [2.1.3](guide/diktat-coding-convention.md#r2.1.3) | yes | Comments | | KDOC_NO_NEWLINE_AFTER_SPECIAL_TAGS | [2.1.3](guide/diktat-coding-convention.md#r2.1.3) | yes | Comments | | KDOC_NO_DEPRECATED_TAG | [2.1.3](guide/diktat-coding-convention.md#r2.1.3) | yes | Comments | | KDOC_CONTAINS_DATE_OR_AUTHOR | [2.1.3](guide/diktat-coding-convention.md#r2.1.3) | no | Comments | | KDOC_NO_EMPTY_TAGS | [2.2.1](guide/diktat-coding-convention.md#r2.2.1) | no | Comments | | HEADER_WRONG_FORMAT | [2.2.1](guide/diktat-coding-convention.md#r2.2.1) | yes | Comments | | HEADER_MISSING_OR_WRONG_COPYRIGHT | [2.2.1](guide/diktat-coding-convention.md#r2.2.1) | yes | Comments | | WRONG_COPYRIGHT_YEAR | [2.2.1](guide/diktat-coding-convention.md#r2.2.1) | yes | Comments | | HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE | [2.2.1](guide/diktat-coding-convention.md#r2.2.1) | no | Comments | | HEADER_NOT_BEFORE_PACKAGE | [2.2.1](guide/diktat-coding-convention.md#r2.2.1) | yes | Comments | | KDOC_TRIVIAL_KDOC_ON_FUNCTION | [2.3.1](guide/diktat-coding-convention.md#r2.3.1) | no | Comments | | WRONG_NEWLINES_AROUND_KDOC | [2.4.1](guide/diktat-coding-convention.md#r2.4.1) | yes | Comments | | FIRST_COMMENT_NO_BLANK_LINE | [2.4.1](guide/diktat-coding-convention.md#r2.4.1) | yes | Comments | | COMMENT_WHITE_SPACE | [2.4.1](guide/diktat-coding-convention.md#r2.4.1) | yes | Comments | | IF_ELSE_COMMENTS | [2.4.1](guide/diktat-coding-convention.md#r2.4.1) | yes | Comments | | COMMENTED_OUT_CODE | [2.4.2](guide/diktat-coding-convention.md#r2.4.2) | no | Comments | | FILE_IS_TOO_LONG | [3.1.1](guide/diktat-coding-convention.md#r3.1.1) | no | General | | FILE_CONTAINS_ONLY_COMMENTS | [3.1.2](guide/diktat-coding-convention.md#r3.1.2) | no | General | | FILE_INCORRECT_BLOCKS_ORDER | [3.1.2](guide/diktat-coding-convention.md#r3.1.2) | yes | General | | FILE_NO_BLANK_LINE_BETWEEN_BLOCKS | [3.1.2](guide/diktat-coding-convention.md#r3.1.2) | yes | General | | FILE_UNORDERED_IMPORTS | [3.1.2](guide/diktat-coding-convention.md#r3.1.2) | yes | General | | FILE_WILDCARD_IMPORTS | [3.1.2](guide/diktat-coding-convention.md#r3.1.2) | no | General | | UNUSED_IMPORT | [3.1.2](guide/diktat-coding-convention.md#r3.1.2) | yes | General | | FILE_NAME_MATCH_CLASS | [3.1.2](guide/diktat-coding-convention.md#r3.1.2) | yes | General | | WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES | [3.1.4](guide/diktat-coding-convention.md#r3.1.4) | yes | General | | BLANK_LINE_BETWEEN_PROPERTIES | [3.1.4](guide/diktat-coding-convention.md#r3.1.4) | yes | General | | WRONG_DECLARATIONS_ORDER | [3.1.4](guide/diktat-coding-convention.md#r3.1.4) | yes | General | | TOP_LEVEL_ORDER | [3.1.5](guide/diktat-coding-convention.md#r3.1.5) | yes | General | | NO_BRACES_IN_CONDITIONALS_AND_LOOPS | [3.2.1](guide/diktat-coding-convention.md#r3.2.1) | yes | General | | BRACES_BLOCK_STRUCTURE_ERROR | [3.2.2](guide/diktat-coding-convention.md#r3.2.2) | yes | General | | WRONG_INDENTATION | [3.3.1](guide/diktat-coding-convention.md#r3.3.1) | yes | General | | EMPTY_BLOCK_STRUCTURE_ERROR | [3.4.1](guide/diktat-coding-convention.md#r3.4.1) | yes | General | | LONG_LINE | [3.5.1](guide/diktat-coding-convention.md#r3.5.1) | yes | General | | MORE_THAN_ONE_STATEMENT_PER_LINE | [3.6.1](guide/diktat-coding-convention.md#r3.6.1) | yes | General | | REDUNDANT_SEMICOLON | [3.6.2](guide/diktat-coding-convention.md#r3.6.2) | yes | General | | WRONG_NEWLINES | [3.6.2](guide/diktat-coding-convention.md#r3.6.2) | yes | General | | TRAILING_COMMA | [3.6.2](guide/diktat-coding-convention.md#r3.6.2) | yes | General | | COMPLEX_EXPRESSION | [3.6.3](guide/diktat-coding-convention.md#r3.6.3) | no | General | | COMPLEX_BOOLEAN_EXPRESSION | [3.6.4](guide/diktat-coding-convention.md#r3.6.4) | yes | General | | TOO_MANY_BLANK_LINES | [3.7.1](guide/diktat-coding-convention.md#r3.7.1) | yes | General | | WRONG_WHITESPACE | [3.8.1](guide/diktat-coding-convention.md#r3.8.1) | yes | General | | TOO_MANY_CONSECUTIVE_SPACES | [3.8.1](guide/diktat-coding-convention.md#r3.8.1) | yes | General | | ENUMS_SEPARATED | [3.9.1](guide/diktat-coding-convention.md#r3.9.1) | yes | General | | LOCAL_VARIABLE_EARLY_DECLARATION | [3.10.2](guide/diktat-coding-convention.md#r3.10.2) | no | General | | WHEN_WITHOUT_ELSE | [3.11.1](guide/diktat-coding-convention.md#r3.11.1) | yes | General | | ANNOTATION_NEW_LINE | [3.12.1](guide/diktat-coding-convention.md#r3.12.1) | yes | General | | WRONG_MULTIPLE_MODIFIERS_ORDER | [3.14.1](guide/diktat-coding-convention.md#r3.14.1) | yes | General | | LONG_NUMERICAL_VALUES_SEPARATED | [3.14.2](guide/diktat-coding-convention.md#r3.14.2) | yes | General | | MAGIC_NUMBER | [3.14.3](guide/diktat-coding-convention.md#r3.14.3) | no | General | | STRING_CONCATENATION | [3.15.1](guide/diktat-coding-convention.md#r3.15.1) | yes | General | | STRING_TEMPLATE_CURLY_BRACES | [3.15.2](guide/diktat-coding-convention.md#r3.15.2) | yes | General | | STRING_TEMPLATE_QUOTES | [3.15.2](guide/diktat-coding-convention.md#r3.15.2) | yes | General | | COLLAPSE_IF_STATEMENTS | [3.16.1](guide/diktat-coding-convention.md#r3.16.1) | yes | General | | CONVENTIONAL_RANGE | [3.17.1](guide/diktat-coding-convention.md#r3.17.1) | yes | General | | DEBUG_PRINT | [3.18.1](guide/diktat-coding-convention.md#r3.18.1) | no | General | | FLOAT_IN_ACCURATE_CALCULATIONS | [4.1.1](guide/diktat-coding-convention.md#r4.1.1) | no | Variables | | SAY_NO_TO_VAR | [4.1.3](guide/diktat-coding-convention.md#r4.1.3) | no | Variables | | SMART_CAST_NEEDED | [4.2.1](guide/diktat-coding-convention.md#r4.2.1) | yes | Variables | | TYPE_ALIAS | [4.2.2](guide/diktat-coding-convention.md#r4.2.2) | no | Variables | | NULLABLE_PROPERTY_TYPE | [4.3.1](guide/diktat-coding-convention.md#r4.3.1) | yes | Variables | | GENERIC_VARIABLE_WRONG_DECLARATION | [4.3.2](guide/diktat-coding-convention.md#r4.3.2) | yes | Variables | | AVOID_NULL_CHECKS | [4.3.3](guide/diktat-coding-convention.md#r4.3.3) | yes | Variables | | TOO_LONG_FUNCTION | [5.1.1](guide/diktat-coding-convention.md#r5.1.1) | no | Functions | | NESTED_BLOCK | [5.1.2](guide/diktat-coding-convention.md#r5.1.2) | no | Functions | | AVOID_NESTED_FUNCTIONS | [5.1.3](guide/diktat-coding-convention.md#r5.1.3) | yes | Functions | | INVERSE_FUNCTION_PREFERRED | [5.1.4](guide/diktat-coding-convention.md#r5.1.4) | yes | Functions | | LAMBDA_IS_NOT_LAST_PARAMETER | [5.2.1](guide/diktat-coding-convention.md#r5.2.1) | no | Functions | | TOO_MANY_PARAMETERS | [5.2.2](guide/diktat-coding-convention.md#r5.2.2) | no | Functions | | WRONG_OVERLOADING_FUNCTION_ARGUMENTS | [5.2.3](guide/diktat-coding-convention.md#r5.2.3) | no | Functions | | RUN_BLOCKING_INSIDE_ASYNC | [5.2.4](guide/diktat-coding-convention.md#r5.2.4) | no | Functions | | TOO_MANY_LINES_IN_LAMBDA | [5.2.5](guide/diktat-coding-convention.md#r5.2.5) | no | Functions | | CUSTOM_LABEL | [5.2.6](guide/diktat-coding-convention.md#r5.2.6) | no | Functions | | PARAMETER_NAME_IN_OUTER_LAMBDA | [5.2.7](guide/diktat-coding-convention.md#r5.2.7) | no | Functions | | SINGLE_CONSTRUCTOR_SHOULD_BE_PRIMARY | [6.1.1](guide/diktat-coding-convention.md#r6.1.1) | yes | Classes | | USE_DATA_CLASS | [6.1.2](guide/diktat-coding-convention.md#r6.1.2) | no | Classes | | EMPTY_PRIMARY_CONSTRUCTOR | [6.1.3](guide/diktat-coding-convention.md#r6.1.3) | yes | Classes | | MULTIPLE_INIT_BLOCKS | [6.1.4](guide/diktat-coding-convention.md#r6.1.4) | yes | Classes | | USELESS_SUPERTYPE | [6.1.5](guide/diktat-coding-convention.md#r6.1.5) | yes | Classes | | CLASS_SHOULD_NOT_BE_ABSTRACT | [6.1.6](guide/diktat-coding-convention.md#r6.1.6) | yes | Classes | | NO_CORRESPONDING_PROPERTY | [6.1.7](guide/diktat-coding-convention.md#r6.1.7) | no | Classes | | CUSTOM_GETTERS_SETTERS | [6.1.8](guide/diktat-coding-convention.md#r6.1.8) | no | Classes | | WRONG_NAME_OF_VARIABLE_INSIDE_ACCESSOR | [6.1.9](guide/diktat-coding-convention.md#r6.1.9) | no | Classes | | TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED | [6.1.10](guide/diktat-coding-convention.md#r6.1.10) | yes | Classes | | COMPACT_OBJECT_INITIALIZATION | [6.1.11](guide/diktat-coding-convention.md#r6.1.11) | yes | Classes | | INLINE_CLASS_CAN_BE_USED | [6.1.12](guide/diktat-coding-convention.md#r6.1.12) | yes | Classes | | EXTENSION_FUNCTION_SAME_SIGNATURE | [6.2.2](guide/diktat-coding-convention.md#r6.2.2) | no | Classes | | EXTENSION_FUNCTION_WITH_CLASS | [6.2.3](guide/diktat-coding-convention.md#r6.2.3) | no | Classes | | USE_LAST_INDEX | [6.2.4](guide/diktat-coding-convention.md#r6.2.4) | yes | Classes | | AVOID_USING_UTILITY_CLASS | [6.4.1](guide/diktat-coding-convention.md#r6.4.1) | no | Classes | | OBJECT_IS_PREFERRED | [6.4.2](guide/diktat-coding-convention.md#r6.4.2) | yes | Classes | | RUN_IN_SCRIPT | [6.5.1](guide/diktat-coding-convention.md#r6.5.1) | yes | Classes | ================================================ FILE: renovate.json ================================================ { "enabled": true, "dependencyDashboard": true, "schedule": [ "before 4am on Monday" ], "packageRules": [ { "managers": ["github-actions"], "groupName": "all github actions", "groupSlug": "all-github-actions" }, { "managers": ["gradle"], "matchPackagePatterns": [ "*" ], "excludePackagePatterns": [ "^org\\.jetbrains\\.kotlin[.:]", "^com\\.pinterest\\.ktlint[.:]", "^com\\.google\\.devtools\\.ksp[.:]" ], "matchUpdateTypes": [ "minor", "patch" ], "groupName": "all non-major dependencies (except core Kotlin)", "groupSlug": "all-minor-patch" }, { "managers": ["gradle"], "matchPackagePatterns": [ "^org\\.jetbrains\\.kotlin[.:]", "^com\\.google\\.devtools\\.ksp[.:]" ], "groupName": "Kotlin core dependencies", "groupSlug": "core-kotlin" }, { "managers": ["gradle"], "matchPackagePatterns": [ "^com\\.pinterest\\.ktlint[.:]" ], "groupName": "Ktlint", "groupSlug": "ktlint" }, { "managers": ["gradle"], "matchPackageNames": [ "org.sonatype.plugins:nexus-staging-maven-plugin" ], "allowedVersions": "<= 1.6.8 || > 1.6.13" }, { "managers": ["gradle"], "matchPackageNames": [ "io.github.microutils:kotlin-logging-jvm" ], "allowedVersions": "<= 2.1.23" }, { "managers": ["gradle"], "matchPackageNames": [ "org.apache.maven.plugins:maven-plugin-plugin" ], "allowedVersions": "< 3.7.0" }, { "managers": ["gradle"], "matchPackageNames": [ "org.apache.maven.plugin-tools:maven-plugin-annotations" ], "allowedVersions": "< 3.7.0" }, { "managers": ["gradle"], "matchPackageNames": [ "commons-cli:commons-cli" ], "allowedVersions": "!/^20040117\\.000000$/" } ] } ================================================ FILE: settings.gradle.kts ================================================ rootProject.name = "diktat" dependencyResolutionManagement { repositories { file("$rootDir/build/diktat-snapshot") .takeIf { it.exists() } ?.run { maven { url = this@run.toURI() } } maven { url = uri("https://s01.oss.sonatype.org/content/repositories/snapshots/") content { includeGroup("com.saveourtool.diktat") } } mavenCentral() } } pluginManagement { repositories { file("$rootDir/build/diktat-snapshot") .takeIf { it.exists() } ?.run { maven { url = this@run.toURI() } } maven { url = uri("https://s01.oss.sonatype.org/content/repositories/snapshots/") content { includeGroup("com.saveourtool.diktat") } } mavenCentral() gradlePluginPortal() } } plugins { id("com.gradle.enterprise") version "3.16.2" // starting from Gradle 8, it's needed to configure a repo from which to take Java for a toolchain id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" } includeBuild("gradle/plugins") include("diktat-api") include("diktat-common-test") include("diktat-ktlint-engine") include("diktat-gradle-plugin") include("diktat-maven-plugin") include("diktat-rules") include("diktat-ruleset") include("diktat-dev-ksp") include("diktat-cli") include("diktat-runner") enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") gradleEnterprise { @Suppress("AVOID_NULL_CHECKS") if (System.getenv("CI") != null) { buildScan { publishAlways() termsOfServiceUrl = "https://gradle.com/terms-of-service" termsOfServiceAgree = "yes" } } } ================================================ FILE: wp/README.md ================================================ ## White paper To generate a pdf document, you need to go to the `wp` folder and execute ``` make ``` **Note** Minimum `MiKTeX` version - `20.11` ================================================ FILE: wp/makefile ================================================ all: pdflatex -interaction=nonstopmode wp.tex ================================================ FILE: wp/references.bib ================================================ %% This BibTeX bibliography file was created using BibDesk. %% http://bibdesk.sourceforge.net/ %% Saved with string encoding Unicode (UTF-8) @article{ref:kremenek, Author = {Kremenek, Ted}, url = "https://llvm.org/devmtg/2008-08/Kremenek_StaticAnalyzer.pdf", Title = {Finding Software Bugs With the Clang Static Analyzer}, Year = {2008}} @article{ref:spa, Author = {Yegor Bugaenko}, Title = {New Object Calculus to Improve Static Program Analysis}, Year = {September 30, 2020} } @article{ref:effective, Author = {Wu Jingyue}, Title = {Effective Dynamic Detection of Alias Analysis Errors}, Year = {2013}, pages = "279-289", journal = "Proceedings of the 9th Joint Meeting on Foundations of Software, Engineering" } @article{ref:simple, Author = {Wand Mitchell}, Title = {A Simple Algorithm and Proof for Type Inference}, Year = {1987}, journal = "Fundamenta Informaticae 10.2", pages = "115–121" } @article{ref:dis, Author = {Jiri Slaby}, Title = {Automatic Bug-finding Techniques for Large Software Projects}, Year = {2013} } @book{ref:ast, title={Abstract Syntax Tree}, author={Miller, F.P. and Vandome, A.F. and John, M.B.}, isbn={9786133773127}, url={https://books.google.ru/books?id=5ns7YgEACAAJ}, year={2010}, publisher={VDM Publishing} } @book{ref:cleancode, title={Clean Code: A Handbook of Agile Software Craftsmanship}, author={Martin, R.C.}, isbn={9780132350884}, lccn={20080240}, series={Robert C. Martin series}, url={https://books.google.ru/books?id=dwSfGQAACAAJ}, year={2009}, publisher={Prentice Hall} } @book{ref:gang, title={Design Patterns}, author={Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides}, isbn={9780201485370}, year={2000}, publisher={Pearson Education} } @book{ref:sca, title={Static Code Analysis: Lint, Static Code Analysis, Data-Flow Analysis, Parasoft, Clang, List of Tools for Static Code Analysis}, author={LLC Books}, isbn={9781155282619}, url={https://books.google.ru/books?id=rYurSQAACAAJ}, year={2010}, publisher={General Books LLC} } @book{ref:kotlinInAction, title={Kotlin in Action}, author={Dmitry Jemerov, Svetlana Isakova}, pages = {88-91}, year={2017}, publisher={ Manning Publications} } @article{ref:offKotlin, Author = {Jetbrains}, Title = {Kotlin official documentation}, url = "https://kotlinlang.org/docs/reference/", year = {2020} } @article{ref:cicd, Author = {Sten Pittet}, Title = {Continuous integration vs. continuous delivery vs. continuous deployment}, url = "https://www.atlassian.com/continuous-delivery/principles/continuous-integration-vs-delivery-vs-deployment", year = {2017} } ================================================ FILE: wp/sections/appendix.tex ================================================ \subsection{Class diagram} \includegraphics[scale=0.5]{pictures/class.PNG} \subsection{Data Flow diagram} \includegraphics[scale=0.6]{pictures/data_flow.PNG} \subsection{Sequence diagram} \includegraphics[scale=0.7]{pictures/sequence.PNG} \subsection{Use-case diagram} \includegraphics[scale=0.8]{pictures/useCase.png} \subsection{Package diagram} \includegraphics[scale=0.65]{pictures/package.png} \section*{Available Rules} \scriptsize \begin{longtable}{ |l|p{0.8cm}|p{0.8cm}| p{3cm} | } \hline \multicolumn{4}{|c|}{Available Rules} \\ \hline \textbf{diKTat rule} & \textbf{code style} & \textbf{autofix} & \textbf{config} \\ \hline VARIABLE\underline{ }NAME\underline{ }INCORRECT & \hyperref[sec:1.1.1]{1.1.1} & no & no \\ VARIABLE\underline{ }HAS\underline{ }PREFIX & \hyperref[sec:1.1.1]{1.1.1} & yes & no \\ IDENTIFIER\underline{ }LENGTH & \hyperref[sec:1.1.1]{1.1.1} & no & no \\ GENERIC\underline{ }NAME & \hyperref[sec:1.1.1]{1.1.1} & yes & no \\ BACKTICKS\underline{ }PROHIBITED & \hyperref[sec:1.1.1]{1.1.1} & no & no \\ FILE\underline{ }NAME\underline{ }INCORRECT & \hyperref[sec:1.1.1]{1.1.1} & yes & no \\ EXCEPTION\underline{ }SUFFIX & \hyperref[sec:1.1.1]{1.1.1} & yes & no \\ CONFUSING\underline{ }IDENTIFIER\underline{ }NAMING & \hyperref[sec:1.1.1]{1.1.1} & no & no \\ PACKAGE\underline{ }NAME\underline{ }MISSING & \hyperref[sec:1.2.1]{1.2.1} & yes & no \\ PACKAGE\underline{ }NAME\underline{ }INCORRECT\underline{ }CASE & \hyperref[sec:1.2.1]{1.2.1} & yes & no \\ PACKAGE\underline{ }NAME\underline{ }INCORRECT\underline{ }PREFIX & \hyperref[sec:1.2.1]{1.2.1} & yes & no \\ PACKAGE\underline{ }NAME\underline{ }INCORRECT\underline{ }SYMBOLS & \hyperref[sec:1.2.1]{1.2.1} & no & no \\ PACKAGE\underline{ }NAME\underline{ }INCORRECT\underline{ }PATH & \hyperref[sec:1.2.1]{1.2.1} & yes & no \\ INCORRECT\underline{ }PACKAGE\underline{ }SEPARATOR & \hyperref[sec:1.2.1]{1.2.1} & yes & no \\ CLASS\underline{ }NAME\underline{ }INCORRECT & \hyperref[sec:1.3.1]{1.3.1} & yes & no \\ OBJECT\underline{ }NAME\underline{ }INCORRECT & \hyperref[sec:1.3.1]{1.3.1} & yes & no \\ ENUM\underline{ }VALUE & \hyperref[sec:1.3.1]{1.3.1} & yes & enumStyle: snakeCase, pascalCase \\ TYPEALIAS\underline{ }NAME\underline{ }INCORRECT\underline{ }CASE & \hyperref[sec:1.3.1]{1.3.1} & yes & no \\ FUNCTION\underline{ }NAME\underline{ }INCORRECT\underline{ }CASE & \hyperref[sec:1.4.1]{1.4.1} & yes & no \\ CONSTANT\underline{ }UPPERCASE & \hyperref[sec:1.5.1]{1.5.1} & yes & no \\ VARIABLE\underline{ }NAME\underline{ }INCORRECT\underline{ }FORMAT & \hyperref[sec:1.6.1]{1.6.1} & yes & no \\ FUNCTION\underline{ }BOOLEAN\underline{ }PREFIX & \hyperref[sec:1.6.2]{1.6.2} & yes & no \\ MISSING\underline{ }KDOC\underline{ }TOP\underline{ }LEVEL & \hyperref[sec:2.1.1]{2.1.1} & no & no \\ MISSING\underline{ }KDOC\underline{ }CLASS\underline{ }ELEMENTS & \hyperref[sec:2.1.1]{2.1.1} & no & no \\ MISSING\underline{ }KDOC\underline{ }ON\underline{ }FUNCTION & \hyperref[sec:2.1.1]{2.1.1} & yes & no \\ KDOC\underline{ }NO\underline{ }CONSTRUCTOR\underline{ }PROPERTY & \hyperref[sec:2.1.1]{2.1.1} & yes & no \\ KDOC\underline{ }NO\underline{ }CLASS\underline{ }BODY\underline{ }PROPERTIES\underline{ }IN\underline{ }HEADER & \hyperref[sec:2.1.1]{2.1.1} & yes & no \\ KDOC\underline{ }EXTRA\underline{ }PROPERTY & \hyperref[sec:2.1.1]{2.1.1} & no & no \\ KDOC\underline{ }NO\underline{ }CONSTRUCTOR\underline{ }PROPERTY\underline{ }WITH\underline{ }COMMENT & \hyperref[sec:2.1.1]{2.1.1} & yes & no \\ KDOC\underline{ }WITHOUT\underline{ }PARAM\underline{ }TAG & \hyperref[sec:2.1.2]{2.1.2} & yes & no \\ KDOC\underline{ }WITHOUT\underline{ }RETURN\underline{ }TAG & \hyperref[sec:2.1.2]{2.1.2} & yes & no \\ KDOC\underline{ }WITHOUT\underline{ }THROWS\underline{ }TAG & \hyperref[sec:2.1.2]{2.1.2} & yes & no \\ KDOC\underline{ }EMPTY\underline{ }KDOC & \hyperref[sec:2.1.3]{2.1.3} & no & no \\ KDOC\underline{ }WRONG\underline{ }SPACES\underline{ }AFTER\underline{ }TAG & \hyperref[sec:2.1.3]{2.1.3} & yes & no \\ KDOC\underline{ }WRONG\underline{ }TAGS\underline{ }ORDER & \hyperref[sec:2.1.3]{2.1.3} & yes & no \\ KDOC\underline{ }NEWLINES\underline{ }BEFORE\underline{ }BASIC\underline{ }TAGS & \hyperref[sec:2.1.3]{2.1.3} & yes & no \\ KDOC\underline{ }NO\underline{ }NEWLINES\underline{ }BETWEEN\underline{ }BASIC\underline{ }TAGS & \hyperref[sec:2.1.3]{2.1.3} & yes & no \\ KDOC\underline{ }NO\underline{ }NEWLINE\underline{ }AFTER\underline{ }SPECIAL\underline{ }TAGS & \hyperref[sec:2.1.3]{2.1.3} & yes & no \\ KDOC\underline{ }NO\underline{ }DEPRECATED\underline{ }TAG & \hyperref[sec:2.1.3]{2.1.3} & yes & no \\ KDOC\underline{ }CONTAINS\underline{ }DATE\underline{ }OR\underline{ }AUTHOR & \hyperref[sec:2.1.3]{2.1.3} & no & no \\ KDOC\underline{ }NO\underline{ }EMPTY\underline{ }TAGS & \hyperref[sec:2.2.1]{2.2.1} & no & no \\ HEADER\underline{ }WRONG\underline{ }FORMAT & \hyperref[sec:2.2.1]{2.2.1} & yes & no \\ HEADER\underline{ }MISSING\underline{ }OR\underline{ }WRONG\underline{ }COPYRIGHT & \hyperref[sec:2.2.1]{2.2.1} & yes & isCopyrightMandatory, copyrightText (sets the copyright pattern for your project, you also can use `;@currYear;` key word in your text in aim to indicate current year, instead of explicit specifying) \\ WRONG\underline{ }COPYRIGHT\underline{ }YEAR & \hyperref[sec:2.2.1]{2.2.1} & yes & no \\ HEADER\underline{ }MISSING\underline{ }IN\underline{ }NON\underline{ }SINGLE\underline{ }CLASS\underline{ }FILE & \hyperref[sec:2.2.1]{2.2.1} & no & no \\ HEADER\underline{ }NOT\underline{ }BEFORE\underline{ }PACKAGE & \hyperref[sec:2.2.1]{2.2.1} & yes & no \\ KDOC\underline{ }TRIVIAL\underline{ }KDOC\underline{ }ON\underline{ }FUNCTION & \hyperref[sec:2.3.1]{2.3.1} & no & no \\ WRONG\underline{ }NEWLINES\underline{ }AROUND\underline{ }KDOC & \hyperref[sec:2.4.1]{2.4.1} & yes & no \\ FIRST\underline{ }COMMENT\underline{ }NO\underline{ }BLANK\underline{ }LINE & \hyperref[sec:2.4.1]{2.4.1} & yes & no \\ COMMENT\underline{ }WHITE\underline{ }SPACE & \hyperref[sec:2.4.1]{2.4.1} & yes & maxSpaces \\ IF\underline{ }ELSE\underline{ }COMMENTS & \hyperref[sec:2.4.1]{2.4.1} & yes & no \\ COMMENTED\underline{ }OUT\underline{ }CODE & \hyperref[sec:2.4.2]{2.4.2} & no & no \\ FILE\underline{ }IS\underline{ }TOO\underline{ }LONG & \hyperref[sec:3.1.1]{3.1.1} & no & maxSize \\ FILE\underline{ }CONTAINS\underline{ }ONLY\underline{ }COMMENTS & \hyperref[sec:3.1.2]{3.1.2} & no & no \\ FILE\underline{ }INCORRECT\underline{ }BLOCKS\underline{ }ORDER & \hyperref[sec:3.1.2]{3.1.2} & yes & no \\ FILE\underline{ }NO\underline{ }BLANK\underline{ }LINE\underline{ }BETWEEN\underline{ }BLOCKS & \hyperref[sec:3.1.2]{3.1.2} & yes & no \\ FILE\underline{ }UNORDERED\underline{ }IMPORTS & \hyperref[sec:3.1.2]{3.1.2} & yes & no \\ FILE\underline{ }WILDCARD\underline{ }IMPORTS & \hyperref[sec:3.1.2]{3.1.2} & no & allowedWildcards \\ UNUSED\underline{ }IMPORT & \hyperref[sec:3.1.2]{3.1.2} & yes & deleteUnusedImport \\ FILE\underline{ }NAME\underline{ }MATCH\underline{ }CLASS & \hyperref[sec:3.1.2]{3.1.2} & yes & no \\ WRONG\underline{ }ORDER\underline{ }IN\underline{ }CLASS\underline{ }LIKE\underline{ }STRUCTURES & \hyperref[sec:3.1.4]{3.1.4} & yes & no \\ BLANK\underline{ }LINE\underline{ }BETWEEN\underline{ }PROPERTIES & \hyperref[sec:3.1.4]{3.1.4} & yes & no \\ WRONG\underline{ }DECLARATIONS\underline{ }ORDER & \hyperref[sec:3.1.4]{3.1.4} & yes & no \\ TOP\underline{ }LEVEL\underline{ }ORDER & \hyperref[sec:3.1.5]{3.1.5} & yes & no \\ NO\underline{ }BRACES\underline{ }IN\underline{ }CONDITIONALS\underline{ }AND\underline{ }LOOPS & \hyperref[sec:3.2.1]{3.2.1} & yes & no \\ BRACES\underline{ }BLOCK\underline{ }STRUCTURE\underline{ }ERROR & \hyperref[sec:3.2.2]{3.2.2} & yes & openBraceNewline closeBraceNewline \\ WRONG\underline{ }INDENTATION & \hyperref[sec:3.3.1]{3.3.1} & yes & extendedIndentOfParameters alignedParameters extendedIndentForExpressionBodies extendedIndentAfterOperators extendedIndentBeforeDot indentationSize \\ EMPTY\underline{ }BLOCK\underline{ }STRUCTURE\underline{ }ERROR & \hyperref[sec:3.4.1]{3.4.1} & yes & allowEmptyBlocks styleEmptyBlockWithNewline \\ LONG\underline{ }LINE & \hyperref[sec:3.5.1]{3.5.1} & yes & lineLength \\ MORE\underline{ }THAN\underline{ }ONE\underline{ }STATEMENT\underline{ }PER\underline{ }LINE & \hyperref[sec:3.6.1]{3.6.1} & yes & no \\ REDUNDANT\underline{ }SEMICOLON & \hyperref[sec:3.6.2]{3.6.2} & yes & no \\ WRONG\underline{ }NEWLINES & \hyperref[sec:3.6.2]{3.6.2} & yes & no \\ TRAILING\underline{ }COMMA & \hyperref[sec:3.6.2]{3.6.2} & yes & valueArgument valueParameter indices whenConditions collectionLiteral typeArgument typeParameter destructuringDeclaration \\ COMPLEX\underline{ }EXPRESSION & \hyperref[sec:3.6.3]{3.6.3} & no & no \\ COMPLEX\underline{ }BOOLEAN\underline{ }EXPRESSION & \hyperref[sec:3.6.4]{3.6.4} & yes & no \\ TOO\underline{ }MANY\underline{ }BLANK\underline{ }LINES & \hyperref[sec:3.7.1]{3.7.1} & yes & no \\ WRONG\underline{ }WHITESPACE & \hyperref[sec:3.8.1]{3.8.1} & yes & no \\ TOO\underline{ }MANY\underline{ }CONSECUTIVE\underline{ }SPACES & \hyperref[sec:3.8.1]{3.8.1} & yes & maxSpaces saveInitialFormattingForEnums \\ ENUMS\underline{ }SEPARATED & \hyperref[sec:3.9.1]{3.9.1} & yes & no \\ LOCAL\underline{ }VARIABLE\underline{ }EARLY\underline{ }DECLARATION & \hyperref[sec:3.10.2]{3.10.2} & no & no \\ WHEN\underline{ }WITHOUT\underline{ }ELSE & \hyperref[sec:3.11.1]{3.11.1} & yes & no \\ ANNOTATION\underline{ }NEW\underline{ }LINE & \hyperref[sec:3.12.1]{3.12.1} & yes & no \\ WRONG\underline{ }MULTIPLE\underline{ }MODIFIERS\underline{ }ORDER & \hyperref[sec:3.14.1]{3.14.1} & yes & no \\ LONG\underline{ }NUMERICAL\underline{ }VALUES\underline{ }SEPARATED & \hyperref[sec:3.14.2]{3.14.2} & yes & maxNumberLength maxBlockLength \\ MAGIC\underline{ }NUMBER & \hyperref[sec:3.14.3]{3.14.3} & no & ignoreNumbers, ignoreHashCodeFunction, ignorePropertyDeclaration, ignoreLocalVariableDeclaration, ignoreConstantDeclaration, ignoreCompanionObjectPropertyDeclaration, ignoreEnums, ignoreRanges, ignoreExtensionFunctions \\ STRING\underline{ }CONCATENATION & \hyperref[sec:3.15.1]{3.15.1} & yes & no \\ STRING\underline{ }TEMPLATE\underline{ }CURLY\underline{ }BRACES & \hyperref[sec:3.15.2]{3.15.2} & yes & no \\ STRING\underline{ }TEMPLATE\underline{ }QUOTES & \hyperref[sec:3.15.2]{3.15.2} & yes & no \\ COLLAPSE\underline{ }IF\underline{ }STATEMENTS & \hyperref[sec:3.16.1]{3.16.1} & yes & no \\ CONVENTIONAL\underline{ }RANGE & \hyperref[sec:3.17.1]{3.17.1} & yes & no \\ DEBUG\underline{ }PRINT & \hyperref[sec:3.18.1]{3.18.1} & no & no \\ FLOAT\underline{ }IN\underline{ }ACCURATE\underline{ }CALCULATIONS & \hyperref[sec:4.1.1]{4.1.1} & no & no \\ SAY\underline{ }NO\underline{ }TO\underline{ }VAR & \hyperref[sec:4.1.3]{4.1.3} & no & no \\ SMART\underline{ }CAST\underline{ }NEEDED & \hyperref[sec:4.2.1]{4.2.1} & yes & no \\ TYPE\underline{ }ALIAS & \hyperref[sec:4.2.2]{4.2.2} & no & typeReferenceLength \\ NULLABLE\underline{ }PROPERTY\underline{ }TYPE & \hyperref[sec:4.3.1]{4.3.1} & yes & no \\ GENERIC\underline{ }VARIABLE\underline{ }WRONG\underline{ }DECLARATION & \hyperref[sec:4.3.2]{4.3.2} & yes & no \\ AVOID\underline{ }NULL\underline{ }CHECKS & \hyperref[sec:4.3.3]{4.3.3} & no & no \\ TOO\underline{ }LONG\underline{ }FUNCTION & \hyperref[sec:5.1.1]{5.1.1} & no & maxFunctionLength isIncludeHeader \\ NESTED\underline{ }BLOCK & \hyperref[sec:5.1.2]{5.1.2} & no & maxNestedBlockQuantit \\ AVOID\underline{ }NESTED\underline{ }FUNCTIONS & \hyperref[sec:5.1.3]{5.1.3} & yes & no \\ INVERSE\underline{ }FUNCTION\underline{ }PREFERRED & \hyperref[sec:5.1.4]{5.1.4} & yes & - \\ LAMBDA\underline{ }IS\underline{ }NOT\underline{ }LAST\underline{ }PARAMETER & \hyperref[sec:5.2.1]{5.2.1} & no & no \\ TOO\underline{ }MANY\underline{ }PARAMETERS & \hyperref[sec:5.2.2]{5.2.2} & no & maxParameterListSize \\ WRONG\underline{ }OVERLOADING\underline{ }FUNCTION\underline{ }ARGUMENTS & \hyperref[sec:5.2.3]{5.2.3} & no & no \\ RUN\underline{ }BLOCKING\underline{ }INSIDE\underline{ }ASYNC & \hyperref[sec:5.2.4]{5.2.4} & no & no \\ TOO\underline{ }MANY\underline{ }LINES\underline{ }IN\underline{ }LAMBDA & \hyperref[sec:5.2.5]{5.2.5} & no & maxLambdaLength \\ CUSTOM\underline{ }LABEL & \hyperref[sec:5.2.6]{5.2.6} & no & no\\ PARAMETER\underline{ }NAME\underline{ }IN\underline{ }OUTER\underline{ }LAMBDA & \hyperref[sec:5.2.7]{5.2.7} & no & no\\ SINGLE\underline{ }CONSTRUCTOR\underline{ }SHOULD\underline{ }BE\underline{ }PRIMARY & \hyperref[sec:6.1.1]{6.1.1} & yes & no \\ USE\underline{ }DATA\underline{ }CLASS & \hyperref[sec:6.1.2]{6.1.2} & no & no \\ EMPTY\underline{ }PRIMARY\underline{ }CONSTRUCTOR & \hyperref[sec:6.1.3]{6.1.3} & yes & no \\ MULTIPLE\underline{ }INIT\underline{ }BLOCKS & \hyperref[sec:6.1.4]{6.1.4} & yes & no \\ USELESS\underline{ }SUPERTYPE & \hyperref[sec:6.1.5]{6.1.5} & yes & no \\ CLASS\underline{ }SHOULD\underline{ }NOT\underline{ }BE\underline{ }ABSTRACT & \hyperref[sec:6.1.6]{6.1.6} & yes & no \\ NO\underline{ }CORRESPONDING\underline{ }PROPERTY & \hyperref[sec:6.1.7]{6.1.7} & no & no \\ CUSTOM\underline{ }GETTERS\underline{ }SETTERS & \hyperref[sec:6.1.8]{6.1.8} & no & no \\ WRONG\underline{ }NAME\underline{ }OF\underline{ }VARIABLE\underline{ }INSIDE\underline{ }ACCESSOR & \hyperref[sec:6.1.9]{6.1.9} & no & no \\ TRIVIAL\underline{ }ACCESSORS\underline{ }ARE\underline{ }NOT\underline{ }RECOMMENDED & \hyperref[sec:6.1.10]{6.1.10} & yes & no \\ COMPACT\underline{ }OBJECT\underline{ }INITIALIZATION & \hyperref[sec:6.1.11]{6.1.11} & yes & no \\ INLINE\underline{ }CLASS\underline{ }CAN\underline{ }BE\underline{ }USED & \hyperref[sec:6.1.12]{6.1.12} & yes & no \\ EXTENSION\underline{ }FUNCTION\underline{ }SAME\underline{ }SIGNATURE & \hyperref[sec:6.2.2]{6.2.2} & no & no \\ EXTENSION\underline{ }FUNCTION\underline{ }WITH\underline{ }CLASS & \hyperref[sec:6.2.3]{6.2.3} & no & no \\ USE\underline{ }LAST\underline{ }INDEX & \hyperref[sec:6.2.4]{6.2.4} & yes & no \\ AVOID\underline{ }USING\underline{ }UTILITY\underline{ }CLASS & \hyperref[sec:6.4.1]{6.4.1} & no & no \\ OBJECT\underline{ }IS\underline{ }PREFERRED & \hyperref[sec:6.4.2]{6.4.2} & yes & no \\ RUN\underline{ }IN\underline{ }SCRIPT & \hyperref[sec:6.5.1]{6.5.1} & yes & no \\ \hline \end{longtable} %CodeStyle \hspace{0.0cm}\hyperref[sec:]{} \section*{Diktat Coding Style Guide, v.0.0.1} \section*{Table of contents} \hspace{0.0cm}\hyperref[sec:]{I Preface} \hspace{0.5cm}\hyperref[sec:]{ I.I Purpose of this document} \hspace{0.5cm}\hyperref[sec:]{ I.II General principles} \hspace{0.5cm}\hyperref[sec:]{ I.III Terminology} \hspace{0.5cm}\hyperref[sec:]{ I.IV Exceptions} \hspace{0.0cm}\hyperref[sec:1.]{1. Naming} \hspace{0.5cm}\hyperref[sec:1.1]{ 1.1 Identifiers} \hspace{0.5cm}\hyperref[sec:1.2]{ 1.2 Packages} \hspace{0.5cm}\hyperref[sec:1.3]{ 1.3 Classes, enumerations, interfaces} \hspace{0.5cm}\hyperref[sec:1.4]{ 1.4 Functions} \hspace{0.5cm}\hyperref[sec:1.5]{ 1.5 Constants} \hspace{0.5cm}\hyperref[sec:1.6]{ 1.6 Non-constant fields (variables)} \hspace{1.0cm}\hyperref[sec:1.6.1]{ 1.6.1 Non-constant field name} \hspace{1.0cm}\hyperref[sec:1.6.2]{ 1.6.2 Boolean variable names with negative meaning} \hspace{0.0cm}\hyperref[sec:2.]{2. Comments} \hspace{0.5cm}\hyperref[sec:2.1]{ 2.1 General form of Kdoc} \hspace{1.0cm}\hyperref[sec:2.1.1]{ 2.1.1 Using KDoc for the public, protected, or internal code elements} \hspace{1.0cm}\hyperref[sec:2.1.2]{ 2.1.2 Describing methods that have arguments, a return value, or can throw an exception in the KDoc block} \hspace{1.0cm}\hyperref[sec:2.1.3]{ 2.1.3 Only one space between the Kdoc tag and content. Tags are arranged in the order.} \hspace{0.5cm}\hyperref[sec:2.2]{ 2.2 Adding comments on the file header} \hspace{0.5cm}\hyperref[sec:2.3]{ 2.3 Comments on the function header} \hspace{0.5cm}\hyperref[sec:2.4]{ 2.4 Code comments} \hspace{1.0cm}\hyperref[sec:2.4.1]{ 2.4.1 Add a blank line between the body of the comment and Kdoc tag-blocks} \hspace{1.0cm}\hyperref[sec:2.4.2]{ 2.4.2 Do not comment on unused code blocks} \hspace{1.0cm}\hyperref[sec:2.4.3]{ 2.4.3 Code delivered to the client should not contain TODO/FIXME comments} \hspace{1.0cm}\hyperref[sec:]{} \hspace{0.0cm}\hyperref[sec:3.]{3. General formatting (typesetting)} \hspace{0.5cm}\hyperref[sec:3.1]{ 3.1 File-related rules} \hspace{1.0cm}\hyperref[sec:3.1.1]{ 3.1.1 Avoid files that are too long} \hspace{1.0cm}\hyperref[sec:3.1.2]{ 3.1.2 Code blocks in the source file should be separated by one blank line} \hspace{1.0cm}\hyperref[sec:3.1.3]{ 3.1.3 Import statements order} \hspace{1.0cm}\hyperref[sec:3.1.4]{ 3.1.4 Order of declaration parts of class-like code structures} \hspace{1.0cm}\hyperref[sec:3.1.5]{ 3.1.5 Order of declaration of top-level code structures} \hspace{0.5cm}\hyperref[sec:3.2]{ 3.2 Braces} \hspace{1.0cm}\hyperref[sec:3.2.1]{ 3.2.1 Using braces in conditional statements and loop blocks} \hspace{1.0cm}\hyperref[sec:3.2.2]{ 3.2.2 Opening braces are placed at the end of the line in non-empty blocks and block structures} \hspace{0.5cm}\hyperref[sec:3.3]{ 3.3 Indentation} \hspace{0.5cm}\hyperref[sec:3.4]{ 3.4 Empty blocks} \hspace{0.5cm}\hyperref[sec:3.5]{ 3.5 Line length} \hspace{0.5cm}\hyperref[sec:3.6]{ 3.6 Line breaks (newlines)} \hspace{1.0cm}\hyperref[sec:3.6.1]{ 3.6.1 Each line can have a maximum of one statement} \hspace{1.0cm}\hyperref[sec:3.6.2]{ 3.6.2 Rules for line-breaking} \hspace{0.5cm}\hyperref[sec:3.7]{ 3.7 Using blank lines} \hspace{0.5cm}\hyperref[sec:3.8]{ 3.8 Horizontal space} \hspace{1.0cm}\hyperref[sec:3.8.1]{ 3.8.1 Usage of whitespace for code separation} \hspace{1.0cm}\hyperref[sec:3.8.2]{ 3.8.2 No spaces for horizontal alignment} \hspace{0.5cm}\hyperref[sec:3.9]{ 3.9 Enumerations} \hspace{0.5cm}\hyperref[sec:3.10]{ 3.10 Variable declaration} \hspace{1.0cm}\hyperref[sec:3.10.1]{ 3.10.1 Declare one variable per line} \hspace{1.0cm}\hyperref[sec:3.10.2]{ 3.10.2 Variables should be declared near the line where they are first used} \hspace{0.5cm}\hyperref[sec:3.11]{ 3.11 'When' expression} \hspace{0.5cm}\hyperref[sec:3.12]{ 3.12 Annotations} \hspace{0.5cm}\hyperref[sec:3.13]{ 3.13 Block comments} \hspace{0.5cm}\hyperref[sec:3.14]{ 3.14 Modifiers and constant values} \hspace{1.0cm}\hyperref[sec:3.14.1]{ 3.14.1 Declaration with multiple modifiers} \hspace{1.0cm}\hyperref[sec:3.14.2]{ 3.14.2 Separate long numerical values with an underscore} \hspace{1.0cm}\hyperref[sec:3.15]{ 3.15 Strings} \hspace{1.0cm}\hyperref[sec:3.15.1]{ 3.15.1 Concatenation of Strings} \hspace{1.0cm}\hyperref[sec:3.15.2]{ 3.15.2 String template format} \hspace{0.0cm}\hyperref[sec:4.]{4. Variables and types} \hspace{0.5cm}\hyperref[sec:4.1]{ 4.1 Variables} \hspace{1.0cm}\hyperref[sec:4.1.1]{ 4.1.1 Do not use Float and Double types when accurate calculations are needed} \hspace{1.0cm}\hyperref[sec:4.1.2]{ 4.1.2 Comparing numeric float type values} \hspace{1.0cm}\hyperref[sec:4.1.3]{ 4.1.3 Try to use 'val' instead of 'var' for variable declaration SAY_NO_TO_VAR} \hspace{0.5cm}\hyperref[sec:4.2]{ 4.2 Types} \hspace{1.0cm}\hyperref[sec:4.2.1]{ 4.2.1 Use Contracts and smart cast as much as possible} \hspace{1.0cm}\hyperref[sec:4.2.2]{ 4.2.2 Try to use type alias to represent types making code more readable} \hspace{0.5cm}\hyperref[sec:4.3]{ 4.3 Null safety and variable declarations} \hspace{1.0cm}\hyperref[sec:4.3.1]{ 4.3.1 Avoid declaring variables with nullable types, especially from Kotlin stdlib} \hspace{1.0cm}\hyperref[sec:4.3.2]{ 4.3.2 Variables of generic types should have an explicit type declaration} \hspace{1.0cm}\hyperref[sec:4.3.3]{ 4.3.3 Null-safety} \hspace{0.0cm}\hyperref[sec:5.]{5. Functions} \hspace{0.5cm}\hyperref[sec:5.1]{ 5.1 Function design} \hspace{1.0cm}\hyperref[sec:5.1.1]{ 5.1.1 Avoid functions that are too long } \hspace{1.0cm}\hyperref[sec:5.1.2]{ 5.1.2 Avoid deep nesting of function code blocks, limiting to four levels} \hspace{1.0cm}\hyperref[sec:5.1.3]{ 5.1.3 Avoid using nested functions} \hspace{1.0cm}\hyperref[sec:5.1.4]{ 5.1.4 Negated function calls} \hspace{0.5cm}\hyperref[sec:5.2]{ 5.2 Function arguments} \hspace{1.0cm}\hyperref[sec:5.2.1]{ 5.2.1 The lambda parameter of the function should be placed at the end of the argument list} \hspace{1.0cm}\hyperref[sec:5.2.2]{ 5.2.2 Number of function parameters should be limited to five} \hspace{1.0cm}\hyperref[sec:5.2.3]{ 5.2.3 Use default values for function arguments instead of overloading them} \hspace{1.0cm}\hyperref[sec:5.2.4]{ 5.2.4 Synchronizing code inside asynchronous code} \hspace{1.0cm}\hyperref[sec:5.2.5]{ 5.2.5 Long lambdas should have explicit parameters} \hspace{1.0cm}\hyperref[sec:5.2.6]{ 5.2.6 Avoid using unnecessary, custom label} \hspace{0.0cm}\hyperref[sec:6.]{6. Classes, interfaces, and extension functions} \hspace{0.5cm}\hyperref[sec:6.1]{ 6.1 Classes} \hspace{1.0cm}\hyperref[sec:6.1.1]{ 6.1.1 Denoting a class with a single constructor} \hspace{1.0cm}\hyperref[sec:6.1.2]{ 6.1.2 Prefer data classes instead of classes without any functional logic} \hspace{1.0cm}\hyperref[sec:6.1.3]{ 6.1.3 Do not use the primary constructor if it is empty or useless} \hspace{1.0cm}\hyperref[sec:6.1.4]{ 6.1.4 Do not use redundant init blocks in your class} \hspace{1.0cm}\hyperref[sec:6.1.5]{ 6.1.5 Explicit supertype qualification} \hspace{1.0cm}\hyperref[sec:6.1.6]{ 6.1.6 Abstract class should have at least one abstract method} \hspace{1.0cm}\hyperref[sec:6.1.7]{ 6.1.7 When using the "implicit backing property" scheme, the name of real and back property should be the same} \hspace{1.0cm}\hyperref[sec:6.1.8]{ 6.1.8 Avoid using custom getters and setters} \hspace{1.0cm}\hyperref[sec:6.1.9]{ 6.1.9 Never use the name of a variable in the custom getter or setter (possible_bug)} \hspace{1.0cm}\hyperref[sec:6.1.10]{ 6.1.10 No trivial getters and setters are allowed in the code} \hspace{1.0cm}\hyperref[sec:6.1.11]{ 6.1.11 Use 'apply' for grouping object initialization} \hspace{1.0cm}\hyperref[sec:6.1.12]{ 6.1.12 Prefer Inline classes when a class has a single property} \hspace{0.5cm}\hyperref[sec:6.2]{ 6.2 Extension functions} \hspace{1.0cm}\hyperref[sec:6.2.1]{ 6.2.1 Use extension functions for making logic of classes less coupled} \hspace{1.0cm}\hyperref[sec:6.2.2]{ 6.2.2 No extension functions with the same name and signature if they extend base and inheritor classes (possible_bug)} \hspace{1.0cm}\hyperref[sec:6.2.3]{ 6.2.3 Don't use extension functions for the class in the same file} \hspace{0.5cm}\hyperref[sec:6.3]{ 6.3 Interfaces} \hspace{0.5cm}\hyperref[sec:6.4]{ 6.4 Objects} \hspace{1.0cm}\hyperref[sec:6.4.1]{ 6.4.1 Instead of using utility classes/objects, use extensions} \hspace{1.0cm}\hyperref[sec:6.4.2]{ 6.4.2 Objects should be used for Stateless Interfaces} \section*{Diktat Coding Style Guide} \section*{International version, v.0.0.1} \hspace{0.0cm}\hyperref[sec:]{} \subsection*{\textbf{Purpose of this document}} \label{sec:} The purpose of this document is to provide a specification that software developers could reference to enhance their ability to write consistent, easy-to-read, and high-quality code. Such a specification will ultimately improve software development efficiency and product competitiveness. For the code to be considered high-quality, it must entail the following characteristics: 1. Simplicity 2. Maintainability 3. Reliability 4. Testability 5. Efficiency 6. Portability 7. Reusability \subsection*{\textbf{General principles}} Like other modern programming languages, Kotlin is an advanced programming language that complies with the following general principles: 1. Clarity — a necessary feature of programs that are easy to maintain and refactor. 2. Simplicity — a code is easy to understand and implement. 3. Consistency — enables a code to be easily modified, reviewed, and understood by the team members. Unification is particularly important when the same team works on the same project, utilizing similar styles enabling a code to be easily modified, reviewed, and understood by the team members. Also, we need to consider the following factors when programming on Kotlin: 1. Writing clean and simple Kotlin code Kotlin combines two of the main programming paradigms: functional and object-oriented. Both of these paradigms are trusted and well-known software engineering practices. As a young programming language, Kotlin is built on top of well-established languages such as Java, C++, C\#, and Scala. This enables Kotlin to introduce many features that help a developer write cleaner, more readable code while also reducing the number of complex code structures. For example, type and null safety, extension functions, infix syntax, immutability, val/var differentiation, expression-oriented features, "when" statements, much easier work with collections, type auto conversion, and other syntactic sugar. 2. Following Kotlin idioms The author of Kotlin, Andrey Breslav, mentioned that Kotlin is both pragmatic and practical, but not academic. Its pragmatic features enable ideas to be transformed into real working software easily. Kotlin is closer to natural languages than its predecessors, and it implements the following design principles: readability, reusability, interoperability, security, and tool-friendliness (https://blog.jetbrains.com/kotlin/2018/10/kotlinconf-2018-announcements/). 3. Using Kotlin efficiently Some Kotlin features can help you to write higher-performance code: including rich coroutine library, sequences, inline functions/classes, arrays of basic types, tailRec, and CallsInPlace of contract. \subsection*{\textbf{Terminology}} \textbf{Rules} — conventions that should be followed when programming. \textbf{Recommendations} — conventions that should be considered when programming. \textbf{Explanation} — necessary explanations of rules and recommendations. \textbf{Valid Example} — recommended examples of rules and recommendations. \textbf{Invalid Example} — not recommended examples of rules and recommendations. Unless otherwise stated, this specification applies to versions 1.3 and later of Kotlin. \subsection*{\textbf{Exceptions}} Even though exceptions may exist, it is essential to understand why rules and recommendations are needed. Depending on a project situation or personal habits, you can break some of the rules. However, remember that one exception may lead to many and eventually can destroy code consistency. As such, there should be very few exceptions. When modifying open-source code or third-party code, you can choose to use the code style from this open-source project (instead of using the existing specifications) to maintain consistency. Software that is directly based on the Android native operating system interface, such as the Android Framework, remains consistent with the Android style. \section*{\textbf{1. Naming}} \label{sec:1.} In programming, it is not always easy to meaningfully and appropriately name variables, functions, classes, etc. Using meaningful names helps to clearly express your code's main ideas and functionality and avoid misinterpretation, unnecessary coding and decoding, "magic" numbers, and inappropriate abbreviations. Note: The source file encoding format (including comments) must be UTF-8 only. The ASCII horizontal space character (0x20, that is, space) is the only permitted whitespace character. Tabs should not be used for indentation. \subsection*{\textbf{1.1 Identifiers}} \label{sec:1.1} This section describes the general rules for naming identifiers. \subsubsection*{\textbf{1.1.1 Identifiers naming conventions}} \leavevmode\newline \label{sec:1.1.1} For identifiers, use the following naming conventions: 1. All identifiers should use only ASCII letters or digits, and the names should match regular expressions \textbf{\textbackslash w\{2,64\}}. Explanation: Each valid identifier name should match the regular expression \textbf{\textbackslash w\{2,64\}}. \textbf{\{2,64\}} means that the name length is 2 to 64 characters, and the length of the variable name should be proportional to its life range, functionality, and responsibility. Name lengths of less than 31 characters are generally recommended. However, this depends on the project. Otherwise, a class declaration with generics or inheritance from a superclass can cause line breaking. No special prefix or suffix should be used in names. The following examples are inappropriate names: name\_, mName, s\_name, and kName. 2. Choose file names that would describe the content. Use camel case (PascalCase) and \textbf{.kt} extension. 3. Typical examples of naming: \begin{center} \begin{tabular}{ |p{5.0cm}|p{5.0cm}|p{5.0cm}| } \hline Meaning&Correct&Incorrect\\ \hline "XML Http Request" & XmlHttpRequest & XMLHTTPRequest \\ "new customer ID" & newCustomerId & newCustomerID \\ "inner stopwatch" & innerStopwatch & innerStopWatch \\ "supports IPv6 on iOS" & supportsIpv6OnIos & supportsIPv6OnIOS \\ "YouTube importer" & YouTubeImporter & YoutubeImporter \\ \hline \end{tabular} \end{center} 4. The usage of (\textbf{}) and free naming for functions and identifiers are prohibited. For example, the following code is not recommended: \begin{lstlisting}[language=Kotlin] val `my dummy name-with-minus` = "value" \end{lstlisting} The only exception is function names in \textbf{Unit tests.} 5. Backticks (\textbf{}) should not be used for identifiers, except the names of test methods (marked with @Test annotation): \begin{lstlisting}[language=Kotlin] @Test fun `my test`() { /*...*/ } \end{lstlisting} 6. The following table contains some characters that may cause confusion. Be careful when using them as identifiers. To avoid issues, use other names instead. \begin{center} \begin{tabular}{ |p{5.0cm}|p{5.0cm}|p{5.0cm}| } \hline Expected&Confusing name&Suggested name\\ \hline 0 (zero) & O, D & obj, dgt \\ 1 (one) & I, l & it, ln, line \\ 2 (two) & Z & n1, n2 \\ 5 (five) & S & xs, str \\ 6 (six) & e & ex, elm \\ 8 (eight) & B & bt, nxt \\ n,h & h,n & nr, head, height \\ rn, m & m,rn & mbr, item \\ \hline \end{tabular} \end{center} \textbf{Exceptions:} - The i,j,k variables used in loops are part of the industry standard. One symbol can be used for such variables. - The \textbf{e} variable can be used to catch exceptions in catch block: \textbf{catch (e: Exception) \{\}} - The Java community generally does not recommend the use of prefixes. However, when developing Android code, you can use the s and m prefixes for static and non-public non-static fields, respectively. Note that prefixing can also negatively affect the style and the auto-generation of getters and setters. \begin{center} \begin{tabular}{ |p{7.5cm}|p{7.5cm}| } \hline Type&Naming style\\ \hline Interfaces, classes, annotations, enumerated types, and object type names & Camel case, starting with a capital letter. Test classes have a Test suffix. The filename is 'TopClassName'.kt. \\ Class fields, local variables, methods, and method parameters & Camel case starting with a low case letter. Test methods can be underlined with '\_'; the only exception is [backing properties]\\ Static constants and enumerated values & Only uppercase underlined with '\_' \\ Generic type variable & Single capital letter, which can be followed by a number, for example: `E, T, U, X, T2` \\ Exceptions & Same as class names, but with a suffix Exception, for example: `AccessException` and `NullPointerException`\\ \hline \end{tabular} \end{center} \subsection*{\textbf{1.2 Packages}} \label{sec:1.2} \subsubsection*{\textbf{Rule 1.2.1 Package names dots}} \leavevmode\newline Package names are in lower case and separated by dots. Code developed within your company should start with \textbf{your.company.domain.} Numbers are permitted in package names. Each file should have a \textbf{package} directive. Package names are all written in lowercase, and consecutive words are concatenated together (no underscores). Package names should contain both the product or module names and the department (or team) name to prevent conflicts with other teams. Numbers are not permitted. For example: \textbf{org.apache.commons.lang3}, \textbf{xxx.yyy.v2}. \textbf{Exceptions:} - In certain cases, such as open-source projects or commercial cooperation, package names should not start with \textbf{your.company.domain.} - If the package name starts with a number or other character that cannot be used at the beginning of the Java/Kotlin package name, then underscores are allowed. For example: \textbf{com.example.\_123name}. - Underscores are sometimes permitted if the package name contains reserved Java/Kotlin keywords, such as \textbf{org.example.hyphenated\_name}, \textbf{int\_.example}. \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] package your.company.domain.mobilecontrol.views \end{lstlisting} \subsection*{\textbf{1.3 Classes}} \label{sec:1.3} This section describes the general rules for naming classes, enumerations, and interfaces. \subsubsection*{\textbf{1.3.1 Classes}} \leavevmode\newline \label{sec:1.3.1} Classes, enumerations, and interface names use \textbf{UpperCamelCase} nomenclature. Follow the naming rules described below: 1. A class name is usually a noun (or a noun phrase) denoted using the camel case nomenclature, such as UpperCamelCase. For example: \textbf{Character} or \textbf{ImmutableList}. An interface name can also be a noun or noun phrase (such as \textbf{List}) or an adjective or adjective phrase (such as \textbf{Readable}). Note that verbs are not used to name classes. However, nouns (such as \textbf{Customer}, \textbf{WikiPage}, and \textbf{Account}) can be used. Try to avoid using vague words such as \textbf{Manager} and \textbf{Process}. 2. Test classes start with the name of the class they are testing and end with 'Test'. For example, \textbf{HashTest} or \textbf{HashIntegrationTest}. \textbf{Invalid example}: \begin{lstlisting}[language=Kotlin] class marcoPolo {} class XMLService {} interface TAPromotion {} class info {} \end{lstlisting} \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] class MarcoPolo {} class XmlService {} interface TaPromotion {} class Order {} \end{lstlisting} \subsection*{\textbf{1.4 Functions}} \label{sec:1.4} This section describes the general rules for naming functions. \subsubsection*{\textbf{1.4.1 Function names should be in camel case}} \leavevmode\newline \label{sec:1.4.1} Function names should use \textbf{lowerCamelCase} nomenclature. Follow the naming rules described below: 1. Function names are usually verbs or verb phrases denoted with the camel case nomenclature (\textbf{lowerCamelCase}). For example: \textbf{sendMessage}, \textbf{stopProcess}, or \textbf{calculateValue}. To name functions, use the following formatting rules: a) To get, modify, or calculate a certain value: get + non-boolean field(). Note that the Kotlin compiler automatically generates getters for some classes, applying the special syntax preferred for the 'get' fields: kotlin private val field: String get() \{ \}. kotlin private val field: String get() \{ \}. \begin{lstlisting}[language=Kotlin] private val field: String get() { } \end{lstlisting} Note: The calling property access syntax is preferred to call getter directly. In this case, the Kotlin compiler automatically calls the corresponding getter. b) \textbf{is} + boolean variable name() c) \textbf{set} + field/attribute name(). However, note that the syntax and code generation for Kotlin are completely the same as those described for the getters in point a. d) \textbf{has} + Noun / adjective () e) verb() Note: Note: Verb are primarily used for the action objects, such as \textbf{document.print ()} f) verb + noun() g) The Callback function allows the names that use the preposition + verb format, such as: \textbf{onCreate()}, \textbf{onDestroy()}, \textbf{toString()}. \textbf{Invalid example}: \begin{lstlisting}[language=Kotlin] fun type(): String fun Finished(): Boolean fun visible(boolean) fun DRAW() fun KeyListener(Listener) \end{lstlisting} \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] fun getType(): String fun isFinished(): Boolean fun setVisible(boolean) fun draw() fun addKeyListener(Listener) \end{lstlisting} 2. An underscore (\textbf{\_}) can be included in the JUnit test function name and should be used as a separator. Each logical part is denoted in \textbf{lowerCamelCase}, for example, a typical pattern of using underscore: \textbf{pop\_emptyStack}. \subsection*{\textbf{1.5 Constants}} \label{sec:1.5} This section describes the general rules for naming constraints. \subsubsection*{\textbf{1.5.1 Using UPPER case and underscore characters in a constraint name}} \leavevmode\newline \label{sec:1.5.1} Constant names should be in UPPER case, words separated by underscore. The general constant naming conventions are listed below: 1. Constants are attributes created with the \textbf{const} keyword or top-level/\textbf{val} local variables of an object that holds immutable data. In most cases, constants can be identified as a \textbf{const val} property from the \textbf{object}/\textbf{companion object}/file top level. These variables contain fixed constant values that typically should never be changed by programmers. This includes basic types, strings, immutable types, and immutable collections of immutable types. The value is not constant for the object, which state can be changed. 2. Constant names should contain only uppercase letters separated by an underscores. They should have a val or const val modifier to make them final explicitly. In most cases, if you need to specify a constant value, then you need to create it with the "const val" modifier. Note that not all \textbf{val} variables are constants. 3. Objects with immutable content, such as \textbf{Logger} and \textbf{Lock}, can be in uppercase as constants or have camel case as regular variables. 4. Use meaningful constants instead of \textbf{magic numbers}. SQL or logging strings should not be treated as magic numbers, nor should they be defined as string constants. Magic constants, such as \textbf{NUM\_FIVE = 5} or \textbf{NUM\_5 = 5} should not be treated as constants. This is because mistakes will easily be made if they are changed to \textbf{NUM\_5 = 50} or 55. These constants typically represent business logic values, such as measures, capacity, scope, location, tax rate, promotional discounts, and power base multiples in algorithms. You can avoid using magic numbers with the following method: - Using library functions and APIs. For example, instead of checking that \textbf{size == 0}, use \textbf{isEmpty()} function. To work with \textbf{time}, use built-ins from \textbf{java.time API}. - Enumerations can be used to name patterns. Refer to [Recommended usage scenario for enumeration in 3.9]. \textbf{Invalid example}: \begin{lstlisting}[language=Kotlin] var int MAXUSERNUM = 200; val String sL = "Launcher"; \end{lstlisting} \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] const val int MAX_USER_NUM = 200; const val String APPLICATION_NAME = "Launcher"; \end{lstlisting} \subsection*{\textbf{1.6 Non-constant fields}} \label{sec:1.6} This section describes the general rules for naming variables. \subsubsection*{\textbf{1.6.1 Non-constant field name}} \leavevmode\newline \label{sec:1.6.1} Non-constant field names should use camel case and start with a lowercase letter. A local variable cannot be treated as constant even if it is final and immutable. Therefore, it should not use the preceding rules. Names of collection type variables (sets, lists, etc.) should contain plural nouns. For example: \textbf{var namesList: List} Names of non-constant variables should use \textbf{lowerCamelCase}. The name of the final immutable field used to store the singleton object can use the same camel case notation. \textbf{Invalid example}: \begin{lstlisting}[language=Kotlin] customername: String user: List = listof() \end{lstlisting} \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] var customerName: String val users: List = listOf(); val mutableCollection: MutableSet = HashSet() \end{lstlisting} \subsubsection*{\textbf{1.6.2 Boolean variable names with negative meaning}} \leavevmode\newline \label{sec:1.6.2} Avoid using Boolean variable names with a negative meaning. When using a logical operator and name with a negative meaning, the code may be difficult to understand, which is referred to as the "double negative". For instance, it is not easy to understand the meaning of !isNotError. The JavaBeans specification automatically generates isXxx() getters for attributes of Boolean classes. However, not all methods returning Boolean type have this notation. For Boolean local variables or methods, it is highly recommended that you add non-meaningful prefixes, including is (commonly used by JavaBeans), has, can, should, and must. Modern integrated development environments (IDEs) such as Intellij are already capable of doing this for you when you generate getters in Java. For Kotlin, this process is even more straightforward as everything is on the byte-code level under the hood. \textbf{Invalid example}: \begin{lstlisting}[language=Kotlin] val isNoError: Boolean val isNotFound: Boolean fun empty() fun next(); \end{lstlisting} \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] val isError: Boolean val isFound: Boolean val hasLicense: Boolean val canEvaluate: Boolean val shouldAbort: Boolean fun isEmpty() fun hasNext() \end{lstlisting} \section*{\textbf{2. Comments}} \label{sec:2.} The best practice is to begin your code with a summary, which can be one sentence. Try to balance between writing no comments at all and obvious commentary statements for each line of code. Comments should be accurately and clearly expressed, without repeating the name of the class, interface, or method. Comments are not a solution to the wrong code. Instead, you should fix the code as soon as you notice an issue or plan to fix it (by entering a TODO comment, including a Jira number). Comments should accurately reflect the code's design ideas and logic and further describe its business logic. As a result, other programmers will be able to save time when trying to understand the code. Imagine that you are writing the comments to help yourself to understand the original ideas behind the code in the future. \subsection*{\textbf{2.1 General form of Kdoc}} \label{sec:2.1} KDoc is a combination of JavaDoc's block tags syntax (extended to support specific constructions of Kotlin) and Markdown's inline markup. The basic format of KDoc is shown in the following example: \begin{lstlisting}[language=Kotlin] /** * There are multiple lines of KDoc text, * Other ... */ fun method(arg: String) { // ... } \end{lstlisting} It is also shown in the following single-line form: \begin{lstlisting}[language=Kotlin] /** Short form of KDoc. */ \end{lstlisting} Use a single-line form when you store the entire KDoc block in one line (and there is no KDoc mark @XXX). For detailed instructions on how to use KDoc, refer to \href{https://docs.oracle.com/en/Kotlin/Kotlinse/11/tools/KDoc.html}{Official Document}. \subsubsection*{\textbf{2.1.1 Using KDoc for the public}} \leavevmode\newline \label{sec:2.1.1} At a minimum, KDoc should be used for every public, protected, or internal decorated class, interface, enumeration, method, and member field (property). Other code blocks can also have KDocs if needed. Instead of using comments or KDocs before properties in the primary constructor of a class - use \textbf{@property} tag in a KDoc of a class. All properties of the primary constructor should also be documented in a KDoc with a \textbf{@property} tag. \textbf{Incorrect example:} \begin{lstlisting}[language=Kotlin] /** * Class description */ class Example( /** * property description */ val foo: Foo, // another property description val bar: Bar ) \end{lstlisting} \textbf{Correct example:} \begin{lstlisting}[language=Kotlin] /** * Class description * @property foo property description * @property bar another property description */ class Example( val foo: Foo, val bar: Bar ) \end{lstlisting} \textbf{Exceptions:} * For setters/getters of properties, obvious comments (like \textbf{this getter returns field}) are optional. Note that Kotlin generates simple \textbf{get/set} methods under the hood. * It is optional to add comments for simple one-line methods, such as shown in the example below: \begin{lstlisting}[language=Kotlin] val isEmpty: Boolean get() = this.size == 0 \end{lstlisting} or \begin{lstlisting}[language=Kotlin] fun isEmptyList(list: List) = list.size == 0 \end{lstlisting} \textbf{Note:} You can skip KDocs for a method's override if it is almost the same as the superclass method. \subsubsection*{\textbf{2.1.2 Describing methods that have arguments}} \leavevmode\newline \label{sec:2.1.2} When the method has such details as arguments, return value, or can throw exceptions, it must be described in the KDoc block (with @param, @return, @throws, etc.). \textbf{Valid examples:} \begin{lstlisting}[language=Kotlin] /** * This is the short overview comment for the example interface. * / * Add a blank line between the comment text and each KDoc tag underneath * / * @since 1.6 */ protected abstract class Sample { /** * This is a long comment with whitespace that should be split in * comments on multiple lines if the line comment formatting is enabled. * / * Add a blank line between the comment text and each KDoc tag underneath * / * @param fox A quick brown fox jumps over the lazy dog * @return battle between fox and dog */ protected abstract fun foo(Fox fox) /** * These possibilities include: Formatting of header comments * / * Add a blank line between the comment text and each KDoc tag underneath * / * @return battle between fox and dog * @throws ProblemException if lazy dog wins */ protected fun bar() throws ProblemException { // Some comments / * No need to add a blank line here * / var aVar = ... // Some comments / * Add a blank line before the comment * / fun doSome() } } \end{lstlisting} \subsubsection*{\textbf{2.1.3 Only one space between the Kdoc tag and content. Tags are arranged in the order.}} \leavevmode\newline \label{sec:2.1.3} There should be only one space between the Kdoc tag and content. Tags are arranged in the following order: @param, @return, and @throws. Therefore, Kdoc should contain the following: - Functional and technical description, explaining the principles, intentions, contracts, API, etc. - The function description and @tags (\textbf{implSpec}, \textbf{apiNote}, and \textbf{implNote}) require an empty line after them. - \textbf{@implSpec}: A specification related to API implementation, and it should let the implementer decide whether to override it. - \textbf{@apiNote}: Explain the API precautions, including whether to allow null and whether the method is thread-safe, as well as the algorithm complexity, input, and output range, exceptions, etc. - \textbf{@implNote}: A note related to API implementation, which implementers should keep in mind. - One empty line, followed by regular \textbf{@param}, \textbf{@return}, \textbf{@throws}, and other comments. - The conventional standard "block labels" are arranged in the following order: \textbf{@param}, \textbf{@return}, \textbf{@throws}. Kdoc should not contain: - Empty descriptions in tag blocks. It is better not to write Kdoc than waste code line space. - There should be no empty lines between the method/class declaration and the end of Kdoc (\textbf{*/} symbols). - \textbf{@author} tag. It doesn't matter who originally created a class when you can use \textbf{git blame} or VCS of your choice to look through the changes history. Important notes: - KDoc does not support the \textbf{@deprecated} tag. Instead, use the \textbf{@Deprecated} annotation. - The \textbf{@since} tag should be used for versions only. Do not use dates in \textbf{@since} tag, it's confusing and less accurate. If a tag block cannot be described in one line, indent the content of the new line by \textit{four spaces} from the \textbf{@} position to achieve alignment (\textbf{@} counts as one + three spaces). \textbf{Exception:} When the descriptive text in a tag block is too long to wrap, you can indent the alignment with the descriptive text in the last line. The descriptive text of multiple tags does not need to be aligned. See [3.8 Horizontal space]. In Kotlin, compared to Java, you can put several classes inside one file, so each class should have a Kdoc formatted comment (as stated in rule 2.1). This comment should contain @since tag. The right style is to write the application version when its functionality is released. It should be entered after the \textbf{@since} tag. \textbf{Examples:} \begin{lstlisting}[language=Kotlin] /** * Description of functionality * * @since 1.6 */ \end{lstlisting} Other KDoc tags (such as @param type parameters and @see.) can be added as follows: \begin{lstlisting}[language=Kotlin] /** * Description of functionality * * @apiNote: Important information about API * * @since 1.6 */ \end{lstlisting} \subsection*{\textbf{2.2 Adding comments on the file header}} \label{sec:2.2} This section describes the general rules of adding comments on the file header. \subsubsection*{\textbf{2.2.1 Formatting of comments in the file header}} \leavevmode\newline \label{sec:2.2.1} Comments on the file header should be placed before the package name and imports. If you need to add more content to the comment, subsequently add it in the same format. Comments on the file header must include copyright information, without the creation date and author's name (use VCS for history management). Also, describe the content inside files that contain multiple or no classes. The following examples for Huawei describe the format of the \textit{copyright license}: \ Chinese version: \textbf{版权所有 (c) 华为技术有限公司 2012-2020} \ English version: \textbf{Copyright (c) Huawei Technologies Co., Ltd. 2012-2020. All rights reserved.} \textbf{2012} and \textbf{2020} are the years the file was first created and the current year, respectively. Do not place \textbf{release notes} in header, use VCS to keep track of changes in file. Notable changes can be marked in individual KDocs using \textbf{@since} tag with version. Invalid example: \begin{lstlisting}[language=Kotlin] /** * Release notes: * 2019-10-11: added class Foo */ class Foo \end{lstlisting} Valid example: \begin{lstlisting}[language=Kotlin] /** * @since 2.4.0 */ class Foo \end{lstlisting} - The \textbf{copyright statement} can use your company's subsidiaries, as shown in the below examples: \ Chinese version: \textbf{版权所有 (c) 海思半导体 2012-2020} \ English version: \textbf{Copyright (c) Hisilicon Technologies Co., Ltd. 2012-2020. All rights reserved.} - The copyright information should not be written in KDoc style or use single-line comments. It must start from the beginning of the file. The following example is a copyright statement for Huawei, without other functional comments: \begin{lstlisting}[language=Kotlin] /* * Copyright (c) Huawei Technologies Co., Ltd. 2012-2020. All rights reserved. */ \end{lstlisting} The following factors should be considered when writing the file header or comments for top-level classes: - File header comments must start from the top of the file. If it is a top-level file comment, there should be a blank line after the last Kdoc \textbf{*/} symbol. If it is a comment for a top-level class, the class declaration should start immediately without using a newline. - Maintain a unified format. The specific format can be formulated by the project (for example, if you use an existing opensource project), and you need to follow it. - A top-level file-Kdoc must include a copyright and functional description, especially if there is more than one top-level class. - Do not include empty comment blocks. If there is no content after the option \textbf{@apiNote}, the entire tag block should be deleted. - The industry practice is not to include historical information in the comments. The corresponding history can be found in VCS (git, svn, etc.). Therefore, it is not recommended to include historical data in the comments of the Kotlin source code. \subsection*{\textbf{2.3 Comments on the function header}} \label{sec:2.3} Comments on the function header are placed above function declarations or definitions. A newline should not exist between a function declaration and its Kdoc. Use the preceding <> style rules. As stated in Chapter 1, the function name should reflect its functionality as much as possible. Therefore, in the Kdoc, try to describe the functionality that is not mentioned in the function name. Avoid unnecessary comments on dummy coding. The function header comment's content is optional, but not limited to function description, return value, performance constraints, usage, memory conventions, algorithm implementation, reentrant requirements, etc. \subsection*{\textbf{2.4 Code comments}} \label{sec:2.4} This section describes the general rules of adding code comments. \subsubsection*{\textbf{2.4.1 Add a blank line between the body of the comment and Kdoc tag-blocks.}} \leavevmode\newline \label{sec:2.4.1} It is a good practice to add a blank line between the body of the comment and Kdoc tag-blocks. Also, consider the following rules: - There must be one space between the comment character and the content of the comment. - There must be a newline between a Kdoc and the presiding code. - An empty line should not exist between a Kdoc and the code it is describing. You do not need to add a blank line before the first comment in a particular namespace (code block) (for example, between the function declaration and first comment in a function body). \textbf{Valid Examples:} \begin{lstlisting}[language=Kotlin] /** * This is the short overview comment for the example interface. * * @since 1.6 */ public interface Example { // Some comments /* Since it is the first member definition in this code block, there is no need to add a blank line here */ val aField: String = ... /* Add a blank line above the comment */ // Some comments val bField: String = ... /* Add a blank line above the comment */ /** * This is a long comment with whitespace that should be split in * multiple line comments in case the line comment formatting is enabled. * /* blank line between description and Kdoc tag */ * @param fox A quick brown fox jumps over the lazy dog * @return the rounds of battle of fox and dog */ fun foo(Fox fox) /* Add a blank line above the comment */ /** * These possibilities include: Formatting of header comments * * @return the rounds of battle of fox and dog * @throws ProblemException if lazy dog wins */ fun bar() throws ProblemException { // Some comments /* Since it is the first member definition in this range, there is no need to add a blank line here */ var aVar = ... // Some comments /* Add a blank line above the comment */ fun doSome() } } \end{lstlisting} - Leave one single space between the comment on the right side of the code and the code. If you use conditional comments in the \textbf{if-else-if} scenario, put the comments inside the \textbf{else-if} branch or in the conditional block, but not before the \textbf{else-if}. This makes the code more understandable. When the if-block is used with curly braces, the comment should be placed on the next line after opening the curly braces. Compared to Java, the \textbf{if} statement in Kotlin statements returns a value. For this reason, a comment block can describe a whole \textbf{if-statement}. \textbf{Valid examples:} \begin{lstlisting}[language=Kotlin] val foo = 100 // right-side comment val bar = 200 /* right-side comment */ // general comment for the value and whole if-else condition val someVal = if (nr % 15 == 0) { // when nr is a multiple of both 3 and 5 println("fizzbuzz") } else if (nr % 3 == 0) { // when nr is a multiple of 3, but not 5 // We print "fizz", only. println("fizz") } else if (nr % 5 == 0) { // when nr is a multiple of 5, but not 3 // we print "buzz" only. println("buzz") } else { // otherwise, we print the number. println(x) } \end{lstlisting} - Start all comments (including KDoc) with a space after the first symbol (\textbf{//}, \textbf{/*}, \textbf{/} and \textbf{*}) \textbf{Valid example:} \begin{lstlisting}[language=Kotlin] val x = 0 // this is a comment \end{lstlisting} \subsubsection*{\textbf{2.4.2 Do not comment on unused code blocks}} \leavevmode\newline \label{sec:2.4.2} Do not comment on unused code blocks, including imports. Delete these code blocks immediately. A code is not used to store history. Git, svn, or other VCS tools should be used for this purpose. Unused imports increase the coupling of the code and are not conducive to maintenance. The commented out code cannot be appropriately maintained. In an attempt to reuse the code, there is a high probability that you will introduce defects that are easily missed. The correct approach is to delete the unnecessary code directly and immediately when it is not used anymore. If you need the code again, consider porting or rewriting it as changes could have occurred since you first commented on the code. \subsubsection*{\textbf{2.4.3 Code delivered to the client should not contain TODO}} \leavevmode\newline \label{sec:2.4.3} The code officially delivered to the client typically should not contain TODO/FIXME comments. \textbf{TODO} comments are typically used to describe modification points that need to be improved and added. For example, refactoring FIXME comments are typically used to describe known defects and bugs that will be subsequently fixed and are not critical for an application. They should all have a unified style to facilitate unified text search processing. \textbf{Example:} \begin{lstlisting}[language=Kotlin] // TODO(): Jira-XXX - support new json format // FIXME: Jira-XXX - fix NPE in this code block \end{lstlisting} At a version development stage, these annotations can be used to highlight the issues in the code, but all of them should be fixed before a new product version is released. \section*{\textbf{3. General formatting}} \label{sec:3.} \subsection*{\textbf{3.1 File-related rules}} \label{sec:3.1} This section describes the rules related to using files in your code. \subsubsection*{\textbf{3.1.1 Avoid files that are too long}} \leavevmode\newline \label{sec:3.1.1} If the file is too long and complicated, it should be split into smaller files, functions, or modules. Files should not exceed 2000 lines (non-empty and non-commented lines). It is recommended to horizontally or vertically split the file according to responsibilities or hierarchy of its parts. The only exception to this rule is code generation - the auto-generated files that are not manually modified can be longer. \subsubsection*{\textbf{3.1.2 Code blocks in the source file should be separated by one blank line}} \leavevmode\newline \label{sec:3.1.2} A source file contains code blocks in the following order: copyright, package name, imports, and top-level classes. They should be separated by one blank line. a) Code blocks should be in the following order: 1. Kdoc for licensed or copyrighted files 2. \textbf{@file} annotation 3. Package name 4. Import statements 5. Top-class header and top-function header comments 6. Top-level classes or functions b) Each of the preceding code blocks should be separated by a blank line. c) Import statements are alphabetically arranged, without using line breaks and wildcards ( wildcard imports - \textbf{*}). d) \textbf{Recommendation}: One \textbf{.kt} source file should contain only one class declaration, and its name should match the filename e) Avoid empty files that do not contain the code or contain only imports/comments/package name f) Unused imports should be removed \subsubsection*{\textbf{3.1.3 Import statements order}} \leavevmode\newline \label{sec:3.1.3} From top to bottom, the order is the following: 1. Android 2. Imports of packages used internally in your organization 3. Imports from other non-core dependencies 4. Java core packages 5. kotlin stdlib Each category should be alphabetically arranged. Each group should be separated by a blank line. This style is compatible with \href{https://source.android.com/setup/contribute/code-style#order-import-statements}{Android import order}. \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] import android.* // android import androidx.* // android import com.android.* // android import com.your.company.* // your company's libs import your.company.* // your company's libs import com.fasterxml.jackson.databind.ObjectMapper // other third-party dependencies import org.junit.jupiter.api.Assertions import java.io.IOException // java core packages import java.net.URL import kotlin.system.exitProcess // kotlin standard library import kotlinx.coroutines.* // official kotlin extension library \end{lstlisting} \subsubsection*{\textbf{3.1.4 Order of declaration parts of class-like code structures}} \leavevmode\newline \label{sec:3.1.4} The declaration parts of class-like code structures (class, interface, etc.) should be in the following order: compile-time constants (for objects), class properties, late-init class properties, init-blocks, constructors, public methods, internal methods, protected methods, private methods, and companion object. Blank lines should separate their declaration. Notes: 1. There should be no blank lines between properties with the following \textbf{exceptions}: when there is a comment before a property on a separate line or annotations on a separate line. 2. Properties with comments/Kdoc should be separated by a newline before the comment/Kdoc. 3. Enum entries and constant properties (\textbf{const val}) in companion objects should be alphabetically arranged. The declaration part of a class or interface should be in the following order: - Compile-time constants (for objects) - Properties - Late-init class properties - Init-blocks - Constructors - Methods or nested classes. Put nested classes next to the code they are used by. If the classes are meant to be used externally, and are not referenced inside the class, put them after the companion object. - Companion object \textbf{Exception:} All variants of a \textbf{(private) val} logger should be placed at the beginning of the class (\textbf{(private) val log}, \textbf{LOG}, \textbf{logger}, etc.). \subsubsection*{\textbf{3.1.5 Order of declaration of top-level code structures}} \leavevmode\newline \label{sec:3.1.5} Kotlin allows several top-level declaration types: classes, objects, interfaces, properties and functions. When declaring more than one class or zero classes (e.g. only functions), as per rule [2.2.1], you should document the whole file in the header KDoc. When declaring top-level structures, keep the following order: 1. Top-level constants and properties (following same order as properties inside a class: \textbf{const val},\textbf{val}, \textbf{lateinit var}, \textbf{var}) 2. Interfaces, classes and objects (grouped by their visibility modifiers) 3. Extension functions 4. Other functions \textbf{Note}: Extension functions shouldn't have receivers declared in the same file according to [rule 6.2.3] Valid example: \begin{lstlisting}[language=Kotlin] package com.saveourtool.diktat.example const val CONSTANT = 42 val topLevelProperty = "String constant" interface IExample class Example : IExample private class Internal fun Other.asExample(): Example { /* ... */ } private fun Other.asInternal(): Internal { /* ... */ } fun doStuff() { /* ... */ } \end{lstlisting} \textbf{Note}: kotlin scripts (.kts) allow arbitrary code to be placed on the top level. When writing kotlin scripts, you should first declare all properties, classes and functions. Only then you should execute functions on top level. It is still recommended wrapping logic inside functions and avoid using top-level statements for function calls or wrapping blocks of code in top-level scope functions like \textbf{run}. Example: \begin{lstlisting}[language=Kotlin] /* class declarations */ /* function declarations */ run { // call functions here } \end{lstlisting} \subsection*{\textbf{3.2 Braces}} \label{sec:3.2} This section describes the general rules of using braces in your code. \subsubsection*{\textbf{3.2.1 Using braces in conditional statements and loop blocks}} \leavevmode\newline \label{sec:3.2.1} Braces should always be used in \textbf{if}, \textbf{else}, \textbf{for}, \textbf{do}, and \textbf{while} statements, even if the program body is empty or contains only one statement. In special Kotlin \textbf{when} statements, you do not need to use braces for single-line statements. \textbf{Valid example:} \begin{lstlisting}[language=Kotlin] when (node.elementType) { FILE -> { checkTopLevelDoc(node) checkSomething() } CLASS -> checkClassElements(node) } \end{lstlisting} \textbf{Exception:} The only exception is ternary operator in Kotlin (a single line \textbf{if () <> else <>} ) \textbf{Invalid example:} \begin{lstlisting}[language=Kotlin] val value = if (string.isEmpty()) // WRONG! 0 else 1 \end{lstlisting} \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] val value = if (string.isEmpty()) 0 else 1 // Okay \end{lstlisting} \begin{lstlisting}[language=Kotlin] if (condition) { println("test") } else { println(0) } \end{lstlisting} \subsubsection*{\textbf{3.2.2 Opening braces are placed at the end of the line in}} \leavevmode\newline \label{sec:3.2.2} For *non-empty* blocks and block structures, the opening brace is placed at the end of the line. Follow the K\&R style (1TBS or OTBS) for *non-empty* code blocks with braces: - The opening brace and first line of the code block are on the same line. - The closing brace is on its own new line. - The closing brace can be followed by a newline character. The only exceptions are \textbf{else}, \textbf{finally}, and \textbf{while} (from \textbf{do-while} statement), or \textbf{catch} keywords. These keywords should not be split from the closing brace by a newline character. \textbf{Exception cases}: 1) For lambdas, there is no need to put a newline character after the first (function-related) opening brace. A newline character should appear only after an arrow (\textbf{->}) (see [point 5 of Rule 3.6.2]). \begin{lstlisting}[language=Kotlin] arg.map { value -> foo(value) } \end{lstlisting} 2) for \textbf{else}/\textbf{catch}/\textbf{finally}/\textbf{while} (from \textbf{do-while} statement) keywords closing brace should stay on the same line: \begin{lstlisting}[language=Kotlin] do { if (true) { x++ } else { x-- } } while (x > 0) \end{lstlisting} \textbf{Valid example:} \begin{lstlisting}[language=Kotlin] return arg.map { value -> while (condition()) { method() } value } return MyClass() { @Override fun method() { if (condition()) { try { something() } catch (e: ProblemException) { recover() } } else if (otherCondition()) { somethingElse() } else { lastThing() } } } \end{lstlisting} \subsection*{\textbf{3.3 Indentation}} \label{sec:3.3} Only spaces are permitted for indentation, and each indentation should equal \textbf{four spaces} (tabs are not permitted). If you prefer using tabs, simply configure them to change to spaces in your IDE automatically. These code blocks should be indented if they are placed on the new line, and the following conditions are met: - The code block is placed immediately after an opening brace. - The code block is placed after each operator, including the assignment operator (\textbf{+}/\textbf{-}/\textbf{\&\&}/\textbf{=}/etc.) - The code block is a call chain of methods: \begin{lstlisting}[language=Kotlin] someObject .map() .filter() \end{lstlisting} - The code block is placed immediately after the opening parenthesis. - The code block is placed immediately after an arrow in lambda: \begin{lstlisting}[language=Kotlin] arg.map { value -> foo(value) } \end{lstlisting} \textbf{Exceptions}: 1. Argument lists: \ a) Eight spaces are used to indent argument lists (both in declarations and at call sites). \ b) Arguments in argument lists can be aligned if they are on different lines. 2. Eight spaces are used if there is a newline after any binary operator. 3. Eight spaces are used for functional-like styles when the newline is placed before the dot. 4. Supertype lists: \ a) Four spaces are used if the colon before the supertype list is on a new line. \ b) Four spaces are used before each supertype, and eight spaces are used if the colon is on a new line. \textbf{Note:} there should be an indentation after all statements such as \textbf{if}, \textbf{for}, etc. However, according to this code style, such statements require braces. \begin{lstlisting}[language=Kotlin] if (condition) foo() \end{lstlisting} \textbf{Exceptions}: - When breaking the parameter list of a method/class constructor, it can be aligned with \textbf{8 spaces}. A parameter that was moved to a new line can be on the same level as the previous argument: \begin{lstlisting}[language=Kotlin] fun visit( node: ASTNode, autoCorrect: Boolean, params: KtLint.ExperimentalParams, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit ) { } \end{lstlisting} - Such operators as \textbf{+}/\textbf{-}/\textbf{*} can be indented with \textbf{8 spaces}: \begin{lstlisting}[language=Kotlin] val abcdef = "my splitted" + " string" \end{lstlisting} - A list of supertypes should be indented with \textbf{4 spaces} if they are on different lines or with \textbf{8 spaces} if the leading colon is also on a separate line \begin{lstlisting}[language=Kotlin] class A : B() class A : B() \end{lstlisting} \subsection*{\textbf{3.4 Empty blocks}} \label{sec:3.4} Avoid empty blocks, and ensure braces start on a new line. An empty code block can be closed immediately on the same line and the next line. However, a newline is recommended between opening and closing braces \textbf{\{\}} (see the examples below.) Generally, empty code blocks are prohibited; using them is considered a bad practice (especially for catch block). They are only appropriate for overridden functions when the base class's functionality is not needed in the class-inheritor. \begin{lstlisting}[language=Kotlin] override fun foo() { } \end{lstlisting} \textbf{Valid examples} (note once again that generally empty blocks are prohibited): \begin{lstlisting}[language=Kotlin] fun doNothing() {} fun doNothingElse() { } \end{lstlisting} \textbf{Invalid examples:} \begin{lstlisting}[language=Kotlin] try { doSomething() } catch (e: Some) {} \end{lstlisting} Use the following valid code instead: \begin{lstlisting}[language=Kotlin] try { doSomething() } catch (e: Some) { } \end{lstlisting} \subsection*{\textbf{3.5 Line length}} \label{sec:3.5} Line length should be less than 120 symbols. The international code style prohibits \textbf{non-Latin} (\textbf{non-ASCII}) symbols. (See [Identifiers]) However, if you still intend on using them, follow the following convention: - One wide character occupies the width of two narrow characters. The "wide" and "narrow" parts of a character are defined by its \href{https://unicode.org/reports/tr11/}{east Asian width Unicode attribute}. Typically, narrow characters are also called "half-width" characters. All characters in the ASCII character set include letters (such as \textbf{a, A}), numbers (such as \textbf{0, 3}), and punctuation spaces (such as \textbf{,} , \textbf{\{}), all of which are narrow characters. Wide characters are also called "full-width" characters. Chinese characters (such as \textbf{中, 文}), Chinese punctuation (\textbf{,} , \textbf{;} ), full-width letters and numbers (such as \textbf{A、3}) are "full-width" characters. Each one of these characters represents two narrow characters. - Any line that exceeds this limit (\textbf{120 narrow symbols}) should be wrapped, as described in the [Newline section]. \textbf{Exceptions:} 1. The long URL or long JSON method reference in KDoc. 2. The \textbf{package} and \textbf{import} statements. 3. The command line in the comment, enabling it to be cut and pasted into the shell for use. \subsection*{\textbf{3.6 Line breaks}} \label{sec:3.6} This section contains the rules and recommendations on using line breaks. \subsubsection*{\textbf{3.6.1 Each line can have a maximum of one statement}} \leavevmode\newline \label{sec:3.6.1} Each line can have a maximum of one code statement. This recommendation prohibits the use of code with \textbf{;} because it decreases code visibility. \textbf{Invalid example:} \begin{lstlisting}[language=Kotlin] val a = ""; val b = "" \end{lstlisting} \textbf{Valid example:} \begin{lstlisting}[language=Kotlin] val a = "" val b = "" \end{lstlisting} \subsubsection*{\textbf{3.6.2 Rules for line-breaking}} \leavevmode\newline \label{sec:3.6.2} 1) Unlike Java, Kotlin allows you not to put a semicolon (\textbf{;}) after each statement separated by a newline character. There should be no redundant semicolon at the end of the lines. When a newline character is needed to split the line, it should be placed after such operators as \textbf{\&\&}/\textbf{||}/\textbf{+}/etc. and all infix functions (for example, \textbf{xor}). However, the newline character should be placed before operators such as \textbf{.}, \textbf{?.}, \textbf{?:}, and \textbf{::}. Note that all comparison operators, such as \textbf{==}, \textbf{>}, \textbf{<}, should not be split. \textbf{Invalid example}: \begin{lstlisting}[language=Kotlin] if (node != null && test != null) {} \end{lstlisting} \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] if (node != null && test != null) { } \end{lstlisting} \textbf{Note:} You need to follow the functional style, meaning each function call in a chain with \textbf{.} should start at a new line if the chain of functions contains more than one call: \begin{lstlisting}[language=Kotlin] val value = otherValue!! .map { x -> x } .filter { val a = true true } .size \end{lstlisting} \textbf{Note:} The parser prohibits the separation of the \textbf{!!} operator from the value it is checking. \textbf{Exception}: If a functional chain is used inside the branches of a ternary operator, it does not need to be split with newlines. \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] if (condition) list.map { foo(it) }.filter { bar(it) } else list.drop(1) \end{lstlisting} \textbf{Note:} If dot qualified expression is inside condition or passed as an argument - it should be replaced with new variable. \textbf{Invalid example}: \begin{lstlisting}[language=Kotlin] if (node.treeParent.treeParent.findChildByType(IDENTIFIER) != null) {} \end{lstlisting} \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] val grandIdentifier = node .treeParent .treeParent .findChildByType(IDENTIFIER) if (grandIdentifier != null) {} \end{lstlisting} 2) Newlines should be placed after the assignment operator (\textbf{=}). 3) In function or class declarations, the name of a function or constructor should not be split by a newline from the opening brace \textbf{(}. A brace should be placed immediately after the name without any spaces in declarations or at call sites. 4) Newlines should be placed right after the comma (\textbf{,}). 5) If a lambda statement contains more than one line in its body, a newline should be placed after an arrow if the lambda statement has explicit parameters. If it uses an implicit parameter (\textbf{it}), the newline character should be placed after the opening brace (\textbf{\{}). The following examples illustrate this rule: \textbf{Invalid example:} \begin{lstlisting}[language=Kotlin] value.map { name -> foo() bar() } \end{lstlisting} \textbf{Valid example:} \begin{lstlisting}[language=Kotlin] value.map { name -> foo() bar() } val someValue = { node:String -> node } \end{lstlisting} 6) When the function contains only a single expression, it can be written as \href{https://kotlinlang.org/docs/reference/functions.html#single-expression-functions}{expression function}. The below example shows the style that should not be used. Instead of: \begin{lstlisting}[language=Kotlin] override fun toString(): String { return "hi" } \end{lstlisting} use: \begin{lstlisting}[language=Kotlin] override fun toString() = "hi" \end{lstlisting} 7) If an argument list in a function declaration (including constructors) or function call contains more than two arguments, these arguments should be split by newlines in the following style. \textbf{Valid example:} \begin{lstlisting}[language=Kotlin] class Foo(val a: String, b: String, val c: String) { } fun foo( a: String, b: String, c: String ) { } \end{lstlisting} If and only if the first parameter is on the same line as an opening parenthesis, all parameters can be horizontally aligned by the first parameter. Otherwise, there should be a line break after an opening parenthesis. Kotlin 1.4 introduced a trailing comma as an optional feature, so it is generally recommended to place all parameters on a separate line and append \href{https://kotlinlang.org/docs/reference/whatsnew14.html#trailing-comma}{trailing comma}. It makes the resolving of merge conflicts easier. \textbf{Valid example:} \begin{lstlisting}[language=Kotlin] fun foo( a: String, b: String, ) { } \end{lstlisting} same should be done for function calls/constructor arguments/e.t.c Kotlin supports trailing commas in the following cases: Enumerations Value arguments Class properties and parameters Function value parameters Parameters with optional type (including setters) Indexing suffix Lambda parameters when entry Collection literals (in annotations) Type arguments Type parameters Destructuring declarations 8) If the supertype list has more than two elements, they should be separated by newlines. \textbf{Valid example:} \begin{lstlisting}[language=Kotlin] class MyFavouriteVeryLongClassHolder : MyLongHolder(), SomeOtherInterface, AndAnotherOne { } \end{lstlisting} \subsection*{\textbf{3.7 Using blank lines}} \label{sec:3.7} Reduce unnecessary blank lines and maintain a compact code size. By reducing unnecessary blank lines, you can display more code on one screen, which improves code readability. - Blank lines should separate content based on relevance and should be placed between groups of fields, constructors, methods, nested classes, \textbf{init} blocks, and objects (see [3.1.2]). - Do not use more than one line inside methods, type definitions, and initialization expressions. - Generally, do not use more than two consecutive blank lines in a row. - Do not put newlines in the beginning or end of code blocks with curly braces. \textbf{Valid example:} \begin{lstlisting}[language=Kotlin] fun baz() { doSomething() // No need to add blank lines at the beginning and end of the code block // ... } \end{lstlisting} \subsection*{\textbf{3.8 Horizontal space}} \label{sec:3.8} This section describes general rules and recommendations for using spaces in the code. \subsubsection*{\textbf{3.8.1: Usage of whitespace for code separation}} \leavevmode\newline \label{sec:3.8.1} Follow the recommendations below for using space to separate keywords: \textbf{Note:} These recommendations are for cases where symbols are located on the same line. However, in some cases, a line break could be used instead of a space. 1. Separate keywords (such as \textbf{if}, \textbf{when}, \textbf{for}) from the opening parenthesis with single whitespace. The only exception is the \textbf{constructor} keyword, which should not be separated from the opening parenthesis. 2. Separate keywords like \textbf{else} or \textbf{try} from the opening brace (\textbf{\{}) with single whitespace. If \textbf{else} is used in a ternary-style statement without braces, there should be a single space between \textbf{else} and the statement after: \textbf{if (condition) foo() else bar()} 3. Use a \textbf{single} whitespace before all opening braces (\textbf{\{}). The only exception is the passing of a lambda as a parameter inside parentheses: \begin{lstlisting}[language=Kotlin] private fun foo(a: (Int) -> Int, b: Int) {} foo({x: Int -> x}, 5) // no space before '{' \end{lstlisting} 4. Single whitespace should be placed on both sides of binary operators. This also applies to operator-like symbols. For example: - A colon in generic structures with the \textbf{where} keyword: \textbf{where T : Type} - Arrow in lambdas: \textbf{(str: String) -> str.length()} \textbf{Exceptions:} - Two colons (\textbf{::}) are written without spaces:\ \textbf{Object::toString} - The dot separator (\textbf{.}) that stays on the same line with an object name:\ \textbf{object.toString()} - Safe access modifiers \textbf{?.} and \textbf{!!} that stay on the same line with an object name:\ \textbf{object?.toString()} - Operator \textbf{..} for creating ranges:\ \textbf{1..100} 5. Use spaces after (\textbf{,}), (\textbf{:}), and (\textbf{;}), except when the symbol is at the end of the line. However, note that this code style prohibits the use of (\textbf{;}) in the middle of a line ([see 3.3.2]). There should be no whitespaces at the end of a line. The only scenario where there should be no space after a colon is when the colon is used in the annotation to specify a use-site target (for example, \textbf{@param:JsonProperty}). There should be no spaces before \textbf{,} , \textbf{:} and \textbf{;}. \textbf{Exceptions} for spaces and colons: - When \textbf{:} is used to separate a type and a supertype, including an anonymous object (after object keyword) - When delegating to a superclass constructor or different constructor of the same class \textbf{Valid example:} \begin{lstlisting}[language=Kotlin] abstract class Foo : IFoo { } class FooImpl : Foo() { constructor(x: String) : this(x) { /*...*/ } val x = object : IFoo { /*...*/ } } \end{lstlisting} 6. There should be \textit{only one space} between the identifier and its type: \textbf{list: List} If the type is nullable, there should be no space before \textbf{?}. 7. When using \textbf{[]} operator (\textbf{get/set}) there should be \textbf{no} spaces between identifier and \textbf{[} : \textbf{someList[0]}. 8. There should be no space between a method or constructor name (both at declaration and at call site) and a parenthesis: \textbf{foo() \{\}}. Note that this sub-rule is related only to spaces; the rules for whitespaces are described in [see 3.6.2]. This rule does not prohibit, for example, the following code: \begin{lstlisting}[language=Kotlin] fun foo ( a: String ) \end{lstlisting} 9. Never put a space after \textbf{(}, \textbf{[}, \textbf{<} (when used as a bracket in templates) or before \textbf{)}, \textbf{]}, \textbf{>} (when used as a bracket in templates). 10. There should be no spaces between a prefix/postfix operator (like \textbf{!!} or \textbf{++}) and its operand. \subsubsection*{\textbf{3.8.2: No spaces for horizontal alignment}} \leavevmode\newline \label{sec:3.8.2} \textit{Horizontal alignment} refers to aligning code blocks by adding space to the code. Horizontal alignment should not be used because: - When modifying code, it takes much time for new developers to format, support, and fix alignment issues. - Long identifier names will break the alignment and lead to less presentable code. - There are more disadvantages than advantages in alignment. To reduce maintenance costs, misalignment (???) is the best choice. Recommendation: Alignment only looks suitable for \textbf{enum class}, where it can be used in table format to improve code readability: \begin{lstlisting}[language=Kotlin] enum class Warnings(private val id: Int, private val canBeAutoCorrected: Boolean, private val warn: String) : Rule { PACKAGE_NAME_MISSING (1, true, "no package name declared in a file"), PACKAGE_NAME_INCORRECT_CASE (2, true, "package name should be completely in a lower case"), PACKAGE_NAME_INCORRECT_PREFIX(3, false, "package name should start from the company's domain") ; } \end{lstlisting} \textbf{Valid example:} \begin{lstlisting}[language=Kotlin] private val nr: Int // no alignment, but looks fine private var color: Color // no alignment \end{lstlisting} \textbf{Invalid example}: \begin{lstlisting}[language=Kotlin] private val nr: Int // aligned comment with extra spaces private val color: Color // alignment for a comment and alignment for identifier name \end{lstlisting} \subsection*{\textbf{3.9 Enumerations}} \label{sec:3.9} Enum values are separated by a comma and line break, with ';' placed on the new line. 1) The comma and line break characters separate enum values. Put \textbf{;} on the new line: \begin{lstlisting}[language=Kotlin] enum class Warnings { A, B, C, ; } \end{lstlisting} This will help to resolve conflicts and reduce the number of conflicts during merging pull requests. Also, use \href{https://kotlinlang.org/docs/reference/whatsnew14.html#trailing-comma}{trailing comma}. 2) If the enum is simple (no properties, methods, and comments inside), you can declare it in a single line: \begin{lstlisting}[language=Kotlin] enum class Suit { CLUBS, HEARTS, SPADES, DIAMONDS } \end{lstlisting} 3) Enum classes take preference (if it is possible to use it). For example, instead of two boolean properties: \begin{lstlisting}[language=Kotlin] val isCelsius = true val isFahrenheit = false \end{lstlisting} use enum class: \begin{lstlisting}[language=Kotlin] enum class TemperatureScale { CELSIUS, FAHRENHEIT } \end{lstlisting} - The variable value only changes within a fixed range and is defined with the enum type. - Avoid comparison with magic numbers of \textbf{-1, 0, and 1}; use enums instead. \begin{lstlisting}[language=Kotlin] enum class ComparisonResult { ORDERED_ASCENDING, ORDERED_SAME, ORDERED_DESCENDING, ; } \end{lstlisting} \subsection*{\textbf{3.10 Variable declaration}} \label{sec:3.10} This section describes rules for the declaration of variables. \subsubsection*{\textbf{3.10.1 Declare one variable per line}} \leavevmode\newline \label{sec:3.10.1} Each property or variable must be declared on a separate line. \textbf{Invalid example}: \begin{lstlisting}[language=Kotlin] val n1: Int; val n2: Int \end{lstlisting} \subsubsection*{\textbf{3.10.2 Variables should be declared near the line where they are first used}} \leavevmode\newline \label{sec:3.10.2} Declare local variables close to the point where they are first used to minimize their scope. This will also increase the readability of the code. Local variables are usually initialized during their declaration or immediately after. The member fields of the class should be declared collectively (see [Rule 3.1.2] for details on the class structure). \subsection*{\textbf{3.11 'When' expression}} \label{sec:3.11} The \textbf{when} statement must have an 'else' branch unless the condition variable is enumerated or a sealed type. Each \textbf{when} statement should contain an \textbf{else} statement group, even if it does not contain any code. \textbf{Exception:} If 'when' statement of the \textbf{enum or sealed} type contains all enum values, there is no need to have an "else" branch. The compiler can issue a warning when it is missing. \subsection*{\textbf{3.12 Annotations}} \label{sec:3.12} Each annotation applied to a class, method or constructor should be placed on its own line. Consider the following examples: 1. Annotations applied to the class, method or constructor are placed on separate lines (one annotation per line). \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] @MustBeDocumented @CustomAnnotation fun getNameIfPresent() { /* ... */ } \end{lstlisting} 2. A single annotation should be on the same line as the code it is annotating. \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] @CustomAnnotation class Foo {} \end{lstlisting} 3. Multiple annotations applied to a field or property can appear on the same line as the corresponding field. \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] @MustBeDocumented @CustomAnnotation val loader: DataLoader \end{lstlisting} \subsection*{\textbf{3.13 Block comments}} \label{sec:3.13} Block comments should be placed at the same indentation level as the surrounding code. See examples below. \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] class SomeClass { /* * This is * okay */ fun foo() {} } \end{lstlisting} \textbf{Note}: Use \textbf{/*...*/} block comments to enable automatic formatting by IDEs. \subsection*{\textbf{3.14 Modifiers and constant values}} \label{sec:3.14} This section contains recommendations regarding modifiers and constant values. \subsubsection*{\textbf{3.14.1 Declaration with multiple modifiers}} \leavevmode\newline \label{sec:3.14.1} If a declaration has multiple modifiers, always follow the proper sequence. \textbf{Valid sequence:} \begin{lstlisting}[language=Kotlin] public / internal / protected / private expect / actual final / open / abstract / sealed / const external override lateinit tailrec crossinline vararg suspend inner out enum / annotation companion inline / noinline reified infix operator data \end{lstlisting} \subsubsection*{\textbf{3.14.2: Separate long numerical values with an underscore}} \leavevmode\newline \label{sec:3.14.2} An underscore character should separate long numerical values. \textbf{Note:} Using underscores simplifies reading and helps to find errors in numeric constants. \begin{lstlisting}[language=Kotlin] val oneMillion = 1_000_000 val creditCardNumber = 1234_5678_9012_3456L val socialSecurityNumber = 999_99_9999L val hexBytes = 0xFF_EC_DE_5E val bytes = 0b11010010_01101001_10010100_10010010 \end{lstlisting} \subsection*{\textbf{3.15 Strings}} \label{sec:3.15} This section describes the general rules of using strings. \subsubsection*{\textbf{3.15.1 Concatenation of Strings}} \leavevmode\newline \label{sec:3.15.1} String concatenation is prohibited if the string can fit on one line. Use raw strings and string templates instead. Kotlin has significantly improved the use of Strings: \href{https://kotlinlang.org/docs/reference/basic-types.html#string-templates}{String templates}, \href{https://kotlinlang.org/docs/reference/basic-types.html#string-literals}{Raw strings}. Therefore, compared to using explicit concatenation, code looks much better when proper Kotlin strings are used for short lines, and you do not need to split them with newline characters. \textbf{Invalid example}: \begin{lstlisting}[language=Kotlin] val myStr = "Super string" val value = myStr + " concatenated" \end{lstlisting} \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] val myStr = "Super string" val value = "$myStr concatenated" \end{lstlisting} \subsubsection*{\textbf{3.15.2 String template format}} \leavevmode\newline \label{sec:3.15.2} \textbf{Redundant curly braces in string templates} If there is only one variable in a string template, there is no need to use such a template. Use this variable directly. \textbf{Invalid example}: \begin{lstlisting}[language=Kotlin] val someString = "${myArgument} ${myArgument.foo()}" \end{lstlisting} \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] val someString = "$myArgument ${myArgument.foo()}" \end{lstlisting} \textbf{Redundant string template} In case a string template contains only one variable - there is no need to use the string template. Use this variable directly. \textbf{Invalid example}: \begin{lstlisting}[language=Kotlin] val someString = "$myArgument" \end{lstlisting} \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] val someString = myArgument \end{lstlisting} \section*{\textbf{4. Variables and types}} \label{sec:4.} This section is dedicated to the rules and recommendations for using variables and types in your code. \subsection*{\textbf{4.1 Variables}} \label{sec:4.1} The rules of using variables are explained in the below topics. \subsubsection*{\textbf{4.1.1 Do not use Float and Double types when accurate calculations are needed}} \leavevmode\newline \label{sec:4.1.1} Floating-point numbers provide a good approximation over a wide range of values, but they cannot produce accurate results in some cases. Binary floating-point numbers are unsuitable for precise calculations because it is impossible to represent 0.1 or any other negative power of 10 in a \textbf{binary representation} with a finite length. The following code example seems to be obvious: \begin{lstlisting}[language=Kotlin] val myValue = 2.0 - 1.1 println(myValue) \end{lstlisting} However, it will print the following value: \textbf{0.8999999999999999} Therefore, for precise calculations (for example, in finance or exact sciences), using such types as \textbf{Int}, \textbf{Long}, \textbf{BigDecimal}are recommended. The \textbf{BigDecimal} type should serve as a good choice. \textbf{Invalid example}: Float values containing more than six or seven decimal numbers will be rounded. \begin{lstlisting}[language=Kotlin] val eFloat = 2.7182818284f // Float, will be rounded to 2.7182817 \end{lstlisting} \textbf{Valid example}: (when precise calculations are needed): \begin{lstlisting}[language=Kotlin] val income = BigDecimal("2.0") val expense = BigDecimal("1.1") println(income.subtract(expense)) // you will obtain 0.9 here \end{lstlisting} \subsubsection*{\textbf{4.1.2: Comparing numeric float type values}} \leavevmode\newline \label{sec:4.1.2} Numeric float type values should not be directly compared with the equality operator (==) or other methods, such as \textbf{compareTo()} and \textbf{equals()}. Since floating-point numbers involve precision problems in computer representation, it is better to use \textbf{BigDecimal} as recommended in [Rule 4.1.1] to make accurate computations and comparisons. The following code describes these problems. \textbf{Invalid example}: \begin{lstlisting}[language=Kotlin] val f1 = 1.0f - 0.9f val f2 = 0.9f - 0.8f if (f1 == f2) { println("Expected to enter here") } else { println("But this block will be reached") } val flt1 = f1; val flt2 = f2; if (flt1.equals(flt2)) { println("Expected to enter here") } else { println("But this block will be reached") } \end{lstlisting} \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] val foo = 1.03f val bar = 0.42f if (abs(foo - bar) > 1e-6f) { println("Ok") } else { println("Not") } \end{lstlisting} \subsubsection*{\textbf{4.1.3 Try to use 'val' instead of 'var' for variable declaration}} \leavevmode\newline \label{sec:4.1.3} Variables with the \textbf{val} modifier are immutable (read-only). Using \textbf{val} variables instead of \textbf{var} variables increases code robustness and readability. This is because \textbf{var} variables can be reassigned several times in the business logic. However, in some scenarios with loops or accumulators, only \textbf{var}s are permitted. \subsection*{\textbf{4.2 Types}} \label{sec:4.2} This section provides recommendations for using types. \subsubsection*{\textbf{4.2.1: Use Contracts and smart cast as much as possible}} \leavevmode\newline \label{sec:4.2.1} The Kotlin compiler has introduced \href{https://kotlinlang.org/docs/reference/typecasts.html#smart-casts}{Smart Casts} that help reduce the size of code. \textbf{Invalid example}: \begin{lstlisting}[language=Kotlin] if (x is String) { print((x as String).length) // x was already automatically cast to String - no need to use 'as' keyword here } \end{lstlisting} \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] if (x is String) { print(x.length) // x was already automatically cast to String - no need to use 'as' keyword here } \end{lstlisting} Also, Kotlin 1.3 introduced \href{https://kotlinlang.org/docs/reference/whatsnew13.html#contracts}{Contracts} that provide enhanced logic for smart-cast. Contracts are used and are very stable in \textbf{stdlib}, for example: \begin{lstlisting}[language=Kotlin] fun bar(x: String?) { if (!x.isNullOrEmpty()) { println("length of '$x' is ${x.length}") // smartcasted to not-null } } \end{lstlisting} Smart cast and contracts are a better choice because they reduce boilerplate code and features forced type conversion. \textbf{Invalid example}: \begin{lstlisting}[language=Kotlin] fun String?.isNotNull(): Boolean = this != null fun foo(s: String?) { if (s.isNotNull()) s!!.length // No smartcast here and !! operator is used } \end{lstlisting} \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] fun foo(s: String?) { if (s.isNotNull()) s.length // We have used a method with contract from stdlib that helped compiler to execute smart cast } \end{lstlisting} \subsubsection*{\textbf{4.2.2: Try to use type alias to represent types making code more readable}} \leavevmode\newline \label{sec:4.2.2} Type aliases provide alternative names for existing types. If the type name is too long, you can replace it with a shorter name, which helps to shorten long generic types. For example, code looks much more readable if you introduce a \textbf{typealias} instead of a long chain of nested generic types. We recommend using a \textbf{typealias} if the type contains \textbf{more than two} nested generic types and is longer than \textbf{25 chars}. \textbf{Invalid example}: \begin{lstlisting}[language=Kotlin] val b: MutableMap> \end{lstlisting} \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] typealias FileTable = MutableMap> val b: FileTable \end{lstlisting} You can also provide additional aliases for function (lambda-like) types: \begin{lstlisting}[language=Kotlin] typealias MyHandler = (Int, String, Any) -> Unit typealias Predicate = (T) -> Boolean \end{lstlisting} \subsection*{\textbf{4.3 Null safety and variable declarations}} \label{sec:4.3} Kotlin is declared as a null-safe programming language. However, to achieve compatibility with Java, it still supports nullable types. \subsubsection*{\textbf{4.3.1: Avoid declaring variables with nullable types}} \leavevmode\newline \label{sec:4.3.1} To avoid \textbf{NullPointerException} and help the compiler prevent Null Pointer Exceptions, avoid using nullable types (with \textbf{?} symbol). \textbf{Invalid example}: \begin{lstlisting}[language=Kotlin] val a: Int? = 0 \end{lstlisting} \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] val a: Int = 0 \end{lstlisting} Nevertheless, when using Java libraries extensively, you have to use nullable types and enrich the code with \textbf{!!} and \textbf{?} symbols. Avoid using nullable types for Kotlin stdlib (declared in \href{https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/}{official documentation}). Try to use initializers for empty collections. For example, if you want to initialize a list instead of \textbf{null}, use \textbf{emptyList()}. \textbf{Invalid example}: \begin{lstlisting}[language=Kotlin] val a: List? = null \end{lstlisting} \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] val a: List = emptyList() \end{lstlisting} \subsubsection*{\textbf{4.3.2: Variables of generic types should have an explicit type declaration}} \leavevmode\newline \label{sec:4.3.2} Like in Java, classes in Kotlin may have type parameters. To create an instance of such a class, we typically need to provide type arguments: \begin{lstlisting}[language=Kotlin] val myVariable: Map = emptyMap() \end{lstlisting} However, the compiler can inherit type parameters from the r-value (value assigned to a variable). Therefore, it will not force users to declare the type explicitly. These declarations are not recommended because programmers would need to find the return value and understand the variable type by looking at the method. \textbf{Invalid example}: \begin{lstlisting}[language=Kotlin] val myVariable = emptyMap() \end{lstlisting} \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] val myVariable: Map = emptyMap() \end{lstlisting} \subsubsection*{\textbf{4.3.3 Null-safety}} \leavevmode\newline \label{sec:4.3.3} Try to avoid explicit null checks (explicit comparison with \textbf{null}) Kotlin is declared as \href{https://kotlinlang.org/docs/reference/null-safety.html}{Null-safe} language. However, Kotlin architects wanted Kotlin to be fully compatible with Java; that's why the \textbf{null} keyword was also introduced in Kotlin. There are several code-structures that can be used in Kotlin to avoid null-checks. For example: \textbf{?:}, \textbf{.let \{\}}, \textbf{.also \{\}}, e.t.c \textbf{Invalid example:} \begin{lstlisting}[language=Kotlin] // example 1 var myVar: Int? = null if (myVar == null) { println("null") return } // example 2 if (myVar != null) { println("not null") return } // example 3 val anotherVal = if (myVar != null) { println("not null") 1 } else { 2 } // example 4 if (myVar == null) { println("null") } else { println("not null") } \end{lstlisting} \textbf{Valid example:} \begin{lstlisting}[language=Kotlin] // example 1 var myVar: Int? = null myVar?: run { println("null") return } // example 2 myVar?.let { println("not null") return } // example 3 val anotherVal = myVar?.also { println("not null") 1 } ?: 2 // example 4 myVar?.let { println("not null") } ?: run { println("null") } \end{lstlisting} \textbf{Exceptions:} In the case of complex expressions, such as multiple \textbf{else-if} structures or long conditional statements, there is common sense to use explicit comparison with \textbf{null}. \textbf{Valid examples:} \begin{lstlisting}[language=Kotlin] if (myVar != null) { println("not null") } else if (anotherCondition) { println("Other condition") } \end{lstlisting} \begin{lstlisting}[language=Kotlin] if (myVar == null || otherValue == 5 && isValid) {} \end{lstlisting} Please also note, that instead of using \textbf{require(a != null)} with a not null check - you should use a special Kotlin function called \textbf{requireNotNull(a)}. \section*{\textbf{5. Functions}} \label{sec:5.} This section describes the rules of using functions in your code. \subsection*{\textbf{5.1 Function design}} \label{sec:5.1} Developers can write clean code by gaining knowledge of how to build design patterns and avoid code smells. You should utilize this approach, along with functional style, when writing Kotlin code. The concepts behind functional style are as follows: Functions are the smallest unit of combinable and reusable code. They should have clean logic, \textbf{high cohesion}, and \textbf{low coupling} to organize the code effectively. The code in functions should be simple and not conceal the author's original intentions. Additionally, it should have a clean abstraction, and control statements should be used straightforwardly. The side effects (code that does not affect a function's return value but affects global/object instance variables) should not be used for state changes of an object. The only exceptions to this are state machines. Kotlin is \href{https://www.slideshare.net/abreslav/whos-more-functional-kotlin-groovy-scala-or-java}{designed} to support and encourage functional programming, featuring the corresponding built-in mechanisms. Also, it supports standard collections and sequences feature methods that enable functional programming (for example, \textbf{apply}, \textbf{with}, \textbf{let}, and \textbf{run}), Kotlin Higher-Order functions, function types, lambdas, and default function arguments. As [previously discussed], Kotlin supports and encourages the use of immutable types, which in turn motivates programmers to write pure functions that avoid side effects and have a corresponding output for specific input. The pipeline data flow for the pure function comprises a functional paradigm. It is easy to implement concurrent programming when you have chains of function calls, where each step features the following characteristics: 2. Verifiability 3. Testability 4. Replaceability 5. Pluggability 6. Extensibility 7. Immutable results There can be only one side effect in this data stream, which can be placed only at the end of the execution queue. \subsubsection*{\textbf{5.1.1 Avoid functions that are too long}} \leavevmode\newline \label{sec:5.1.1} The function should be displayable on one screen and only implement one certain logic. If a function is too long, it often means complex and could be split or simplified. Functions should consist of 30 lines (non-empty and non-comment) in total. \textbf{Exception:} Some functions that implement complex algorithms may exceed 30 lines due to aggregation and comprehensiveness. Linter warnings for such functions \textbf{can be suppressed}. Even if a long function works well, new problems or bugs may appear due to the function's complex logic once it is modified by someone else. Therefore, it is recommended to split such functions into several separate and shorter functions that are easier to manage. This approach will enable other programmers to read and modify the code properly. \subsubsection*{\textbf{5.1.2 Avoid deep nesting of function code blocks}} \leavevmode\newline \label{sec:5.1.2} The nesting depth of a function's code block is the depth of mutual inclusion between the code control blocks in the function (for example: if, for, while, and when). Each nesting level will increase the amount of effort needed to read the code because you need to remember the current "stack" (for example, entering conditional statements and loops). \textbf{Exception:} The nesting levels of the lambda expressions, local classes, and anonymous classes in functions are calculated based on the innermost function. The nesting levels of enclosing methods are not accumulated. Functional decomposition should be implemented to avoid confusion for the developer who reads the code. This will help the reader switch between contexts. \subsubsection*{\textbf{5.1.3 Avoid using nested functions}} \leavevmode\newline \label{sec:5.1.3} Nested functions create a more complex function context, thereby confusing readers. With nested functions, the visibility context may not be evident to the code reader. \textbf{Invalid example}: \begin{lstlisting}[language=Kotlin] fun foo() { fun nested():String { return "String from nested function" } println("Nested Output: ${nested()}") } \end{lstlisting} \subsubsection*{\textbf{5.1.4 Negated function calls}} \leavevmode\newline \label{sec:5.1.4} Don't use negated function calls if it can be replaced with negated version of this function \textbf{Invalid example}: \begin{lstlisting}[language=Kotlin] fun foo() { val list = listOf(1, 2, 3) if (!list.isEmpty()) { // Some cool logic } } \end{lstlisting} \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] fun foo() { val list = listOf(1, 2, 3) if (list.isNotEmpty()) { // Some cool logic } } \end{lstlisting} \subsection*{\textbf{5.2 Function arguments}} \label{sec:5.2} The rules for using function arguments are described in the below topics. \subsubsection*{\textbf{5.2.1 The lambda parameter of the function should be placed at the end of the argument list}} \leavevmode\newline \label{sec:5.2.1} With such notation, it is easier to use curly brackets, leading to better code readability. \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] // declaration fun myFoo(someArg: Int, myLambda: () -> Unit) { // ... } // usage myFoo(1) { println("hey") } \end{lstlisting} \subsubsection*{\textbf{5.2.2 Number of function parameters should be limited to five}} \leavevmode\newline \label{sec:5.2.2} A long argument list is a \href{https://en.wikipedia.org/wiki/Code\_smell}{code smell} that leads to less reliable code. It is recommended to reduce the number of parameters. Having \textbf{more than five} parameters leads to difficulties in maintenance and conflicts merging. If parameter groups appear in different functions multiple times, these parameters are closely related and can be encapsulated into a single Data Class. It is recommended that you use Data Classes and Maps to unify these function arguments. \subsubsection*{\textbf{5.2.3 Use default values for function arguments instead of overloading them}} \leavevmode\newline \label{sec:5.2.3} In Java, default values for function arguments are prohibited. That is why the function should be overloaded when you need to create a function with fewer arguments. In Kotlin, you can use default arguments instead. \textbf{Invalid example}: \begin{lstlisting}[language=Kotlin] private fun foo(arg: Int) { // ... } private fun foo() { // ... } \end{lstlisting} \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] private fun foo(arg: Int = 0) { // ... } \end{lstlisting} \subsubsection*{\textbf{5.2.4 Synchronizing code inside asynchronous code}} \leavevmode\newline \label{sec:5.2.4} Try to avoid using \textbf{runBlocking} in asynchronous code \textbf{Invalid example}: \begin{lstlisting}[language=Kotlin] GlobalScope.async { runBlocking { count++ } } \end{lstlisting} \subsubsection*{\textbf{5.2.5 Long lambdas should have explicit parameters}} \leavevmode\newline \label{sec:5.2.5} The lambda without parameters shouldn't be too long. If a lambda is too long, it can confuse the user. Lambda without parameters should consist of 10 lines (non-empty and non-comment) in total. \subsubsection*{\textbf{5.2.6 Avoid using unnecessary}} \leavevmode\newline \label{sec:5.2.6} Expressions with unnecessary, custom labels generally increase complexity and worsen the maintainability of the code. \textbf{Invalid example}: \begin{lstlisting}[language=Kotlin] run lab@ { list.forEach { return@lab } } \end{lstlisting} \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] list.forEachIndexed { index, i -> return@forEachIndexed } lab@ for(i: Int in q) { for (j: Int in q) { println(i) break@lab } } \end{lstlisting} \section*{\textbf{6. Classes}} \label{sec:6.} \subsection*{\textbf{6.1 Classes}} \label{sec:6.1} This section describes the rules of denoting classes in your code. \subsubsection*{\textbf{6.1.1 Denoting a class with a single constructor}} \leavevmode\newline \label{sec:6.1.1} When a class has a single constructor, it should be defined as a primary constructor in the declaration of the class. If the class contains only one explicit constructor, it should be converted to a primary constructor. \textbf{Invalid example}: \begin{lstlisting}[language=Kotlin] class Test { var a: Int constructor(a: Int) { this.a = a } } \end{lstlisting} \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] class Test(var a: Int) { // ... } // in case of any annotations or modifiers used on a constructor: class Test private constructor(var a: Int) { // ... } \end{lstlisting} \subsubsection*{\textbf{6.1.2 Prefer data classes instead of classes without any functional logic}} \leavevmode\newline \label{sec:6.1.2} Some people say that the data class is a code smell. However, if you need to use it (which makes your code more simple), you can utilize the Kotlin \textbf{data class}. The main purpose of this class is to hold data, but also \textbf{data class} will automatically generate several useful methods: - equals()/hashCode() pair; - toString() - componentN() functions corresponding to the properties in their order of declaration; - copy() function Therefore, instead of using \textbf{normal} classes: \begin{lstlisting}[language=Kotlin] class Test { var a: Int = 0 get() = field set(value: Int) { field = value} } class Test { var a: Int = 0 var b: Int = 0 constructor(a:Int, b: Int) { this.a = a this.b = b } } // or class Test(var a: Int = 0, var b: Int = 0) // or class Test() { var a: Int = 0 var b: Int = 0 } \end{lstlisting} \textbf{prefer data classes:} \begin{lstlisting}[language=Kotlin] data class Test1(var a: Int = 0, var b: Int = 0) \end{lstlisting} \textbf{Exception 1}: Note that data classes cannot be abstract, open, sealed, or inner; that is why these types of classes cannot be changed to a data class. \textbf{Exception 2}: No need to convert a class to a data class if this class extends some other class or implements an interface. \subsubsection*{\textbf{6.1.3 Do not use the primary constructor if it is empty or useless}} \leavevmode\newline \label{sec:6.1.3} The primary constructor is a part of the class header; it is placed after the class name and type parameters (optional) but can be omitted if it is not used. \textbf{Invalid example}: \begin{lstlisting}[language=Kotlin] // simple case that does not need a primary constructor class Test() { var a: Int = 0 var b: Int = 0 } // empty primary constructor is not needed here // it can be replaced with a primary contructor with one argument or removed class Test() { var a = "Property" init { println("some init") } constructor(a: String): this() { this.a = a } } \end{lstlisting} \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] // the good example here is a data class; this example also shows that you should get rid of braces for the primary constructor class Test { var a: Int = 0 var b: Int = 0 } \end{lstlisting} \subsubsection*{\textbf{6.1.4 Do not use redundant init blocks in your class}} \leavevmode\newline \label{sec:6.1.4} Several init blocks are redundant and generally should not be used in your class. The primary constructor cannot contain any code. That is why Kotlin has introduced \textbf{init} blocks. These blocks store the code to be run during the class initialization. Kotlin allows writing multiple initialization blocks executed in the same order as they appear in the class body. Even when you follow (rule 3.2)[\#r3.2], this makes your code less readable as the programmer needs to keep in mind all init blocks and trace the execution of the code. Therefore, you should try to use a single \textbf{init} block to reduce the code's complexity. If you need to do some logging or make some calculations before the class property assignment, you can use powerful functional programming. This will reduce the possibility of the error if your \textbf{init} blocks' order is accidentally changed and make the code logic more coupled. It is always enough to use one \textbf{init} block to implement your idea in Kotlin. \textbf{Invalid example}: \begin{lstlisting}[language=Kotlin] class YourClass(var name: String) { init { println("First initializer block that prints ${name}") } val property = "Property: ${name.length}".also(::println) init { println("Second initializer block that prints ${name.length}") } } \end{lstlisting} \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] class YourClass(var name: String) { init { println("First initializer block that prints ${name}") } val property = "Property: ${name.length}".also { prop -> println(prop) println("Second initializer block that prints ${name.length}") } } \end{lstlisting} The \textbf{init} block was not added to Kotlin to help you initialize your properties; it is needed for more complex tasks. Therefore if the \textbf{init} block contains only assignments of variables - move it directly to properties to be correctly initialized near the declaration. In some cases, this rule can be in clash with [6.1.1], but that should not stop you. \textbf{Invalid example}: \begin{lstlisting}[language=Kotlin] class A(baseUrl: String) { private val customUrl: String init { customUrl = "$baseUrl/myUrl" } } \end{lstlisting} \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] class A(baseUrl: String) { private val customUrl = "$baseUrl/myUrl" } \end{lstlisting} \subsubsection*{\textbf{6.1.5 Explicit supertype qualification}} \leavevmode\newline \label{sec:6.1.5} The explicit supertype qualification should not be used if there is no clash between called methods. This rule is applicable to both interfaces and classes. \textbf{Invalid example}: \begin{lstlisting}[language=Kotlin] open class Rectangle { open fun draw() { /* ... */ } } class Square() : Rectangle() { override fun draw() { super.draw() // no need in super here } } \end{lstlisting} \subsubsection*{\textbf{6.1.6 Abstract class should have at least one abstract method}} \leavevmode\newline \label{sec:6.1.6} Abstract classes are used to force a developer to implement some of its parts in their inheritors. When the abstract class has no abstract methods, it was set \textbf{abstract} incorrectly and can be converted to a regular class. \textbf{Invalid example}: \begin{lstlisting}[language=Kotlin] abstract class NotAbstract { fun foo() {} fun test() {} } \end{lstlisting} \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] abstract class NotAbstract { abstract fun foo() fun test() {} } // OR class NotAbstract { fun foo() {} fun test() {} } \end{lstlisting} \subsubsection*{\textbf{6.1.7 When using the}} \leavevmode\newline \label{sec:6.1.7} Kotlin has a mechanism of \href{https://kotlinlang.org/docs/reference/properties.html#backing-properties}{backing properties}. In some cases, implicit backing is not enough and it should be done explicitly: \begin{lstlisting}[language=Kotlin] private var _table: Map? = null val table: Map get() { if (_table == null) { _table = HashMap() // Type parameters are inferred } return _table ?: throw AssertionError("Set to null by another thread") } \end{lstlisting} In this case, the name of the backing property (\textbf{\_table}) should be the same as the name of the real property (\textbf{table}) but should have an underscore (\textbf{\_}) prefix. It is one of the exceptions from the [identifier names rule] \subsubsection*{\textbf{6.1.8 Avoid using custom getters and setters}} \leavevmode\newline \label{sec:6.1.8} Kotlin has a perfect mechanism of \href{https://kotlinlang.org/docs/reference/properties.html#properties-and-fields}{properties}. Kotlin compiler automatically generates \textbf{get} and \textbf{set} methods for properties and can override them. \textbf{Invalid example:} \begin{lstlisting}[language=Kotlin] class A { var size: Int = 0 set(value) { println("Side effect") field = value } // user of this class does not expect calling A.size receive size * 2 get() = field * 2 } \end{lstlisting} From the callee code, these methods look like access to this property: \textbf{A().isEmpty = true} for setter and \textbf{A().isEmpty} for getter. However, when \textbf{get} and \textbf{set} are overridden, it isn't very clear for a developer who uses this particular class. The developer expects to get the property value but receives some unknown value and some extra side-effect hidden by the custom getter/setter. Use extra functions instead to avoid confusion. \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] class A { var size: Int = 0 fun initSize(value: Int) { // some custom logic } // this will not confuse developer and he will get exactly what he expects fun goodNameThatDescribesThisGetter() = this.size * 2 } \end{lstlisting} \textbf{Exception:} \textbf{Private setters} are only exceptions that are not prohibited by this rule. \subsubsection*{\textbf{6.1.9 Never use the name of a variable in the custom getter or setter}} \leavevmode\newline \label{sec:6.1.9} If you ignored [recommendation 6.1.8], be careful with using the name of the property in your custom getter/setter as it can accidentally cause a recursive call and a \textbf{StackOverflow Error}. Use the \textbf{field} keyword instead. \textbf{Invalid example (very bad)}: \begin{lstlisting}[language=Kotlin] var isEmpty: Boolean set(value) { println("Side effect") isEmpty = value } get() = isEmpty \end{lstlisting} \subsubsection*{\textbf{6.1.10 No trivial getters and setters are allowed in the code}} \leavevmode\newline \label{sec:6.1.10} In Java, trivial getters - are the getters that are just returning the field value. Trivial setters - are merely setting the field with a value without any transformation. However, in Kotlin, trivial getters/setters are generated by default. There is no need to use it explicitly for all types of data structures in Kotlin. \textbf{Invalid example}: \begin{lstlisting}[language=Kotlin] class A { var a: Int = 0 get() = field set(value: Int) { field = value } // } \end{lstlisting} \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] class A { var a: Int = 0 get() = field set(value: Int) { field = value } // } \end{lstlisting} \subsubsection*{\textbf{6.1.11 Use 'apply' for grouping object initialization}} \leavevmode\newline \label{sec:6.1.11} In Java, before functional programming became popular, many classes from common libraries used the configuration paradigm. To use these classes, you had to create an object with the constructor with 0-2 arguments and set the fields needed to run the object. In Kotlin, to reduce the number of dummy code line and to group objects \href{https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/apply.html}{apply extension} was added: \textbf{Invalid example}: \begin{lstlisting}[language=Kotlin] class HttpClient(var name: String) { var url: String = "" var port: String = "" var timeout = 0 fun doRequest() {} } fun main() { val httpClient = HttpClient("myConnection") httpClient.url = "http://example.com" httpClient.port = "8080" httpClient.timeout = 100 httpCLient.doRequest() } \end{lstlisting} \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] class HttpClient(var name: String) { var url: String = "" var port: String = "" var timeout = 0 fun doRequest() {} } fun main() { val httpClient = HttpClient("myConnection") .apply { url = "http://example.com" port = "8080" timeout = 100 } httpClient.doRequest() } \end{lstlisting} \subsubsection*{\textbf{6.1.12 Prefer Inline classes when a class has a single property}} \leavevmode\newline \label{sec:6.1.12} If a class has only one immutable property, then it can be converted to the inline class. Sometimes it is necessary for business logic to create a wrapper around some type. However, it introduces runtime overhead due to additional heap allocations. Moreover, if the wrapped type is primitive, the performance hit is terrible, because primitive types are usually heavily optimized by the runtime, while their wrappers don't get any special treatment. \textbf{Invalid example}: \begin{lstlisting}[language=Kotlin] class Password { val value: String } \end{lstlisting} \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] inline class Password(val value: String) \end{lstlisting} \subsection*{\textbf{6.2 Extension functions}} \label{sec:6.2} This section describes the rules of using extension functions in your code. \href{https://kotlinlang.org/docs/reference/extensions.html}{Extension functions} is a killer-feature in Kotlin. It gives you a chance to extend classes that were already implemented in external libraries and helps you to make classes less heavy. Extension functions are resolved statically. \subsubsection*{\textbf{6.2.1 Use extension functions for making logic of classes less coupled}} \leavevmode\newline \label{sec:6.2.1} It is recommended that for classes, the non-tightly coupled functions, which are rarely used in the class, should be implemented as extension functions where possible. They should be implemented in the same class/file where they are used. This is a non-deterministic rule, so the code cannot be checked or fixed automatically by a static analyzer. \subsubsection*{\textbf{6.2.2 No extension functions with the same name and signature if they extend base and inheritor classes}} \leavevmode\newline \label{sec:6.2.2} You should have ho extension functions with the same name and signature if they extend base and inheritor classes (possible\_bug).esolved statically. There could be a situation when a developer implements two extension functions: one is for the base class and another for the inheritor. This can lead to an issue when an incorrect method is used. \textbf{Invalid example}: \begin{lstlisting}[language=Kotlin] open class A class B: A() // two extension functions with the same signature fun A.foo() = "A" fun B.foo() = "B" fun printClassName(s: A) { println(s.foo()) } // this call will run foo() method from the base class A, but // programmer can expect to run foo() from the class inheritor B fun main() { printClassName(B()) } \end{lstlisting} \subsubsection*{\textbf{6.2.3 Don't use extension functions for the class in the same file}} \leavevmode\newline \label{sec:6.2.3} You should not use extension functions for the class in the same file, where it is defined. \textbf{Invalid example}: \begin{lstlisting}[language=Kotlin] class SomeClass { } fun SomeClass.deleteAllSpaces() { } \end{lstlisting} \subsection*{\textbf{6.3 Interfaces}} \label{sec:6.3} An \textbf{Interface} in Kotlin can contain declarations of abstract methods, as well as method implementations. What makes them different from abstract classes is that interfaces cannot store state. They can have properties, but these need to be abstract or to provide accessor implementations. Kotlin's interfaces can define attributes and functions. In Kotlin and Java, the interface is the main presentation means of application programming interface (API) design and should take precedence over the use of (abstract) classes. \subsection*{\textbf{6.4 Objects}} \label{sec:6.4} This section describes the rules of using objects in code. \subsubsection*{\textbf{6.4.1 Instead of using utility classes}} \leavevmode\newline \label{sec:6.4.1} Avoid using utility classes/objects; use extensions instead. As described in [6.2 Extension functions], using extension functions is a powerful method. This enables you to avoid unnecessary complexity and class/object wrapping and use top-level functions instead. \textbf{Invalid example}: \begin{lstlisting}[language=Kotlin] object StringUtil { fun stringInfo(myString: String): Int { return myString.count{ "something".contains(it) } } } StringUtil.stringInfo("myStr") \end{lstlisting} \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] fun String.stringInfo(): Int { return this.count{ "something".contains(it) } } "myStr".stringInfo() \end{lstlisting} \subsubsection*{\textbf{6.4.2 Objects should be used for Stateless Interfaces}} \leavevmode\newline \label{sec:6.4.2} Kotlin’s objects are extremely useful when you need to implement some interface from an external library that does not have any state. There is no need to use classes for such structures. \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] interface I { fun foo() } object O: I { override fun foo() {} } \end{lstlisting} \subsubsection*{\textbf{6.5.1 kts files should wrap logic into top-level scope}} \leavevmode\newline \label{sec:6.5.1} It is still recommended wrapping logic inside functions and avoid using top-level statements for function calls or wrapping blocks of code in top-level scope functions like \textbf{run}. \textbf{Valid example}: \begin{lstlisting}[language=Kotlin] run { // some code } fun foo() { } \end{lstlisting} ================================================ FILE: wp/sections/compare.tex ================================================ \subsection{About ktlint} Ktlint is a popular an anti-bikeshedding Kotlin linter with a built-in formatter created by Pinterest\footnote{\url{https://github.com/pinterest/}}. It tries to reflect official code style from \texttt{kotlinlang.org} and Android Kotlin Style Guide and then automatically apply these rules to your codebase. Ktlint can check and automatically fix code. It claims to be simple and easy to use. As it is focused more on checking code-style and code-smell related issues, ktlint inspections work with Abstract Syntax Tree generated by Kotlin parser. Ktlint framework has some basic utilities to make the work with Kotlin AST easier, but anyway all inspections work with original ASTNode provided by Kotlin parser. Ktlint has been developed since 2016 and since then it has 3.8k stars, 309 forks and 390 closed PRs (2020). It looks to be the most popular and mature linter in the Kotlin community right now with approximately 15k lines of code written. Ktlint has its own set of rules, which are divided on standard and experimental rules. But unfortunately the number of fixers and checkers in the standard ruleset is very few (~20 rules) and inspections are trivial. Ktlint can be used as a plugin for Maven, Gradle or command line app. \texttt{.editorconfig} file should be modified to configure rules. This is the - only configuration that ktlint provides and it contains just simple configuration like the number of spaces in indents. Actually user even can’t configure specific rules (for example to disable or suppress any of them), instead you can provide some common settings like the number of spaces for indenting. In other words, ktlint has a "fixed hardcoded” code-style that is not very configurable. If you want to implement your own rules you need to create a your own Ruleset. Ktlint is very user-friendly for creation of custom Rulesets. In this case ktlint will parse the code using a Kotlin parser and will trigger your inspection (as visitor) for each node of AST. Ktlint uses javas \texttt{ServiceLoader} to discover all available Rulesets. \texttt{ServiceLoader} is used to inject your own implementation of rules for the static analysis. In this case ktlint becomes both a third-party dependency and a framework. Basically you should provide implementation of \texttt{RuleSetProvider} interface. Ktlint refers to article on Medium\footnote{\url{https://medium.com/mydevnotes/ktlint-improve-your-kotlin-code-quality-with-lint-checks-13a4456c4600}} on how to create a custom Ruleset and a Rule. A lot of projects uses ktlint as their code formatting tool. For example, OmiseGo \footnote{\url{https://github.com/omgnetwork/android-sdk}} (currently rebranding to OMG Network) - is a quite popular cryptocurrency. To summarize: Ktlint is very mature and useful as a framework for creating your own checker\&fixer of Kotlin code and doing AST-analysis. It can be very useful if you need only simple inspections that check (and fix) code-style issues (like indents). \begin{figure}[H] \centering \includegraphics[scale = 0.6]{pictures/ktlint.png} \caption{Ktlint Code Frequency} \label{fig:png_ktlint} \end{figure} \subsection{About detekt} Detekt \footnote{\url{https://github.com/detekt/detekt}} is a static code analysis tool. It operates on an abstract syntax tree (AST) and meta-information provided by Kotlin compiler. On the top of that info, it does a complex analysis of the code. However, this project is more focused on checking the code rather than fixing. Similarly, to ktlint, it has its own rules and inspections. Detekt uses wrapped ktlint to redefine RuleSet of ktlint as it’s formatting rules. Detekt supports detection of code smells, bugs searching and code-style checking. It has a highly configurable rule sets (can even make suppression of issues from the code). And the number of checkers is large: it has more than 100 inspections. Detekt has IntelliJ integration, third-party integrations for Maven, Bazel and Github actions and a mechanism for suppression of their warnings with @Suppress annotation from the code. It is being developed since 2016 and today it has 3.2k stars, 411 forks and 1850 closed PRs. It has about 45k lines of code. And its codebase is the biggest comparing to other analyzers. Detekt is used in such projects as fountain \footnote{\url{https://github.com/xmartlabs/fountain}} or Kaspresso \footnote{\url{https://github.com/KasperskyLab/Kaspresso}}. To summarize: Detekt is very useful as a Kotlin static analyser for CI/CD. It tries to find bugs in the code and is focused more on checking of the code. Detekt has 100+ rules which check the code. \begin{figure}[H] \centering \includegraphics[scale = 0.6]{pictures/detekt.png} \caption{Detekt Code Frequency} \label{fig:png_detekt} \end{figure} \subsection{About ktfmt} Ktfmt formats is a program that formats Kotlin code, based on google-java-format. Its development started in Facebook at the end of 2019. It can be added to client’s project through a Maven dependency, Gradle dependency, IntelliJ plugin or run through a command line. Ktfmt is not a configurable application, so to change any rule logic you need to download the project and redefine some constants. Ktfmt has 214 stars, 16 forks, 20 closed PRs and around 7500 lines of code. To summarize: no one knows why Facebook has invested their money in this tool. Nothing new was introduced. If they really needed to have new rules - they could create their own Ruleset for ktlint or detekt. \begin{figure}[H] \centering \includegraphics[scale = 0.6]{pictures/ktfmt.png} \caption{Ktfmt Code Frequency} \label{fig:png_ktfmt} \end{figure} \subsection{About diKTat} Diktat is a static code analysis tool as well as ktlint and detect. But diktat is not only a tool, but also a coding convention that describes in details all the rules that you should follow when writing a code on Kotlin. Its development has started in 2020 and at the time of writing this article diKTat has 168 stars and 13 forks. DiKTat operates on AST provided by kotlin compiler. So why diKTat is better? First of all, it supports much more rules than ktlint. Its ruleset includes more than 100 rules, that can both check and fix your code. Secondly, diKTat is configurable. A lot of rules have their own settings, and all of them can be easily understood. For example, you can choose whether you need a copyright, choose a length of line or you can configure your indents. Third, diKTat is very easy to configure. You don’t need to spend hours only to understand what each rule does. Diktat’s ruleset is a \texttt{.yml} file, where each rule is commented out with the description. Also you can suppress error on the particular lines of code using \texttt{@Suppress} annotation in your code. DiKTat can be used as a CI/CD tool in order to avoid merging errors in the code. Overall it can find code smells and code style issues. Also it can find pretty not obvious bugs by complex AST analysis. Diktat works with maven, gradle and as command-line application powered by ktlint. To summarize: diktat contains a strict coding convention that was not yet introduced by other linters. It works both as a checker and as a fixer. Diktat has much more inspections (100+) and is very configurable (each inspection can be disabled/configured separately), so you can configure it for your particular project. \begin{figure}[H] \centering \includegraphics[scale = 0.6]{pictures/diktat.png} \caption{DiKTat Code Frequency} \label{fig:png_diktat} \end{figure} \subsection{A few words about Jetbrains} Jetbrains invented Kotlin and created one of the best IDEs for Java and Kotlin called IntelliJ. This IDE supports a built-in linter. However, it is not a well-configurable tool, you are not able to specify your own coding convention and it is not useful for CI/CD as it is highly coupled with UI. Unfortunately such static analysis is not so effective as it cannot prevent merging of the code with bugs into the repository. As experience shows - many developers simply ignore those static analysis errors until they are blocked from merging their pull requests. So it is not so suitable for CI/CD, but very good for finding and fixing issues inside your IDE. \subsection{Summary} To sum up, four linters, excepting diKTat, were mentioned above and each of them has it's own strengths and weaknesses. Diktat, in its turn, is uniting its strengths and providing new features in code linting and fixing tools. \begin{center} \begin{tabular}{ |p{3cm}|p{2.5cm}|p{2.5cm}|p{2.5cm}|p{2.5cm}| } \hline \multicolumn{5}{|c|}{\textbf{Comparing table}} \\ \hline & diKTat& ktlint &detekt & ktfmt \\ \hline starting year & 2020 & 2016 & 2016 & 2019 \\ stars & 168 & 3.2k & 3.8k & 214\\ forks & 13 & 299 & 411 & 16\\ closed PRs & 321 & 390 & 1850 & 20 \\ lines of code & 32k & 15k & 45k & 7,5k\\ number of rules & $>$100 & $\approx$ 20 & $>$100 & $\approx$ 10 \\ is configurable & yes & no & yes/no & no \\ maven/gradle plugin & both & both & gradle only & no \\ web version & yes & yes & no & no \\ \hline \hline \end{tabular} \end{center} ================================================ FILE: wp/sections/conclusion.tex ================================================ \par DiKTat is a static code analyzer that finds and fixes code style inconsistencies. DiKTat is configurable, easy-to-use and it implements CI/CD pipelines, which distinguishes it from analogues. We offer many convenient ways to use diktat in projects, so you can use it as Maven/Gradle plugin, CLI tool, Web or github actions. It supports more than 100 rules, where each of them has clear explanation and can be configured by user. For diktat we have instroduced the coding convention for Kotlin code that now has 6 chapters and will be extended in the future. \par When the development of diKTat will be finished, we are going to support rules, update frameworks and track latest Kotlin releases to keep diKTat up to date. We are also planning to implement some number of Inspections that can detect real bugs in Kotlin code. ================================================ FILE: wp/sections/definition.tex ================================================ Before we will move one, it is necessary to define some terms for better understanding of context. The first and basic concept that should be introduced is \textbf{Rule} (marked with $R_i$). Rule in diKTat is the logic described in a special class named with \texttt{"Rule"} suffix, which checks whether code meets a certain paragraph of code-style. The set - is a well-defined collection of distinct objects, considered as an object in its own right. So we can define a \textbf{Ruleset} - a set of such code analysis Rules. We will mark any of such set of Rules with $R$. \textbf{Inspection} is the part of any Rule. It is an algorithm that can detect (marked with $W_i$) or fix (marked with $F_i$) invalid code. It is very important to understand that $Rule \neq Inspection (Inspection \subset Rule)$. We will use $I_i$ notation to mark each separate inspection. So it is obvious that: $I_i = W_i \cup F_i$, where $i \in \mathbb {N}$. Using the same logic we can say that $R = \bigcup\limits_{i} R_i$ where $R_i = \bigcup\limits_{j} I_j$. \textbf{Abstract syntax tree (AST)} is a tree representation of the abstract syntactic structure of source code written in a programming language (Kotlin in our case). Each node of the tree denotes a construct occurring in the source code. \textbf{CI/CD} - continuous integration (CI) and continuous delivery (CD) is a methodology that allows application development teams to make changes to code more frequently and reliably \cite{ref:cicd}. \textbf{KDoc} - is the language used to document Kotlin code (the equivalent of Java's JavaDoc). ================================================ FILE: wp/sections/diKTat.tex ================================================ \subsection{What is diKTat?} DiKTat \footnote{\url{https://github.com/saveourtool/diKTat}} - is a formal strict code-style for Kotlin language and a linter with a set of rules that implement this code-style. Basically, it is a collection of Kotlin code style rules implemented as AST visitors on top of KTlint framework \footnote{\url{https://github.com/pinterest/ktlint}}. Diktat detects and automatically fixes code style errors and code smells based on the configuration of rules. DiKTat is a highly configurable framework, that can be extended further by adding custom rules. It can be run as command line application or with maven or gradle plugins. In this paper, we will explain how diKTat works, describe its advantages and disadvantages and compare it with other static analyzers for Kotlin. The main idea is to use diktat in your CI/CD pipeline. \subsection{Why diKTat?} DiKTat permits formal flexible description or Rules and Inspections expressed by means of yml file. We looked at similar existing projects and realized that their functionality does not give us a chance to implement our own configurable code style. Most of rules which we wanted to implement were missing in other analyzers. Mostly all of those analyzers had hardcoded logic and prohibited configuration. That’s why we decided that we need to create convenient, user friendly and easily configured tool for developers. First of all, diKTat has its own highly configurable Ruleset $R_{diktat}$ that contains unique Inspections, missing in other Kotlin static analyzers. You just need to set your own options which fit your project the most. In case you don't want to do this - you can use the default configuration, but some of complex inspections will be disabled. Basically, Ruleset is an \texttt{yml} file with a description of each rule. Secondly, DiKTat has its own plugins and can be run via Maven, Gradle and command line. Developer can use build automation system that he prefers. Finally, developer can disable with diKTat each inspection from the code using special annotations on the line where he wants to suppress an Inspection. ================================================ FILE: wp/sections/download.tex ================================================ \subsection{CLI-application} You can run diKTat as a CLI-application. To do this you simply need to install ktlint/diktat and to run it via console with a special option \texttt{--disabled\_rules=standard} that we have introduced in ktlint \footnote{\url{https://github.com/pinterest/ktlint/pull/977/files}} : \begin{center} \texttt{\$ ./ktlint -R diktat.jar --disabled\_rules=standard "path/to/project/**/*.kt"} \end{center} After the run, all detected errors will displayed. Each warning contains of a rule name, a description of the rule and line/column where this error appears in the code. It also will contain \texttt{"cannot be auto-corrected"} note if the Inspection does not have autofixer. The format of warning is the following: \begin{center} /path/to/project/file.kt:6:5: [WARNING\_ID\_NAME] free text of the warning (cannot be auto-corrected) \end{center} Please also note, that as diktat is using ktlint framework - the format of the reported warnings can be changed: it can be xml, json and other formats that are supported by ktlint. Please refer to ktlint documentation \footnote{\url{https://github.com/pinterest/ktlint\#creating-a-reporter}} to see the information about custom reporters. \subsection{Maven plugin} Maven plugin was introduced for diktat since the version 0.1.3. The following code snippet from \texttt{pom.xml} shows how to use diktat with Maven plugin: \begin{lstlisting}[caption={DiKTat with Maven plugin}, label={lst:maven}, language=Kotlin] com.saveourtool.diktat diktat-maven-plugin ${diktat.version} diktat none check fix ${project.basedir}/src/main/kotlin ${project.basedir}/src/test/kotlin diktat-analysis.yml ${project.basedir}/src/test/kotlin/excluded \end{lstlisting} To run diktat in only-check mode use the command: \texttt{mvn diktat:check@diktat}. To run diktat in autocorrect mode use the command: \texttt{mvn diktat:fix@diktat}. \subsection{Gradle plugin} This plugin is available since version 0.1.5. The following code snippet shows how to configure Gradle plugin for diktat. \begin{lstlisting}[caption={DiKTat with Gradle plugin}, label={lst:gradle1}, language=Kotlin] plugins { id("com.saveourtool.diktat.diktat-gradle-plugin") version "0.1.7" } \end{lstlisting} Or use buildscript syntax: \begin{lstlisting}[caption={DiKTat with Gradle plugin}, label={lst:gradle1}, language=Kotlin] buildscript { repositories { mavenCentral() } dependencies { classpath("com.saveourtool.diktat:diktat-gradle-plugin:0.1.7") } } apply(plugin = "com.saveourtool.diktat.diktat-gradle-plugin") \end{lstlisting} Then you can configure diktat using diktat extension: \begin{lstlisting}[caption={DiKTat extension}, label={lst:gradle2}, language=Kotlin] diktat { inputs = files("src/**/*.kt") // file collection that will be checked by diktat debug = true // turn on debug logging excludes = files("src/test/kotlin/excluded") // these files will not be checked by diktat } \end{lstlisting} You can run diktat checks using task \texttt{diktatCheck} and automatically fix errors with tasks \texttt{diktatFix}. \subsection{Configuratifon file} As described above, diKTat has a configuration file. Note that you should place the \textsl{diktat-analysis.yml} file containing the diktat configuration in the parent directory of your project when running as a CLI application. Diktat-maven-plugin and diktat-gradle-plugin have a separate option to setup the path where the configuration file is stored. \subsection{DiKTat-web} The easiest way to use diktat without any downloads or installations is the web version of the app. You can try it by the following link: \url{https://ktlint-demo.herokuapp.com/demo}. Web app supports both checking and fixing, using either ktlint or diktat ruleset. For diktat you can also upload a custom configuration file. ================================================ FILE: wp/sections/feature.tex ================================================ \par As described above, diKTat is configurable and user-friendly. But these are not all of it's advantages and features. Below we will present and describe unusual and important killer-features of diKTat. \subsection{Configuration file} \par It's worth starting with the configuration file. This is a file in which the user can manually turn rules on and off or configure the rules settings. Below is one of the rules in the configuration file. \\ \begin{lstlisting}[ caption={Part of configuration file.}, label={lst:example1}, language=yaml] - name: DIKTAT_COMMON configuration: # put your package name here - it will be autofixed and checked domainName: your.name.here - name: COMMENTED_OUT_CODE enabled: true - name: HEADER_MISSING_OR_WRONG_COPYRIGHT enabled: true configuration: isCopyrightMandatory: true copyrightText: 'Copyright (c) Your Company Name Here. 2010-2020' testDirs: test - name: FILE_IS_TOO_LONG enabled: true configuration: maxSize: '2000' ignoreFolders: ' ' \end{lstlisting} Each Inspection in this file has 3 fields: \texttt{name} - the name of the Inspection, \texttt{enabled} - whether the rule is enabled or disabled (all rules are enabled by default), \texttt{configuration} - parameters for the Inspection. With the first two, everything is obvious. The third parameter is less obvious. The configuration is a set of "properties" to configure this rule. For example, for an Inspection \texttt{FILE\underline{ }IS\underline{ }TOO\underline{ }LONG}, that checks the number of lines in a Kotlin file, the user can configure the maximum number of lines allowed in the file - by changing the "maxSize" in the configuration, or the user can specify paths to folders that do not need to be checked - by writing the path in "ignoreFolders". \\ \subsection{Create ASTNode} Another feature is a special mechanism that allows you to construct an abstract syntax tree node from the text. It is extremely useful for creating automatic fixers, because you do not need to think about the AST implementation and you simply need to provide a text block with a code. Everything will be done under the hood by the framework. This algorithm can parse the code even partially, when you do not need to save the hierarchy of the file (with imports/packages/classes). For example it can parse and provide you a sub-tree for these lines of code: \begin{lstlisting}[caption={Example of creating an AST from text of code.}, label={lst:example1}, language=Kotlin] val nodeFromText: ASTNode = KotlinParser().createNode("val age: Int = 21") \end{lstlisting} \tikzstyle{every node}=[draw=black,thick,anchor=west, scale = 0.5] \begin{tikzpicture}[% grow via three points={one child at (0.3,-0.8) and two children at (0.3,-0.8) and (0.3,-1.5)}, scale=0.5, edge from parent path={(\tikzparentnode.south) |- (\tikzchildnode.west)}] \node {PROPERTY} child { node {val}} child {node {WHITE\underline{ }SPACE}} child {node {IDENTIFIER}} child {node {COLON}} child {node {WHITE\underline{ }SPACE}} child {node {TYPE\underline{ }REFERENCE} child {node {USER\underline{ }TYPE} child {node {REFERENCE\underline{ }EXPRESSION} child {node {IDENTIFIER}} } } } child [missing] {} child [missing] {} child [missing] {} child {node {WHITE\underline{ }SPACE}} child {node {EQ}} child {node {WHITE\underline{ }SPACE}} child {node {INTEGER\underline{ }CONSTANT} child {node {INTEGER\underline{ }LIRETAL}} }; \end{tikzpicture} \\ As you can see in the example, we pass the text of the source code, that we want to transform, to the method. What's going on inside this method? First of all, special system properties (used by Kotlin parser) are set (for example: set "idea.io.use.nio2" to true). If the text of the code contains high-level keywords like \texttt{import} or \texttt{package}, then the method builds a tree with a root node of the FILE type, otherwise it tries with a different root type. In both cases, at the end, if the tree contains a node with type \texttt{ERROR\underline{ }ELEMENT}, it means that some of the code and the method was unable to build the tree and, therefore, throws an exception.\\ This helps us to implement such complex inspections like the detection of commented code (and distinguish real comments from commented code blocks), helps easily fix the code without manually building sub-trees in visitors.\\ \subsection{Suppress annotation} \par What if the user wants one of the diKTat Inspections not to check a particular piece of code? The \textsl{SUPPRESS} annotation will help us with it. This annotation can be used to ignore a certain Inspection in a certain code block. For instance, if we run this code: \begin{lstlisting}[caption={Function with incorrect name.}, label={lst:example1}, language=Kotlin] /** * This is example */ package com.saveourtool.diktat /** * Simple class */ class User(private val name: String, private val age: Int) { /** * Function with incorrect name * * @return is username longer than age */ fun IsInCoRrEcTnAMe() = name.length > age } \end{lstlisting} Diktat will raise the warning: $$ \texttt{ \small{ $\big[$FUNCTION\underline{ }NAME\underline{ }INCORRECT\underline{ }CASE$\big]$ function/method name should be in lowerCamelCase}} $$ But if there is a \texttt{@Suppress} before this method, then there will be no warnings during the run: \begin{lstlisting}[caption={Function with incorrect name, but with suppressed Inspection.}, label={lst:example1}, language=Kotlin] /** * This is example */ package com.saveourtool.diktat /** * Simple class */ @Suppress("FUNCTION_NAME_INCORRECT_CASE") class User(private val name: String, private val age: Int) { /** * Function with incorrect name * * @return is username longer than age */ fun IsInCoRrEcTnAMe() = name.length > age } \end{lstlisting} The example shows that the method has a suppress annotation. Therefore, the \texttt{FUNCTION\underline{ }NAME\underline{ }INCORRECT\underline{ }CASE} rule will be ignored on this method and there will be no error. The search method for a given annotation goes up recursively to the root element of type \texttt{FILE}, looking for the annotation. This means that \texttt{@Suppress} can be placed not only in front of knowingly incorrect code, but also at the upper levels of the abstract syntax tree. In our example, the annotation is not in front of the method, but in front of the class and it still works. Also, you can put several annotations: \begin{lstlisting}[caption={Several suppression annotations}, label={lst:example1}, language=Kotlin] @set:[Suppress("WRONG_DECLARATION_ORDER") Suppress("IDENTIFIER_LENGTH")] \end{lstlisting} \subsection{WEB} It worth mentioning that there is a web version of diKTat. This is a handy tool that can be used quickly without any installations, and it is very simple. The link can be found in or you can find it in "\nameref{sec:download}" chapter or in ktlint project as reference.\footnote{\url{https://github.com/pinterest/ktlint\#online-demo}} \begin{figure}[H] \centering \includegraphics[scale=0.3]{pictures/web-example.png} \caption{Diktat-web online demo} \end{figure} \subsection{Options validation} As it has been mention earlier, diktat has a highly customizable configuration file, but manually editing it is error-prone, for example, name of the rule can be incorrect due to a typo. Diktat will validate configuration file on startup and suggest the closest name based on \textsl{Levenshtein} method. \subsection{Self checks} Diktat fully supports self checking of it's own code using both release and snapshot (master) versions. Diktat uses it's self to validate the code inside during it's CI/CD process. ================================================ FILE: wp/sections/introduction.tex ================================================ It is necessary to follow a specific style of code during software development. Otherwise code will become less readable and the developer will not be able to understand other programmers’ intent. It can lead to functional defects and bugs in the code. Static analyzers, in it's turn, have methods for detecting and auto-correcting style errors and bugs. Modern linters and static analyzers are extremely useful not only for simple code-style analysis but also for bugs detection and automatic code fixing. There are many methods and techniques used by existing analyzers to find bugs (path-sensitive data flow analysis \cite{ref:kremenek}, alias analysis \cite{ref:effective}, type analysis \cite{ref:simple}, symbolic execution \cite{ref:dis}, abstract interpretation \cite{ref:dis}). Senior developer can write the same comment again and again in hundreds of code reviews. Static analysis reduces this bureaucracy as it can be thought of as an automated code review process, because it can detect those issues in code automatically. And of course it perfectly reduces the human factor in the review process. There are two main tasks that can be solved by static code analysis: identifying errors (bugs) in programs and recommending code formatting (fixes). This means that the analyzer allows you, for example, to check whether the source code complies with the accepted coding convention and automatically fix found issues. Also, a static analyzer can be used to determine the level of maintainability of a code. It shows how easy is it to read, modify and adapt a given code of software by detecting code-smells and design patterns used in the code. Static analysis tools allow you to identify a large number of errors in the design phase, which significantly reduces the development cost of the entire project. Static analysis covers the entire code - it checks even those code fragments that are difficult to test. It does not depend on the compiler used and the environment in which the compiled program will be executed. This white-paper covers the work that was done to create a static analyzer for Kotlin language, called diKTat. It also briefly describes it's implementation and functionality. You can treat this document as a "how-to" instruction for diKTat. ================================================ FILE: wp/sections/kotlin.tex ================================================ This section explains why we are focused exactly on the static analysis for Kotlin language. Kotlin is a cross-platform, statically typed, general-purpose programming language with type inference. Kotlin is designed to interoperate fully with Java, and the JVM version of Kotlin's standard library depends on the Java Class Library, but type inference allows its syntax to be more concise. This language can be called "Java on steroids" as it takes best features from different languages and puts it on the top of JVM-world. Kotlin mainly targets the JVM, but also is compiled to JavaScript (e.g. for frontend web applications using React or Thymeleaf) or native code (via LLVM), e.g. for native iOS apps sharing business logic with Android apps. % wikipedia Kotlin has quickly skyrocketed in popularity. It's used by Google, Square, Pinterest, Pivotal, Netflix, Atlassian and many other companies. It's the fastest-growing programming language, according to GitHub, growing over 2,5 times in the past year (2019). It was voted one of the five most loved languages, according to Stack Overflow. There are even meetups and conferences focused only on Kotlin\footnote{\url{https://www.businessinsider.com/kotlin-programming-language-explained-popularity-2019-5\#:\~:text=Kotlin\%20has\%20quickly\%20skyrocketed\%20in,times\%20in\%20the\%20past\%20year}}. % https://www.businessinsider.com/kotlin-programming-language-explained-popularity-2019-5#:~:text=Kotlin%20has%20quickly%20skyrocketed%20in,times%20in%20the%20past%20year Kotlin is used in a lot of ways. For example, it can be used for backend development using \texttt{ktor} framework (Kotlin framework developed by JetBrains), and \texttt{Spring} framework that also has first-party support for kotlin (\texttt{Spring} is one of the most popular framework on Java for Web development). Kotlin/JS provides the ability to transpile your Kotlin code to JavaScript, as well as providing JS variant of Kotlin standard library and interopability with existing JS dependencies, both for Node.js and browser. There are numerous ways how Kotlin/JS can be used. For instance, Kotlin/JS is used to create frontend web applications, server-side and serverless applications, libraries for use with JavaScript and TypeScript. Support for multiplatform programming is one of key benefits. It reduces time for writing and maintaining the same code for different platforms while retaining the flexibility and benefits of native programming. We think that it is the main reason why Kotlin is so popular in the community of mobile developers. Kotlin supports well asynchronous or non-blocking programming. Whether we're creating server-side, desktop or mobile applications, it's important that we provide an experience that is not only fluid from the user's perspective, but scalable when needed. Kotlin has chosen a very flexible approach one by providing Coroutine support as a first-party library \texttt{kotlinx.coroutines} with a kotlin compiler plugin. A coroutine is a concurrent design pattern that you can use on Android to simplify code that executes asynchronously. Coroutines (in a form of kotlinx.coroutines library and kotlin compiler plugin) were added to Kotlin in version 1.3 and are based on established concepts from other languages. Also, coroutines do not only open the doors to an easy asynchronous programming in Kotlin, but also provide a wealth of other possibilities such as concurrency, actors, etc. On Android, coroutines help to manage long-running tasks that might otherwise block the main thread and cause your app to become unresponsive. Over 50\% of professional developers who use coroutines have reported that productivity had increased. So designers of Kotlin made a correct decision. Coroutines help you to write cleaner and more concise code for your applications. The state of Kotlin in the Q3 of 2020 (according to the latest Kotlin Census and statistical data): \begin{itemize} \item 4,7 million users \item 65\% of users use Kotlin in production \item Kotlin is primary language for 56\% of users, which means the main or only one they use at work \item 100+ people are on the Kotlin development team at JetBrains \item 350+ independent contributors develop the language and its ecosystem outside of JetBrains \end{itemize} % https://techcrunch.com/2019/05/07/kotlin-is-now-googles-preferred-language-for-android-app-development/ Kotlin is used by developers of open-source operating systems like HarmonyOS and Android. In 2019 Google announced that the Kotlin programming language is now its preferred language for Android app developers. In the same year Stack Overflow stated that Kotlin is fourth most loved language in community. Nowadays there are over 60\% of android developers who use Kotlin as their main language. \footnote{\url{https://techcrunch.com/2019/05/07/kotlin-is-now-googles-preferred-language-for-android-app-development/}} Kotlin's popularity can be explained by the rising number of Android users (last year, 124.4m in the USA) and, thus, Android-based devices. 80\% of Kotlin programmers use the language to build Android apps, 31\% for back-end applications, 30\% for SDK/libraries. Kotlin is also interoperable with Java, which allows developers to use all existing Android libraries in a Kotlin app. Now (2020) Kotlin is in the top-10 of PYPL rating: \begin{figure}[H] \centering \includegraphics[scale = 0.3]{pictures/kotlinRating.png} \caption{Top programming languages, 2020} \label{fig:top_languages} \end{figure} Overall, Kotlin is a modern language that gains its popularity incredibly fast. It is mostly used by Android developers, but other "branches of programming" are gaining popularity as well, for example Spring framework (the most popular Java framework) supports Kotlin. It supports both OO (object-oriented) and FP (function-oriented) programming paradigms. Since the release 1.4 Kotlin claims to bring major updates every 6 month. ================================================ FILE: wp/sections/work.tex ================================================ Diktat does AST-analysis, using Abstract Syntax Tree for creation of internal representation (IR) from the parsed code by the kotlin-compiler. This chapter describes how diktat works. \subsection{ktlint} To quickly and efficiently analyze the program code, you first need to transform it into a convenient data structure. This is exactly what ktlint does - it parses plain text code into an abstract syntax tree. So we decided not to choose the way of development that was chosen by Facebook in ktfmt and not invent our own framework for parsing the code. We decided to write our own Ruleset on the top of ktlint framework. The good thing is that we were able to inject our set of rules to ktlint via Java's \texttt{ServiceLoader}\footnote{\url{https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html}} In ktlint, the transformation of code happens in the \textsl{prepareCodeForLinting}\footnote{\url{https://github.com/pinterest/ktlint/blob/master/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/KtLint.kt}} method. This method uses kotlin-compiler-embeddable library to create a root node of type FILE. For example, this simple code: \begin{lstlisting}[caption={Simple function that will be transformed to AST}, label={lst:example1}, language=Kotlin] fun main() { println("Hello World") } \end{lstlisting} will be converted into the following AST: \tikzstyle{every node}=[draw=black,thick,anchor=west, scale = 0.6] \begin{tikzpicture}[% grow via three points={one child at (0.3,-0.8) and two children at (0.3,-0.8) and (0.3,-1.5)}, scale=0.6, edge from parent path={(\tikzparentnode.south) |- (\tikzchildnode.west)}] \node {FILE} child { node {PACKAGE\underline{ }DIRECTIVE}} child { node {IMPORT\underline{ }LIST}} child { node {FUN} child {node {fun}} child {node {WHITE\underline{ }SPACE}} child {node {IDENTIFIER}} child {node {VALUE\underline{ }PARAMETER\underline{ }LIST} child {node {LPAR}} child {node {RPAR}} } child [missing] {} child [missing] {} child {node {WHITE\underline{ }SPACE}} child {node {BLOCK} child {node {LBRACE}} child {node {WHITE\underline{ }SPACE}} child {node {CALL\underline{ }EXPRESSION} child {node {REFERENCE\underline{ }EXPRESSION} child {node {IDENTIFIER}} } child [missing] {} child {node {VALUE\underline{ }ARGUMENT\underline{}LIST} child {node {LPAR}} child {node {VALUE\underline{ }ARGUMENT} child {node {STRING\underline{ }TEMPLATE} child {node {OPEN\underline{ }QUOTE}} child {node {LITERAL\underline{ }STRING\underline{ }TEMPLATE\underline{ }ENTRY} child {node {REGULAR\underline{ }STRING\underline{ }PART}} } child [missing] {} child {node {CLOSING\underline{ }QUOTE}} } } child [missing] {} child [missing] {} child [missing] {} child [missing] {} child [missing] {} child [missing] {} child {node {RPAR}} } } child [missing] {} child [missing] {} child [missing] {} child [missing] {} child [missing] {} child [missing] {} child [missing] {} child [missing] {} child [missing] {} child [missing] {} child [missing] {} child [missing] {} child [missing] {} child {node {WHITE\underline{ }SPACE}} child {node {RBRACE}} } }; \end{tikzpicture} \\ If there are error elements inside the constructed tree, then the corresponding error is displayed. If the code is valid and parsed without errors, for each rule in the Ruleset, the \textsl{visit} method is called for the root node itself and its “children” are sequentially passed. When you run program, you can pass flags to ktlint - one of them is \texttt{-F}. This flag means that the rule will not only report an error, but will also try to fix it. \subsection{DiKTat} Another feature of ktlint is that at it's startup you can provide a JAR file with additional ruleset(s), that will be discovered by the \texttt{ServiceLoader} and then all AST nodes will be passed to these rules. DiKTat uses this approach. The only modification Diktat makes to the framework is that it adds a mechanism to disable Inspection from the code using annotations or configuration file. The set of all rules is described in the \textsl{DiktatRuleSetProvider}\footnote{\url{https://github.com/saveourtool/diKTat/blob/v0.1.3/diktat-rules/src/main/kotlin/com/saveourtool/diktat/ruleset/rules/DiktatRuleSetProvider.kt}} class. This class overrides the \textsl{get()} method of the \textsl{RuleSetProvider}\footnote{\url{https://github.com/pinterest/ktlint/blob/master/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/RuleSetProvider.kt}} interface, which returns a set of rules to be "traversed". But before returning this set Diktat is reading the configuration file where the user has independently configured all the Inspections. If there is no configuration file, then a warning will be displayed and Inspections will use the default configuration file. Each rule must implement the \textsl{visit} method of the abstract Rule class, which describes the logic of the rule. By the way it is a special case of a famous pattern Visitor \cite{ref:gang}. Implementation example of the simple Rule that contains one Inspections can be found below. \begin{lstlisting}[caption={Example of the Rule.}, label={lst:example1}, language=Kotlin] class SingleLineStatementsRule(private val configRules: List) : Rule("statement") { private var isFixMode: Boolean = false private lateinit var emitWarn: EmitType override fun visit(node: ASTNode, autoCorrect: Boolean, emit: EmitType) { emitWarn = emit isFixMode = autoCorrect // all the work is done with ASTNode - this is the type, provided by Kotlin compiler node.getChildren(TokenSet.create(SEMICOLON)).forEach { if (!it.isFollowedByNewline()) { // configuration is checked by warning mechanism under the hood // warnings are mapped to proper paragraph of a code standard MORE_THAN_ONE_STATEMENT_PER_LINE.warnAndFix( configRules, emitWarn, isFixMode, it.extractLineOfText(), it.startOffset, it // this lambda provides the logic that will be used to fix the code ) { if (it.treeParent.elementType == ENUM_ENTRY) { node.treeParent.addChild(PsiWhiteSpaceImpl("\n"), node.treeNext) } else { if (!it.isBeginByNewline()) { val nextNode = it.parent( { parent -> parent.treeNext != null }, strict = false )?.treeNext node.appendNewlineMergingWhiteSpace(nextNode, it) } node.removeChild(it) } } } } } } \end{lstlisting} The example above describes the Rule that checks that there are no statements separated by a semicolon. The list of configurations is passed to the parameter of this rule so that the error is displayed only when the Rule is enabled (further it will be described how to enable or disable the Rule). The class fields and the \textsl{visit()} method are described below. The first parameter in method is of \textsl{ASTNode} type, this type is produced by the Kotlin compiler. It is important to understand that these visitors are called for each and every node of AST that is provided by the compiler. This is not optimal from the perspective of the performance, but makes the code much more readable and isolated. Then a check occurs: if the code contains a line in which more than one statement per line and this rule is enabled, then the rule will be executed and, depending on the mode in which the user started ktlint, the rule will either simply report an error or fix it. In our case, when an error is found, the method is called to report and fix the error - \textsl{warnAndFix()}. All warnings that contain similar logic (e.g. regarding formatting of function KDocs) are checked in the same Rule. This way we can make small optimisation and check similar parts of AST only once. Whether the Inspection is enabled or disabled is checked inside \textsl{warn()} or \textsl{warnAndFix()} methods. The same works for the suppression of Inspections: right before emitting the warning we check whether any of current node's parents has a Suppress annotation. The diagram below describes it's architecture: \begin{figure}[H] \centering \includegraphics[scale=0.5]{pictures/class.PNG} \caption{Diktat class diagram} \label{fig:top_languages} \end{figure} \subsection{Examples of unique inspections} \par As already described above, DiKTat has more rules than existing analogues, therefore, it will find and fix more errors and shortcomings and, thereby, make the code cleaner and better. To better understand how detailed are checks in Diktat,let's mention a few examples: \begin{enumerate} \item \textbf{Package.} In DiKTat, there are about 6 Inspections that are checking the package naming. For comparison: detekt has only one rule, where the package name is simply checked by a pattern, in ktlint there are zero inspections. \item \textbf{KDoc.} KDoc is an important part of good code to make it easier to understand and navigate the program. In DiKTat there are 15 rules on KDoc, in detekt there are only 7. Therefore, DiKTat will make and correct KDoc in more detail and correctly. \end{enumerate} There are also many unique Inspections that other analyzers do not have. Here are some of them: \begin{enumerate} \item \textbf{COMMENTED\underline{ }OUT\underline{ }CODE} – This Inspection checks if the code contains commented code blocks. \item \textbf{FILE\underline{ }CONTAINS\underline{ }ONLY\underline{ }COMMENTS} – This rule checks file contains not only comments. \item \textbf{LOCAL\underline{ }VARIABLE\underline{ }EARLY\underline{ }DECLARATION} – This rule checks that local variables are declared close to the point where they are first used. \item \textbf{AVOID\underline{ }NESTED\underline{ }FUNCTIONS} - This rule checks for nested functions and warns and fixes if it finds any. An example of changing the tree when this rule is triggered and DiKTat is run with fix mode:\\\\ \begin{tikzpicture}[% grow via three points={one child at (0.3,-0.8) and two children at (0.3,-0.8) and (0.3,-1.5)}, scale=0.5, edge from parent path={(\tikzparentnode.south) |- (\tikzchildnode.west)}] \node(a) {...} child { node {FUN} child {node {...}} child {node {BLOCK} child {node {FUN} child{node{...}} child{node{BLOCK} child{node{...}} } } } }; \node(b) [right of=a, xshift=17cm]{...} child { node {FUN} child {node {...}} child {node {BLOCK} child{node{...}} } } child [missing] {} child [missing] {} child { node {FUN} child {node {...}} child {node {BLOCK} child{node{...}} } }; \draw[-latex,very thick,shorten <=5mm,shorten >=5mm,] ([xshift=5cm,yshift=-3cm]a.north) to ([xshift=-2cm, yshift=-3cm]b.north); \end{tikzpicture} \\ \item \textbf{FLOAT\underline{ }IN\underline{ }ACCURATE\underline{ }CALCULATIONS} - Inspection that checks that floating-point numbers are not used for accurate calculations (see the corresponding Rule from the code style to get more information). \item \textbf{PACKAGE\underline{ }NAMING} - This Inspection checks that package name is in a proper format and is separated by dots. This inspection is very demonstrative to show how the work with AST is done. This inspection receives different nodes and checks them one by one. First it checks their element type (type of the node in AST). When it's element type equals to \texttt{PACKAGE\_DIRECTIVE} it gets the file name and collects all nodes with \texttt{IDENTIFIER} type as it is shown on the following graph: \begin{center} \begin{tikzpicture}[nodes={draw, circle}, sibling distance=1.5cm, level distance = 1.2cm, minimum size=3.1cm, scale = 1.2, node distance=16cm] \node(main)[text width = 2cm] { PACKAGE DIRECTIVE} child { node(a) [below left] { PACKAGE} } child { node(b)[below] { WHITE SPACE} } child { node(c)[below right, text width=3cm] { DOT QUALIFIED EXPRESSION} child { node(d)[below left, text width=2.5cm] { REFERENCE EXPRESSION} child { node(e)[below, fill = selectColor] { IDENTIFIER} } } child { node(f)[below] { DOT} } child { node(g)[below right, text width=2.5cm] { REFERENCE EXPRESSION} child { node(h)[below, fill = selectColor] { IDENTIFIER} } } }; \draw[->, line width = 0.5mm] (main) -- (a); \draw[->, line width = 0.5mm] (main) --(b); \draw[->, line width = 0.5mm] (main) -- (c); \draw[->, line width = 0.5mm] (c) -- (d); \draw[->, line width = 0.5mm] (d) -- (e); \draw[->, line width = 0.5mm] (c) -- (f); \draw[->, line width = 0.5mm] (c) -- (g); \draw[->, line width = 0.5mm] (g) --(h); \end{tikzpicture} \end{center} In order to collect elements with a proper type, Inspection has to do a tree traversal. Tree traversal is done by a special method called \texttt{findAllNodesWithCondition()} (see Listing 3). This function searches for a node with a given condition (in this case it is when node's type equals to \texttt{IDENTIFIER}). As a basis it uses DFS (Depth-first search): it goes recursively in depth of the tree and compares types of AST nodes. When it finds necessary node it returns it and the result from it's parent search as a list, otherwise it returns empty list. At the end the Inspection checks the package name based on identifiers and file name. \begin{lstlisting}[caption={Method for AST traversal}, label={lst:example1}, language=Kotlin] /** * This method performs tree traversal and returns all nodes which satisfy the condition */ fun ASTNode.findAllNodesWithCondition(condition: (ASTNode) -> Boolean, withSelf: Boolean = true): List { val result = if (condition(this) && withSelf) mutableListOf(this) else mutableListOf() return result + this.getChildren(null).flatMap { it.findAllNodesWithCondition(condition) } } \end{lstlisting} \tikzstyle{every node}=[draw=black,thick,anchor=west, scale = 0.5, font = \large] \end{enumerate} ================================================ FILE: wp/wp.tex ================================================ \documentclass[acmlarge, screen, nonacm]{acmart} \usepackage[utf8]{inputenc} \usepackage{CJKutf8} \usepackage{graphicx} \usepackage{pgf} \usepackage{multicol} \usepackage{setspace} \usepackage{listings} \usepackage{hyperref} \usepackage{float} \usepackage{placeins} \graphicspath{ {./images/} } \usepackage{tikz} \usepackage{verbatim} \usepackage{cleveref} \usepackage{datetime} \usepackage{longtable} \crefname{figure}{Figure}{Figures} \usetikzlibrary{trees} \usetikzlibrary{arrows} \definecolor{codegreen}{rgb}{0,0.6,0} \definecolor{codegray}{rgb}{0.5,0.5,0.5} \definecolor{codepurple}{rgb}{0.58,0,0.82} \definecolor{backcolour}{rgb}{0.95,0.95,0.92} \definecolor{OrangeRed}{rgb}{1,0.27,0} \definecolor{ForestGreen}{rgb}{0.1334,0.545,0.1334} \definecolor{NavyBlue}{rgb}{0,0.502,0} \definecolor{BurntOrange}{rgb}{0.8,0.3334,0.1334} \definecolor{selectColor}{rgb}{0.686,1,0.592} \newcommand\YAMLcolonstyle{\color{black}\mdseries} \newcommand\YAMLkeystyle{\color{BurntOrange}\bfseries} \newcommand\YAMLvaluestyle{\color{black}\mdseries} \lstdefinelanguage{yaml} { keywords={true,false,null,y,n}, keywordstyle=\color{darkgray}\bfseries, basicstyle=\YAMLkeystyle, sensitive=false, comment=[l]{\#}, morecomment=[s]{/*}{*/}, commentstyle=\color{ForestGreen}\ttfamily, stringstyle=\YAMLvaluestyle\ttfamily, moredelim=[l][\color{orange}]{\&}, moredelim=[l][\color{magenta}]{*}, moredelim=**[il][\YAMLcolonstyle{:}\YAMLvaluestyle]{:}, morestring=[b]', morestring=[b]", literate = {---}{{\ProcessThreeDashes}}3 {>}{{\textcolor{red}\textgreater}}1 {|}{{\textcolor{red}\textbar}}1 {\ -\ }{{\mdseries\ -\ }}3, } \lstdefinestyle{mystyle}{ backgroundcolor=\color{backcolour}, commentstyle=\color{codegreen}, keywordstyle=\color{magenta}, numberstyle=\tiny\color{codegray}, stringstyle=\color{codepurple}, basicstyle=\sffamily\footnotesize, breakatwhitespace=false, breaklines=true, captionpos=b, keepspaces=true, numbers=left, numbersep=5pt, showspaces=false, showstringspaces=false, showtabs=false, tabsize=2 } \lstdefinelanguage{Kotlin}{ comment=[l]{//}, commentstyle={\color{gray}\ttfamily}, emph={delegate, filter, first, firstOrNull, forEach, lazy, map, mapNotNull, println, return@}, emphstyle={\color{OrangeRed}}, identifierstyle=\color{black}, keywords={abstract, actual, as, as?, break, by, class, companion, continue, data, do, dynamic, else, enum, expect, false, final, for, fun, get, if, import, in, interface, internal, is, object, override, package, private, public, return, set, super, suspend, throw, true, try, typealias, val, var, vararg, when, where, while}, keywordstyle={\color{NavyBlue}\bfseries}, morecomment=[s]{/*}{*/}, morestring=[b]", morestring=[s]{"""*}{*"""}, ndkeywords={@Deprecated, @JvmField, @JvmName, @JvmOverloads, @JvmStatic, @JvmSynthetic, Array, Byte, Double, Float, this, null, Int, Integer, Iterable, Long, Runnable, Short, String, Boolean}, ndkeywordstyle={\color{BurntOrange}\bfseries}, sensitive=true, stringstyle={\color{ForestGreen}\ttfamily}, } \lstset{style=mystyle} \settopmatter{printacmref=false} \settopmatter{printfolios=true} \setcopyright{none} \makeatletter \let\@authorsaddresses\@empty \makeatletter \let\newdate\@date \settopmatter{printacmref=false} \setcopyright{none} \renewcommand\footnotetextcopyrightpermission[1]{} \pagestyle{plain} \begin{document} \begin{CJK*}{UTF8}{gbsn} \title[]{ \includegraphics[width = 150pt, height = 150]{pictures/logo.png}\\ DiKTat - Kotlin linter } \author{Andrey Kuleshov} \email{andrewkuleshov7@gmail.com} \affiliation{ \institution{Lomonosov Moscow State University} \country{Russia} } \author{Petr Trifanov} \email{peter.trifanov@mail.ru} \affiliation{ \institution{Lomonosov Moscow State University} \country{Russia} } \author{Denis Kumar} \email{qbextted0@gmail.com} \affiliation{ \institution{Higher School of Economics} \country{Russia} } \author{Alexander Tsay} \email{aktsay6@gmail.com} \affiliation{ \institution{Higher School of Economics} \country{Russia} } \renewcommand{\shortauthors}{} \maketitle \section{Introduction} \label{sec:intro} \input{sections/introduction} \section{Definition} \label{sec:definition} \input{sections/definition} \section{Kotlin} \label{sec:kotlin} \input{sections/kotlin} \section{diKTat} \label{sec:diKTat} \input{sections/diKTat} \section{Comparative analysis} \label{sec:compare} \input{sections/compare} \section{How does diktat work} \label{sec:work} \input{sections/work} \section{Killer-Features} \label{sec:feature} \input{sections/feature} \section{How to use diKTat} \label{sec:download} \input{sections/download} \section{Conclusion \& Future Work} \label{sec:conclusion} \input{sections/conclusion} \newpage \nocite{*} \bibliographystyle{ieeetr} \bibliography{references.bib} \newpage \section{Appendix} \label{sec:appendix} \input{sections/appendix} \end{CJK*} \end{document}