From 67d93df46b1b66932ab456e1c49925f255298269 Mon Sep 17 00:00:00 2001 From: Menny Even Danan Date: Thu, 15 Oct 2026 18:52:38 +0000 Subject: [PATCH] Replace ktlint with Detekt using custom aspect_rules_lint aspect Introduce custom Detekt aspect in tools/lint/detekt.bzl using aspect_rules_lint. Fetch io.gitlab.arturbosch.detekt:detekt-cli via rules_jvm_external maven dependency and run via java_binary. Wire Detekt into linters.bzl and .bazelrc, producing both terminal output and SARIF reports. Clean up ktlint configuration and baseline. --- .bazelrc | 2 +- .editorconfig | 1 - BUILD.bazel | 2 +- MODULE.bazel | 7 +- MODULE.bazel.lock | 25 - .../calque/linguistics/CognateDetector.kt | 34 +- detekt.yml | 797 ++++++++++++++++++ ktlint-baseline.xml | 3 - tools/lint/BUILD.bazel | 8 + tools/lint/detekt.bzl | 158 ++++ tools/lint/linters.bzl | 12 +- 11 files changed, 990 insertions(+), 59 deletions(-) create mode 100644 detekt.yml delete mode 100644 ktlint-baseline.xml create mode 100644 tools/lint/detekt.bzl diff --git a/.bazelrc b/.bazelrc index 447bf15..d14e870 100644 --- a/.bazelrc +++ b/.bazelrc @@ -7,6 +7,6 @@ common --color=yes test --test_output=errors # Lint configuration -build:lint --aspects=//tools/lint:linters.bzl%ktlint,//tools/lint:linters.bzl%buildifier +build:lint --aspects=//tools/lint:linters.bzl%detekt,//tools/lint:linters.bzl%buildifier build:lint --output_groups=rules_lint_human build:lint --@aspect_rules_lint//lint:fail_on_violation diff --git a/.editorconfig b/.editorconfig index f68d576..f1f636d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,7 +11,6 @@ trim_trailing_whitespace = true [*.{kt,kts}] indent_size = 4 max_line_length = 120 -ktlint_code_style = intellij_idea ij_kotlin_allow_trailing_comma = true ij_kotlin_allow_trailing_comma_on_call_site = true diff --git a/BUILD.bazel b/BUILD.bazel index ab694f5..a6afc26 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -2,7 +2,7 @@ package(default_visibility = ["//visibility:public"]) exports_files([ ".editorconfig", - "ktlint-baseline.xml", + "detekt.yml", ]) alias( diff --git a/MODULE.bazel b/MODULE.bazel index de1af20..8d67376 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -14,6 +14,7 @@ bazel_dep(name = "buildifier_prebuilt", version = "10.1.0", dev_dependency = Tru maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven") maven.install( artifacts = [ + "io.gitlab.arturbosch.detekt:detekt-cli:1.23.8", "junit:junit:4.13.2", "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0", "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0", @@ -27,11 +28,7 @@ maven.install( ) use_repo(maven, "maven") -# Lint & Format tools +# Formatting tools format_tools = use_extension("@aspect_rules_lint//format:extensions.bzl", "tools") format_tools.ktfmt() use_repo(format_tools, "ktfmt") - -lint_tools = use_extension("@aspect_rules_lint//lint:extensions.bzl", "tools") -lint_tools.ktlint() -use_repo(lint_tools, "com_github_pinterest_ktlint") diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 91bccc1..c1ea66a 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -327,31 +327,6 @@ }, "selectedYankedVersions": {}, "moduleExtensions": { - "@@aspect_rules_lint+//lint:extensions.bzl%tools": { - "general": { - "bzlTransitiveDigest": "k+PotYT27HSYOmBun4tKiWAAs8wB3PCA7RYFEy4+X5s=", - "usagesDigest": "opYHLwQPC9jQE7ozr+3snzwNPRn/iDnmUsL7UL0UTlY=", - "recordedInputs": [ - "REPO_MAPPING:aspect_rules_lint+,aspect_rules_lint aspect_rules_lint+", - "REPO_MAPPING:aspect_rules_lint+,aspect_tools_telemetry_report aspect_tools_telemetry++telemetry+aspect_tools_telemetry_report", - "REPO_MAPPING:aspect_rules_lint+,bazel_features bazel_features+", - "REPO_MAPPING:aspect_rules_lint+,bazel_lib bazel_lib+", - "REPO_MAPPING:aspect_rules_lint+,bazel_tools bazel_tools", - "REPO_MAPPING:bazel_features+,bazel_features_globals bazel_features++version_extension+bazel_features_globals", - "REPO_MAPPING:bazel_features+,bazel_features_version bazel_features++version_extension+bazel_features_version" - ], - "generatedRepoSpecs": { - "com_github_pinterest_ktlint": { - "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_file", - "attributes": { - "sha256": "2e28cf46c27d38076bf63beeba0bdef6a845688d6c5dccd26505ce876094eb92", - "url": "https://github.com/pinterest/ktlint/releases/download/1.2.1/ktlint", - "executable": true - } - } - } - } - }, "@@aspect_tools_telemetry+//:extension.bzl%telemetry": { "general": { "bzlTransitiveDigest": "XVKR7Fz0TpsBTBIjxfUrpsRKdh9a5XZHQEJmfRGJOD8=", diff --git a/core/linguistics/src/main/kotlin/com/anysoftkeyboard/calque/linguistics/CognateDetector.kt b/core/linguistics/src/main/kotlin/com/anysoftkeyboard/calque/linguistics/CognateDetector.kt index 733d821..2720940 100644 --- a/core/linguistics/src/main/kotlin/com/anysoftkeyboard/calque/linguistics/CognateDetector.kt +++ b/core/linguistics/src/main/kotlin/com/anysoftkeyboard/calque/linguistics/CognateDetector.kt @@ -12,6 +12,12 @@ import kotlin.math.min * Levenshtein distance on word stems. */ object CognateDetector { + private const val MIN_WORD_LENGTH = 2 + private const val MIN_EXACT_MATCH_LENGTH = 3 + private const val MIN_STEM_LENGTH = 2 + private const val MIN_FUZZY_STEM_LENGTH = 3 + private const val MAX_FUZZY_DISTANCE = 1 + /** Suffix conversion pairs between Spanish and English. */ private val SUFFIX_MAP: List> = listOf( @@ -60,7 +66,7 @@ object CognateDetector { */ fun detectCognate(spanishWord: String, englishGloss: String): Boolean { val es = normalize(spanishWord) - if (es.length <= 2) return false + if (es.length <= MIN_WORD_LENGTH) return false val candidates = extractCandidateWords(englishGloss) if (candidates.isEmpty()) return false @@ -73,38 +79,32 @@ object CognateDetector { val es = normalize(spanishWord) val en = normalize(englishWord) - if (es.length <= 2 || en.length <= 2) return false + if (es.length <= MIN_WORD_LENGTH || en.length <= MIN_WORD_LENGTH) return false // Direct identical match for words > 3 characters (e.g. "natural" == "natural") - if (es.length > 3 && es == en) { + if (es.length > MIN_EXACT_MATCH_LENGTH && es == en) { return true } // Direct match after accent normalization (e.g. "café" -> "cafe") val esUnaccented = stripAccents(es) - if (esUnaccented.length > 3 && esUnaccented == en) { + if (esUnaccented.length > MIN_EXACT_MATCH_LENGTH && esUnaccented == en) { return true } // Suffix mapping and stem comparison - for ((esSuffix, enSuffix) in SUFFIX_MAP) { + return SUFFIX_MAP.any { (esSuffix, enSuffix) -> if (es.endsWith(esSuffix) && en.endsWith(enSuffix)) { val esStem = stripAccents(es.removeSuffix(esSuffix)) val enStem = en.removeSuffix(enSuffix) - // Exact match on short stems (e.g. "nación" -> "na" == "nation" -> "na") - if (esStem == enStem && esStem.length >= 2) { - return true - } - - // Levenshtein distance <= 1 for stems of length >= 3 - if (esStem.length >= 3 && levenshteinDistance(esStem, enStem) <= 1) { - return true - } + (esStem == enStem && esStem.length >= MIN_STEM_LENGTH) || + (esStem.length >= MIN_FUZZY_STEM_LENGTH && + levenshteinDistance(esStem, enStem) <= MAX_FUZZY_DISTANCE) + } else { + false } } - - return false } /** Normalizes a word by lowercasing and trimming whitespace and non-letter edge characters. */ @@ -138,7 +138,7 @@ object CognateDetector { val words = WORD_TOKEN_REGEX.findAll(withoutParticle).map { it.value } for (word in words) { val normalized = normalize(word) - if (normalized.length >= 2 && normalized !in result) { + if (normalized.length >= MIN_WORD_LENGTH && normalized !in result) { result.add(normalized) } } diff --git a/detekt.yml b/detekt.yml new file mode 100644 index 0000000..4e56ad2 --- /dev/null +++ b/detekt.yml @@ -0,0 +1,797 @@ +build: + maxIssues: 0 + excludeCorrectable: false + weights: + # complexity: 2 + # LongParameterList: 1 + # style: 1 + # comments: 1 + +config: + validation: true + warningsAsErrors: false + checkExhaustiveness: 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' + # - 'LiteFindingsReport' + +output-reports: + active: true + exclude: + # - 'TxtOutputReport' + # - 'XmlOutputReport' + # - 'HtmlOutputReport' + # - 'MdOutputReport' + # - 'SarifOutputReport' + +comments: + active: true + AbsentOrWrongFileLicense: + active: false + licenseTemplateFile: 'license.template' + licenseTemplateIsRegex: false + CommentOverPrivateFunction: + active: false + CommentOverPrivateProperty: + active: false + DeprecatedBlockTag: + active: false + EndOfSentenceFormat: + active: false + endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)' + KDocReferencesNonPublicProperty: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + OutdatedDocumentation: + active: false + matchTypeParameters: true + matchDeclarationsOrder: true + allowParamOnConstructorProperties: false + UndocumentedPublicClass: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchInNestedClass: true + searchInInnerClass: true + searchInInnerObject: true + searchInInnerInterface: true + searchInProtectedClass: false + ignoreDefaultCompanionObject: false + UndocumentedPublicFunction: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchProtectedFunction: false + UndocumentedPublicProperty: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + searchProtectedProperty: false + +complexity: + active: true + CognitiveComplexMethod: + active: false + threshold: 15 + ComplexCondition: + active: true + threshold: 4 + ComplexInterface: + active: false + threshold: 10 + includeStaticDeclarations: false + includePrivateDeclarations: false + ignoreOverloaded: false + CyclomaticComplexMethod: + active: true + threshold: 15 + ignoreSingleWhenExpression: false + ignoreSimpleWhenEntries: false + ignoreNestingFunctions: false + nestingFunctions: + - 'also' + - 'apply' + - 'forEach' + - 'isNotNull' + - 'ifNull' + - 'let' + - 'run' + - 'use' + - 'with' + 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 + ignoreAnnotatedParameter: [] + MethodOverloading: + active: false + threshold: 6 + NamedArguments: + active: false + threshold: 3 + ignoreArgumentsMatchingNames: false + NestedBlockDepth: + active: true + threshold: 4 + NestedScopeFunctions: + active: false + threshold: 1 + functions: + - 'kotlin.apply' + - 'kotlin.run' + - 'kotlin.with' + - 'kotlin.let' + - 'kotlin.also' + ReplaceSafeCallChainWithRun: + active: false + StringLiteralDuplication: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + threshold: 3 + ignoreAnnotation: true + excludeStringsWithLessThan5Characters: true + ignoreStringsRegex: '$^' + TooManyFunctions: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + thresholdInFiles: 11 + thresholdInClasses: 11 + thresholdInInterfaces: 11 + thresholdInObjects: 11 + thresholdInEnums: 11 + ignoreDeprecated: false + ignorePrivate: false + ignoreOverridden: false + ignoreAnnotatedFunctions: [] + +coroutines: + active: true + GlobalCoroutineUsage: + active: false + InjectDispatcher: + active: true + dispatcherNames: + - 'IO' + - 'Default' + - 'Unconfined' + RedundantSuspendModifier: + active: true + SleepInsteadOfDelay: + active: true + SuspendFunSwallowedCancellation: + active: false + SuspendFunWithCoroutineScopeReceiver: + active: false + 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: + - 'equals' + - 'finalize' + - 'hashCode' + - 'toString' + InstanceOfCheckForException: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + NotImplementedDeclaration: + active: false + ObjectExtendsThrowable: + active: false + PrintStackTrace: + active: true + RethrowCaughtException: + active: true + ReturnFromFinally: + active: true + ignoreLabeled: false + SwallowedException: + active: true + ignoredExceptionTypes: + - 'InterruptedException' + - 'MalformedURLException' + - 'NumberFormatException' + - 'ParseException' + allowedExceptionNameRegex: '_|(ignore|expected).*' + ThrowingExceptionFromFinally: + active: true + ThrowingExceptionInMain: + active: false + ThrowingExceptionsWithoutMessageOrCause: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + exceptions: + - 'ArrayIndexOutOfBoundsException' + - 'Exception' + - 'IllegalArgumentException' + - 'IllegalMonitorStateException' + - 'IllegalStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' + ThrowingNewInstanceOfSameException: + active: true + TooGenericExceptionCaught: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + exceptionNames: + - 'ArrayIndexOutOfBoundsException' + - 'Error' + - 'Exception' + - 'IllegalMonitorStateException' + - 'IndexOutOfBoundsException' + - 'NullPointerException' + - 'RuntimeException' + - 'Throwable' + allowedExceptionNameRegex: '_|(ignore|expected).*' + TooGenericExceptionThrown: + active: true + exceptionNames: + - 'Error' + - 'Exception' + - 'RuntimeException' + - 'Throwable' + +naming: + active: true + BooleanPropertyNaming: + active: false + allowedPattern: '^(is|has|are)' + ClassNaming: + active: true + classPattern: '[A-Z][a-zA-Z0-9]*' + ConstructorParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + privateParameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + EnumNaming: + active: true + enumEntryPattern: '[A-Z][_a-zA-Z0-9]*' + ForbiddenClassName: + active: false + forbiddenName: [] + FunctionMaxLength: + active: false + maximumFunctionNameLength: 30 + FunctionMinLength: + active: false + minimumFunctionNameLength: 3 + FunctionNaming: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + functionPattern: '[a-z][a-zA-Z0-9]*' + excludeClassPattern: '$^' + FunctionParameterNaming: + active: true + parameterPattern: '[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + InvalidPackageDeclaration: + active: true + rootPackage: '' + requireRootInDeclaration: false + LambdaParameterNaming: + active: false + parameterPattern: '[a-z][A-Za-z0-9]*|_' + MatchingDeclarationName: + active: true + mustBeFirst: true + multiplatformTargets: + - 'ios' + - 'android' + - 'js' + - 'jvm' + - 'native' + - 'iosArm64' + - 'iosX64' + - 'macosX64' + - 'mingwX64' + - 'linuxX64' + MemberNameEqualsClassName: + active: true + ignoreOverridden: true + NoNameShadowing: + active: true + NonBooleanPropertyPrefixedWithIs: + active: false + ObjectPropertyNaming: + active: true + 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 + packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*' + TopLevelPropertyNaming: + active: true + 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 + maximumVariableNameLength: 64 + VariableMinLength: + active: false + minimumVariableNameLength: 1 + VariableNaming: + active: true + variablePattern: '[a-z][A-Za-z0-9]*' + privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' + excludeClassPattern: '$^' + +performance: + active: true + ArrayPrimitive: + active: true + CouldBeSequence: + active: false + threshold: 3 + ForEachOnRange: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + SpreadOperator: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + UnnecessaryPartOfBinaryExpression: + active: false + UnnecessaryTemporaryInstantiation: + active: true + +potential-bugs: + active: true + AvoidReferentialEquality: + active: true + forbiddenTypePatterns: + - 'kotlin.String' + CastNullableToNonNullableType: + active: false + CastToNullableType: + active: false + Deprecation: + active: false + DontDowncastCollectionTypes: + active: false + DoubleMutabilityForCollection: + active: true + mutableTypes: + - 'kotlin.collections.MutableList' + - 'kotlin.collections.MutableMap' + - 'kotlin.collections.MutableSet' + - 'java.util.ArrayList' + - 'java.util.LinkedHashSet' + - 'java.util.HashSet' + - 'java.util.LinkedHashMap' + - 'java.util.HashMap' + ElseCaseInsteadOfExhaustiveWhen: + active: false + ignoredSubjectTypes: [] + EqualsAlwaysReturnsTrueOrFalse: + active: true + EqualsWithHashCodeExist: + active: true + ExitOutsideMain: + active: false + ExplicitGarbageCollectionCall: + active: true + HasPlatformType: + active: true + IgnoredReturnValue: + active: true + restrictToConfig: true + returnValueAnnotations: + - 'CheckResult' + - '*.CheckResult' + - 'CheckReturnValue' + - '*.CheckReturnValue' + ignoreReturnValueAnnotations: + - 'CanIgnoreReturnValue' + - '*.CanIgnoreReturnValue' + returnValueTypes: + - 'kotlin.sequences.Sequence' + - 'kotlinx.coroutines.flow.*Flow' + - 'java.util.stream.*Stream' + ignoreFunctionCall: [] + ImplicitDefaultLocale: + active: true + ImplicitUnitReturnType: + active: false + allowExplicitReturnType: true + InvalidRange: + active: true + IteratorHasNextCallsNextMethod: + active: true + IteratorNotThrowingNoSuchElementException: + active: true + LateinitUsage: + active: false + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + ignoreOnClassesPattern: '' + MapGetWithNotNullAssertionOperator: + active: true + MissingPackageDeclaration: + active: false + excludes: ['**/*.kts'] + NullCheckOnMutableProperty: + active: false + NullableToStringCall: + active: false + PropertyUsedBeforeDeclaration: + active: false + UnconditionalJumpStatementInLoop: + active: false + UnnecessaryNotNullCheck: + active: false + UnnecessaryNotNullOperator: + active: true + UnnecessarySafeCall: + active: true + UnreachableCatchBlock: + active: true + UnreachableCode: + active: true + UnsafeCallOnNullableType: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**'] + UnsafeCast: + active: true + UnusedUnaryOperator: + active: true + UselessPostfixExpression: + active: true + WrongEqualsTypeParameter: + active: true + +style: + active: true + AlsoCouldBeApply: + active: false + BracesOnIfStatements: + active: false + singleLine: 'never' + multiLine: 'always' + BracesOnWhenStatements: + active: false + singleLine: 'necessary' + multiLine: 'consistent' + CanBeNonNullable: + active: false + CascadingCallWrapping: + active: false + includeElvis: true + ClassOrdering: + active: false + CollapsibleIfStatements: + active: false + DataClassContainsFunctions: + active: false + conversionFunctionPrefix: + - 'to' + allowOperators: false + DataClassShouldBeImmutable: + active: false + DestructuringDeclarationWithTooManyEntries: + active: true + maxDestructuringEntries: 3 + DoubleNegativeLambda: + active: false + negativeFunctions: + - reason: 'Use `takeIf` instead.' + value: 'takeUnless' + - reason: 'Use `all` instead.' + value: 'none' + negativeFunctionNameParts: + - 'not' + - 'non' + EqualsNullCall: + active: true + EqualsOnSignatureLine: + active: false + ExplicitCollectionElementAccessMethod: + active: false + ExplicitItLambdaParameter: + active: true + ExpressionBodySyntax: + active: false + includeLineWrapping: false + ForbiddenAnnotation: + active: false + annotations: + - reason: 'it is a java annotation. Use `Suppress` instead.' + value: 'java.lang.SuppressWarnings' + - reason: 'it is a java annotation. Use `kotlin.Deprecated` instead.' + value: 'java.lang.Deprecated' + - reason: 'it is a java annotation. Use `kotlin.annotation.MustBeDocumented` instead.' + value: 'java.lang.annotation.Documented' + - reason: 'it is a java annotation. Use `kotlin.annotation.Target` instead.' + value: 'java.lang.annotation.Target' + - reason: 'it is a java annotation. Use `kotlin.annotation.Retention` instead.' + value: 'java.lang.annotation.Retention' + - reason: 'it is a java annotation. Use `kotlin.annotation.Repeatable` instead.' + value: 'java.lang.annotation.Repeatable' + - reason: 'Kotlin does not support @Inherited annotation, see https://youtrack.jetbrains.com/issue/KT-22265' + value: 'java.lang.annotation.Inherited' + ForbiddenComment: + active: true + comments: + - reason: 'Forbidden FIXME todo marker in comment, please fix the problem.' + value: 'FIXME:' + - reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.' + value: 'STOPSHIP:' + - reason: 'Forbidden TODO todo marker in comment, please do the changes.' + value: 'TODO:' + allowedPatterns: '' + ForbiddenImport: + active: false + imports: [] + forbiddenPatterns: '' + ForbiddenMethodCall: + active: false + methods: + - reason: 'print does not allow you to configure the output stream. Use a logger instead.' + value: 'kotlin.io.print' + - reason: 'println does not allow you to configure the output stream. Use a logger instead.' + value: 'kotlin.io.println' + ForbiddenSuppress: + active: false + rules: [] + ForbiddenVoid: + active: true + ignoreOverridden: false + ignoreUsageInGenerics: false + FunctionOnlyReturningConstant: + active: true + ignoreOverridableFunction: true + ignoreActualFunction: true + excludedFunctions: [] + LoopWithTooManyJumpStatements: + active: true + maxJumpCount: 1 + MagicNumber: + active: true + excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts'] + 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 + MaxChainedCallsOnSameLine: + active: false + maxChainedCalls: 5 + MaxLineLength: + active: true + maxLineLength: 120 + excludePackageStatements: true + excludeImportStatements: true + excludeCommentStatements: false + excludeRawStrings: true + MayBeConst: + active: true + ModifierOrder: + active: true + MultilineLambdaItParameter: + active: false + MultilineRawStringIndentation: + active: false + indentSize: 4 + trimmingMethods: + - 'trimIndent' + - 'trimMargin' + NestedClassesVisibility: + active: true + NewLineAtEndOfFile: + active: true + NoTabs: + active: false + NullableBooleanCheck: + active: false + ObjectLiteralToLambda: + active: true + OptionalAbstractKeyword: + active: true + OptionalUnit: + active: false + PreferToOverPairSyntax: + active: false + ProtectedMemberInFinalClass: + active: true + RedundantExplicitType: + active: false + RedundantHigherOrderMapUsage: + active: true + RedundantVisibilityModifierRule: + active: false + ReturnCount: + active: true + max: 4 + excludedFunctions: + - 'equals' + excludeLabeled: false + excludeReturnFromLambda: true + excludeGuardClauses: true + SafeCast: + active: true + SerialVersionUIDInSerializableClass: + active: true + SpacingBetweenPackageAndImports: + active: false + StringShouldBeRawString: + active: false + maxEscapedCharacterCount: 2 + ignoredCharacters: [] + ThrowsCount: + active: true + max: 2 + excludeGuardClauses: false + TrailingWhitespace: + active: false + TrimMultilineRawString: + active: false + trimmingMethods: + - 'trimIndent' + - 'trimMargin' + UnderscoresInNumericLiterals: + active: false + acceptableLength: 4 + allowNonStandardGrouping: false + UnnecessaryAbstractClass: + active: true + UnnecessaryAnnotationUseSiteTarget: + active: false + UnnecessaryApply: + active: true + UnnecessaryBackticks: + active: false + UnnecessaryBracesAroundTrailingLambda: + active: false + UnnecessaryFilter: + active: true + UnnecessaryInheritance: + active: true + UnnecessaryInnerClass: + active: false + UnnecessaryLet: + active: false + UnnecessaryParentheses: + active: false + allowForUnclearPrecedence: false + UntilInsteadOfRangeTo: + active: false + UnusedImports: + active: false + UnusedParameter: + active: true + allowedNames: 'ignored|expected' + UnusedPrivateClass: + active: true + UnusedPrivateMember: + active: true + allowedNames: '' + UnusedPrivateProperty: + active: true + allowedNames: '_|ignored|expected|serialVersionUID' + UseAnyOrNoneInsteadOfFind: + active: true + UseArrayLiteralsInAnnotations: + active: true + UseCheckNotNull: + active: true + UseCheckOrError: + active: true + UseDataClass: + active: false + allowVars: false + UseEmptyCounterpart: + active: false + UseIfEmptyOrIfBlank: + active: false + UseIfInsteadOfWhen: + active: false + ignoreWhenContainingVariableDeclaration: false + UseIsNullOrEmpty: + active: true + UseLet: + active: false + UseOrEmpty: + active: true + UseRequire: + active: true + UseRequireNotNull: + active: true + UseSumOfInsteadOfFlatMapSize: + active: false + UselessCallOnNotNull: + active: true + UtilityClassWithPublicConstructor: + active: true + VarCouldBeVal: + active: true + ignoreLateinitVar: false + WildcardImport: + active: true + excludeImports: + - 'java.util.*' diff --git a/ktlint-baseline.xml b/ktlint-baseline.xml deleted file mode 100644 index 868ce41..0000000 --- a/ktlint-baseline.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/tools/lint/BUILD.bazel b/tools/lint/BUILD.bazel index ffd0fb0..5b6359b 100644 --- a/tools/lint/BUILD.bazel +++ b/tools/lint/BUILD.bazel @@ -1 +1,9 @@ +load("@rules_java//java:defs.bzl", "java_binary") + package(default_visibility = ["//visibility:public"]) + +java_binary( + name = "detekt", + main_class = "io.gitlab.arturbosch.detekt.cli.Main", + runtime_deps = ["@maven//:io_gitlab_arturbosch_detekt_detekt_cli"], +) diff --git a/tools/lint/detekt.bzl b/tools/lint/detekt.bzl new file mode 100644 index 0000000..528b181 --- /dev/null +++ b/tools/lint/detekt.bzl @@ -0,0 +1,158 @@ +"""API for declaring a Detekt lint aspect that visits Kotlin rules.""" + +load( + "@aspect_rules_lint//lint/private:lint_aspect.bzl", + "LintOptionsInfo", + "filter_srcs", + "noop_lint_action", + "output_files", + "should_visit", +) + +_MNEMONIC = "AspectRulesLintDetekt" + +def detekt_action( + ctx, + executable, + srcs, + config, + baseline, + human_out, + machine_out, + human_exit_code = None, + machine_exit_code = None, + args = []): + """Run Detekt as an action under Bazel.""" + inputs = list(srcs) + outputs = [human_out, machine_out] + + action_args = ctx.actions.args() + action_args.add("--input") + action_args.add_joined(srcs, join_with = ",") + + if config: + inputs.append(config) + action_args.add("--config") + action_args.add(config.path) + + if baseline: + inputs.append(baseline) + action_args.add("--baseline") + action_args.add(baseline.path) + + action_args.add("--report") + action_args.add("sarif:{}".format(machine_out.path)) + action_args.add_all(args) + + command = """ +{detekt} "$@" >"{human_out}" 2>&1 +EC=$? +if [ ! -f "{machine_out}" ]; then + touch "{machine_out}" +fi +""" + + if human_exit_code: + outputs.extend([human_exit_code, machine_exit_code]) + command += """ +echo $EC > "{human_exit_code}" +echo $EC > "{machine_exit_code}" +""" + else: + command += """ +if [ $EC -ne 0 ]; then + cat "{human_out}" + exit $EC +fi +""" + + ctx.actions.run_shell( + inputs = inputs, + outputs = outputs, + tools = [executable], + command = command.format( + detekt = executable.path, + human_out = human_out.path, + machine_out = machine_out.path, + human_exit_code = human_exit_code.path if human_exit_code else "", + machine_exit_code = machine_exit_code.path if machine_exit_code else "", + ), + arguments = [action_args], + mnemonic = _MNEMONIC, + progress_message = "Linting %{label} with Detekt", + ) + +def _detekt_aspect_impl(target, ctx): + if not should_visit(ctx.rule, ctx.attr._rule_kinds): + return [] + + files_to_lint = [ + f + for f in filter_srcs(ctx.rule) + if f.extension in ["kt", "kts"] + ] + + outputs, info = output_files(_MNEMONIC, target, ctx) + + if len(files_to_lint) == 0: + noop_lint_action(ctx, outputs) + return [info] + + baseline = getattr(ctx.file, "_baseline", None) + + detekt_action( + ctx = ctx, + executable = ctx.executable._detekt, + srcs = files_to_lint, + config = ctx.file._config, + baseline = baseline, + human_out = outputs.human.out, + machine_out = outputs.machine.out, + human_exit_code = outputs.human.exit_code, + machine_exit_code = outputs.machine.exit_code, + args = ctx.attr._args, + ) + + return [info] + +def lint_detekt_aspect( + binary, + config, + baseline = None, + rule_kinds = ["kt_jvm_library", "kt_jvm_binary", "kt_jvm_test"], + args = []): + """A factory function to create a Detekt linter aspect.""" + attrs = { + "_options": attr.label( + default = Label("@aspect_rules_lint//lint:options"), + providers = [LintOptionsInfo], + ), + "_detekt": attr.label( + default = binary, + executable = True, + cfg = "exec", + ), + "_config": attr.label( + allow_single_file = True, + mandatory = True, + doc = "Detekt YAML configuration file", + default = config, + ), + "_rule_kinds": attr.string_list( + default = rule_kinds, + ), + "_args": attr.string_list( + default = args, + ), + } + + if baseline: + attrs["_baseline"] = attr.label( + allow_single_file = True, + default = baseline, + ) + + return aspect( + implementation = _detekt_aspect_impl, + attrs = attrs, + ) diff --git a/tools/lint/linters.bzl b/tools/lint/linters.bzl index 2ed0c2a..ac79f90 100644 --- a/tools/lint/linters.bzl +++ b/tools/lint/linters.bzl @@ -1,16 +1,16 @@ """Linter aspects for Calque.""" load("@aspect_rules_lint//lint:buildifier.bzl", "lint_buildifier_aspect") -load("@aspect_rules_lint//lint:ktlint.bzl", "lint_ktlint_aspect") load("@aspect_rules_lint//lint:lint_test.bzl", "lint_test") +load("//tools/lint:detekt.bzl", "lint_detekt_aspect") -ktlint = lint_ktlint_aspect( - binary = Label("@com_github_pinterest_ktlint//file"), - editorconfig = Label("//:.editorconfig"), - baseline_file = Label("//:ktlint-baseline.xml"), +detekt = lint_detekt_aspect( + binary = Label("//tools/lint:detekt"), + config = Label("//:detekt.yml"), + args = ["--build-upon-default-config"], ) -ktlint_test = lint_test(aspect = ktlint) +detekt_test = lint_test(aspect = detekt) buildifier = lint_buildifier_aspect( binary = Label("@buildifier_prebuilt//:buildifier"),