diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..0ae52625 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{kt,kts}] +indent_size = 4 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 492a7265..6e776478 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,3 +29,6 @@ jobs: licenses-check: uses: ./.github/workflows/licenses-check.yml + + code-style: + uses: ./.github/workflows/code-style.yml diff --git a/.github/workflows/code-style.yml b/.github/workflows/code-style.yml new file mode 100644 index 00000000..7ad34899 --- /dev/null +++ b/.github/workflows/code-style.yml @@ -0,0 +1,24 @@ +on: + workflow_call: + +jobs: + code-style: + name: 👔 Code style + runs-on: ubuntu-24.04 + + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup-jdk-gradle + + - name: 🕵🏻‍♂️ Detekt + run: | + ./gradlew --no-daemon --parallel --console=plain \ + detekt + echo "### Detekt Report" >> $GITHUB_STEP_SUMMARY + if [ -f build/reports/detekt/detekt.txt ]; then + echo '```' >> $GITHUB_STEP_SUMMARY + cat build/reports/detekt/detekt.txt >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + else + echo "No report found." >> $GITHUB_STEP_SUMMARY + fi \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 43cdd852..2cd6b0f7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -20,6 +20,7 @@ * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +import io.gitlab.arturbosch.detekt.Detekt import kotlinx.kover.gradle.plugin.dsl.CoverageUnit import org.gradle.api.tasks.testing.logging.TestExceptionFormat @@ -39,6 +40,7 @@ plugins { alias(libs.plugins.firebase.crashlytics) apply false alias(libs.plugins.about.libraries) apply false alias(libs.plugins.kover) + alias(libs.plugins.detekt) } val koverProjects = listOf( @@ -124,6 +126,32 @@ kover { } } +allprojects { + project.afterEvaluate { + apply(plugin = libs.plugins.detekt.get().pluginId) + detekt { + autoCorrect = true + config.setFrom("$rootDir/detekt.yml") + allRules = true + parallel = true + ignoreFailures = false + } + + dependencies { + detektPlugins(libs.detekt.formatting) + } + } + + tasks.withType().configureEach { + reports { + txt.required.set(true) + html.required.set(false) + xml.required.set(false) + sarif.required.set(false) + } + } +} + subprojects { tasks { findByName("test") ?: return@tasks diff --git a/config/detekt/license.template b/config/detekt/license.template new file mode 100644 index 00000000..dfb5136c --- /dev/null +++ b/config/detekt/license.template @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2025 Olivier Patry + * + * 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. + */ \ No newline at end of file diff --git a/detekt.yml b/detekt.yml new file mode 100644 index 00000000..f02310a4 --- /dev/null +++ b/detekt.yml @@ -0,0 +1,84 @@ +processors: + active: true + +naming: + FunctionNaming: + active: true + ignoreAnnotated: ['Composable'] + functionPattern: '^[a-z][A-Za-z0-9]*$' + MatchingDeclarationName: + active: false + excludes: ['**/ui/**'] + ignoreAnnotated: ['Composable', 'VisibleForTesting'] + FunctionMaxLength: + active: true + ignoreAnnotated: ['Test', 'ParameterizedTest'] + +style: + UnusedPrivateMember: + active: true + ignoreAnnotated: ['Preview', 'PreviewLightDark'] + MaxLineLength: + active: true + maxLineLength: 120 + excludePackageStatements: true + excludeImportStatements: true + excludeCommentStatements: true + ignoreAnnotated: ['Test'] + ClassOrdering: + active: false + UseIfInsteadOfWhen: + active: false + +formatting: + NoEmptyFirstLineInClassBody: + active: false + TrailingCommaOnCallSite: + active: false + useTrailingCommaOnCallSite: true + TrailingCommaOnDeclarationSite: + active: false + useTrailingCommaOnDeclarationSite: true + FunctionSignature: + active: false + forceMultilineWhenParameterCountGreaterOrEqualThan: 3 + FinalNewline: + active: true + MultilineExpressionWrapping: + active: false + PropertyName: + active: false + FunctionName: + active: false + Filename: + active: false + StringTemplateIndent: + active: false + +comments: + UndocumentedPublicClass: + active: false + UndocumentedPublicFunction: + active: false + UndocumentedPublicProperty: + active: false + AbsentOrWrongFileLicense: + active: true + licenseTemplateFile: 'config/detekt/license.template' + licenseTemplateIsRegex: false + +complexity: + LongMethod: + active: true + ignoreAnnotated: ['Composable', 'Test'] + threshold: 60 + StringLiteralDuplication: + active: false + +performance: + SpreadOperator: + active: false + +potential-bugs: + MissingPackageDeclaration: + active: false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ba6c51f0..55cbeda7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,6 +23,7 @@ coil = "3.2.0" about-libraries = "12.2.1" mockito = "5.18.0" kover = "0.9.1" +detekt = "1.23.8" androidx-test-runner = "1.6.2" [libraries] @@ -99,6 +100,8 @@ mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" } androidx-ui-tooling-preview-android = { module = "androidx.compose.ui:ui-tooling-preview-android", version.ref = "compose" } androidx-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } +detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } + [bundles] ktor-server = ["ktor-server-core", "ktor-server-cio"] ktor-client = [ @@ -133,3 +136,4 @@ google-services = { id = "com.google.gms.google-services", version = "4.4.2" } firebase-crashlytics = { id = "com.google.firebase.crashlytics", version = "3.0.3" } about-libraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "about-libraries" } kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } +detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } diff --git a/tasks-app-android/src/main/java/net/opatry/tasks/app/MainActivity.kt b/tasks-app-android/src/main/java/net/opatry/tasks/app/MainActivity.kt index a9f65b6c..37252c91 100644 --- a/tasks-app-android/src/main/java/net/opatry/tasks/app/MainActivity.kt +++ b/tasks-app-android/src/main/java/net/opatry/tasks/app/MainActivity.kt @@ -44,7 +44,6 @@ import net.opatry.tasks.app.ui.theme.TaskfolioTheme import net.opatry.tasks.app.util.readText import org.koin.compose.viewmodel.koinViewModel - class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -64,7 +63,9 @@ class MainActivity : AppCompatActivity() { TaskfolioTheme { Surface(Modifier.statusBarsPadding()) { when (userState) { - null -> LoadingPane() + null -> { + LoadingPane() + } UserState.Unsigned, is UserState.SignedIn -> { @@ -78,8 +79,10 @@ class MainActivity : AppCompatActivity() { TasksApp(aboutApp, userViewModel, tasksViewModel) } - UserState.Newcomer -> AuthorizationScreen(userViewModel::skipSignIn) { - AuthorizeGoogleTasksButton(onSuccess = userViewModel::signIn) + UserState.Newcomer -> { + AuthorizationScreen(userViewModel::skipSignIn) { + AuthorizeGoogleTasksButton(onSuccess = userViewModel::signIn) + } } } } diff --git a/tasks-app-android/src/main/java/net/opatry/tasks/app/TasksApplication.kt b/tasks-app-android/src/main/java/net/opatry/tasks/app/TasksApplication.kt index e79947b0..29fa6919 100644 --- a/tasks-app-android/src/main/java/net/opatry/tasks/app/TasksApplication.kt +++ b/tasks-app-android/src/main/java/net/opatry/tasks/app/TasksApplication.kt @@ -62,4 +62,4 @@ open class TasksApplication : Application(), KoinStartup { initNetworkMonitor(this) } -} \ No newline at end of file +} diff --git a/tasks-app-desktop/src/main/kotlin/mainApp.kt b/tasks-app-desktop/src/main/kotlin/mainApp.kt index e4ae69a0..379d86f6 100644 --- a/tasks-app-desktop/src/main/kotlin/mainApp.kt +++ b/tasks-app-desktop/src/main/kotlin/mainApp.kt @@ -69,6 +69,12 @@ private const val GCP_CLIENT_ID = "191682949161-esokhlfh7uugqptqnu3su9vgqmvltv95 object MainApp +@Suppress( + "detekt:LongMethod", + "detekt:CyclomaticComplexMethod", + "detekt:CognitiveComplexMethod", + "detekt:MagicNumber", +) @OptIn(ExperimentalMaterial3Api::class) fun main() { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()) @@ -86,7 +92,9 @@ fun main() { val minSize = Dimension(600, 400) var windowState = rememberWindowState( - position = WindowPosition(Alignment.Center), width = defaultSize.width, height = defaultSize.height + position = WindowPosition(Alignment.Center), + width = defaultSize.width, + height = defaultSize.height ) var showNewTaskListDialog by remember { mutableStateOf(false) } @@ -133,17 +141,19 @@ fun main() { onNetworkLogClick = { showKtorMonitor = true }, ) - KoinApplication(application = { - modules( - utilModule, - loggingModule, - platformModule("desktop"), - dataModule, - authModule(GCP_CLIENT_ID), - networkModule, - tasksAppModule, - ) - }) { + KoinApplication( + application = { + modules( + utilModule, + loggingModule, + platformModule("desktop"), + dataModule, + authModule(GCP_CLIENT_ID), + networkModule, + tasksAppModule, + ) + } + ) { val userViewModel = koinViewModel() val userState by userViewModel.state.collectAsState(null) @@ -156,7 +166,9 @@ fun main() { TaskfolioTheme { Surface { when (userState) { - null -> LoadingPane() + null -> { + LoadingPane() + } UserState.Unsigned, is UserState.SignedIn -> { @@ -199,8 +211,10 @@ fun main() { TasksApp(aboutApp, userViewModel, tasksViewModel) } - UserState.Newcomer -> AuthorizationScreen(userViewModel::skipSignIn) { - AuthorizeGoogleTasksButton(onSuccess = userViewModel::signIn) + UserState.Newcomer -> { + AuthorizationScreen(userViewModel::skipSignIn) { + AuthorizeGoogleTasksButton(onSuccess = userViewModel::signIn) + } } } } diff --git a/tasks-app-desktop/src/main/kotlin/net/opatry/tasks/app/ui/component/AppMenuBar.kt b/tasks-app-desktop/src/main/kotlin/net/opatry/tasks/app/ui/component/AppMenuBar.kt index e84e307b..2b23f1fb 100644 --- a/tasks-app-desktop/src/main/kotlin/net/opatry/tasks/app/ui/component/AppMenuBar.kt +++ b/tasks-app-desktop/src/main/kotlin/net/opatry/tasks/app/ui/component/AppMenuBar.kt @@ -81,4 +81,4 @@ fun FrameWindowScope.AppMenuBar( } } } -} \ No newline at end of file +} diff --git a/tasks-app-desktop/src/main/kotlin/net/opatry/tasks/app/util/os.kt b/tasks-app-desktop/src/main/kotlin/net/opatry/tasks/app/util/os.kt index 2215a5f6..368a411f 100644 --- a/tasks-app-desktop/src/main/kotlin/net/opatry/tasks/app/util/os.kt +++ b/tasks-app-desktop/src/main/kotlin/net/opatry/tasks/app/util/os.kt @@ -40,4 +40,4 @@ val currentOS: OS by lazy { osName.matches(Regex("n[iu]x", RegexOption.IGNORE_CASE)) -> OS.Linux else -> OS.Windows } -} \ No newline at end of file +}