From da942624a4abd82797f563cf8c57f1124c5de435 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Mon, 24 Aug 2026 07:54:01 +0000 Subject: Add TDD quality gate: detekt, coverage tracking, pre-commit hook; fix theme crash risk CLAUDE.md now documents mandatory red-green-refactor TDD, a per-commit quality gate (build, tests, lint, detekt, coverage-non-decrease, WebView-JS justification, manifest permission diff, secret scan), and why each check exists (tracing back to real bugs this audit found). Wires up detekt (static analysis) and a standalone Jacoco setup (scoped to data/readability/opml/work/stash -- ui/ Compose screens are impractical to unit-test for coverage) via app/build.gradle.kts. Bumped Kotlin/KSP to 2.0.21/2.0.21-1.0.28 to match detekt 1.23.8's compatibility requirement. Also fixes the themes.xml bug the lint gate immediately caught: Theme.FeedReader's parent required API 29 while minSdk is 26 -- a real crash risk on Android 8.0-9.0 devices. Swapped to the plain (non-DayNight) DeviceDefault parent, since Compose already owns all real theming via a hardcoded darkColorScheme() regardless of system day/night state. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017GMEkHeqKz6FLkmizowBTK --- config/detekt/baseline.xml | 77 ++++++++++++++++++++++++++++++++++++++++++++++ config/detekt/detekt.yml | 16 ++++++++++ 2 files changed, 93 insertions(+) create mode 100644 config/detekt/baseline.xml create mode 100644 config/detekt/detekt.yml (limited to 'config') diff --git a/config/detekt/baseline.xml b/config/detekt/baseline.xml new file mode 100644 index 0000000..bd02fbe --- /dev/null +++ b/config/detekt/baseline.xml @@ -0,0 +1,77 @@ + + + + + ComplexCondition:FeedParser.kt$FeedParser$inEntry && rel == "enclosure" && type?.startsWith("image/") == true && href != null + CyclomaticComplexMethod:ArticleListScreen.kt$@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class, ExperimentalCoroutinesApi::class) @Composable fun ArticleListScreen( feedId: String, onOpenArticle: (String) -> Unit, onBack: () -> Unit, onOpenFeedList: () -> Unit ) + CyclomaticComplexMethod:FeedFetchWorker.kt$FeedFetchWorker$private suspend fun fetchOne(feed: FeedEntity, db: AppDatabase): Int + CyclomaticComplexMethod:FeedParser.kt$FeedParser$private fun parseAtom(parser: XmlPullParser): ParsedFeed + CyclomaticComplexMethod:FeedParser.kt$FeedParser$private fun parseRss(parser: XmlPullParser): ParsedFeed + CyclomaticComplexMethod:LinkReaderScreen.kt$@SuppressLint("SetJavaScriptEnabled") @OptIn(ExperimentalMaterial3Api::class) @Composable fun LinkReaderScreen(url: String, onBack: () -> Unit, onOpenLink: (String) -> Unit, onOpenBrowser: (String) -> Unit) + CyclomaticComplexMethod:ReaderScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable private fun ReaderContent( articleId: String, onBack: () -> Unit, onOpenLink: (String) -> Unit, onOpenBrowser: (String) -> Unit, hasPrevious: Boolean, hasNext: Boolean, onPrevious: () -> Unit, onNext: () -> Unit, onNextKeepUnread: () -> Unit ) + LongMethod:AddFeedScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun AddFeedScreen(onDone: () -> Unit) + LongMethod:ArticleListScreen.kt$@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class, ExperimentalCoroutinesApi::class) @Composable fun ArticleListScreen( feedId: String, onOpenArticle: (String) -> Unit, onBack: () -> Unit, onOpenFeedList: () -> Unit ) + LongMethod:FeedFetchWorker.kt$FeedFetchWorker$private suspend fun fetchOne(feed: FeedEntity, db: AppDatabase): Int + LongMethod:FeedListScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun FeedListScreen( onOpenFeed: (String) -> Unit, onOpenUnread: () -> Unit, onOpenOlder: () -> Unit, onOpenSaved: () -> Unit, onAddFeed: () -> Unit, onSettings: () -> Unit ) + LongMethod:FeedParser.kt$FeedParser$private fun parseAtom(parser: XmlPullParser): ParsedFeed + LongMethod:FeedParser.kt$FeedParser$private fun parseRss(parser: XmlPullParser): ParsedFeed + LongMethod:LinkReaderScreen.kt$@SuppressLint("SetJavaScriptEnabled") @OptIn(ExperimentalMaterial3Api::class) @Composable fun LinkReaderScreen(url: String, onBack: () -> Unit, onOpenLink: (String) -> Unit, onOpenBrowser: (String) -> Unit) + LongMethod:MainActivity.kt$@Composable fun FeedReaderApp() + LongMethod:ReaderExtractor.kt$ReaderExtractor.<no name provided>$override fun onPageFinished(view: WebView, finishedUrl: String) + LongMethod:ReaderScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable private fun ReaderContent( articleId: String, onBack: () -> Unit, onOpenLink: (String) -> Unit, onOpenBrowser: (String) -> Unit, hasPrevious: Boolean, hasNext: Boolean, onPrevious: () -> Unit, onNext: () -> Unit, onNextKeepUnread: () -> Unit ) + LongMethod:SettingsScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun SettingsScreen(onBack: () -> Unit) + LongParameterList:ArticleListScreen.kt$( article: ArticleListItem, highlighted: Boolean, showFeedName: Boolean, showReadState: Boolean, onClick: () -> Unit, onMarkReadFrom: () -> Unit ) + LongParameterList:Daos.kt$FeedDao$( id: String, title: String, siteUrl: String?, lastFetchedAt: Long, etag: String?, lastModified: String?, lastError: String? ) + LongParameterList:FeedListScreen.kt$( onOpenFeed: (String) -> Unit, onOpenUnread: () -> Unit, onOpenOlder: () -> Unit, onOpenSaved: () -> Unit, onAddFeed: () -> Unit, onSettings: () -> Unit ) + LongParameterList:ReaderScreen.kt$( articleId: String, onBack: () -> Unit, onOpenLink: (String) -> Unit, onOpenBrowser: (String) -> Unit, hasPrevious: Boolean, hasNext: Boolean, onPrevious: () -> Unit, onNext: () -> Unit, onNextKeepUnread: () -> Unit ) + LongParameterList:ReaderScreen.kt$( feedId: String, articleId: String, onBack: () -> Unit, onOpenLink: (String) -> Unit, onOpenBrowser: (String) -> Unit, onNavigateToArticle: (String) -> Unit ) + LongParameterList:ReaderWebView.kt$( context: Context, onOpenLink: (String) -> Unit, onLongPressLink: (String) -> Unit = {}, onYouTubeVideo: (String) -> Unit = {}, onScrollChanged: (Int) -> Unit = {}, onPageFinished: (WebView) -> Unit = {} ) + LongParameterList:ReaderWebView.kt$( url: String, onDismiss: () -> Unit, onOpenInBrowser: () -> Unit, onShare: () -> Unit, onSaveToStash: () -> Unit, onSaveForLater: () -> Unit ) + MagicNumber:AppDatabase.kt$AppDatabase.Companion.<no name provided>$10 + MagicNumber:AppDatabase.kt$AppDatabase.Companion.<no name provided>$11 + MagicNumber:AppDatabase.kt$AppDatabase.Companion.<no name provided>$3 + MagicNumber:AppDatabase.kt$AppDatabase.Companion.<no name provided>$4 + MagicNumber:AppDatabase.kt$AppDatabase.Companion.<no name provided>$5 + MagicNumber:AppDatabase.kt$AppDatabase.Companion.<no name provided>$6 + MagicNumber:AppDatabase.kt$AppDatabase.Companion.<no name provided>$7 + MagicNumber:AppDatabase.kt$AppDatabase.Companion.<no name provided>$8 + MagicNumber:AppDatabase.kt$AppDatabase.Companion.<no name provided>$9 + MagicNumber:Daos.kt$500 + MagicNumber:FeedFetchWorker.kt$FeedFetchWorker$304 + MagicNumber:ReaderWebView.kt$16f + MagicNumber:ReaderWebView.kt$9f + MagicNumber:StashClient.kt$StashClient$200 + MagicNumber:StashClient.kt$StashClient$201 + MagicNumber:StashClient.kt$StashClient$35 + MagicNumber:StashClient.kt$StashClient$401 + MagicNumber:StashClient.kt$StashClient$403 + MagicNumber:StashClient.kt$StashClient$409 + MatchingDeclarationName:ReaderWebView.kt$ReaderScrollPositions + MaxLineLength:Daos.kt$ArticleDao$"WHERE (articles.isRead = 0 OR articles.readAt >= :sinceFetch) AND (articles.publishedAt IS NULL OR articles.publishedAt >= :cutoff) " + MaxLineLength:Daos.kt$ArticleDao$"WHERE (articles.isRead = 0 OR articles.readAt >= :sinceFetch) AND articles.publishedAt IS NOT NULL AND articles.publishedAt < :cutoff " + MaxLineLength:Entities.kt$ArticleEntity$@ColumnInfo(index = true) override val feedId: String? + MaxLineLength:Entities.kt$ArticleEntity$val imageUrl: String? = null + MaxLineLength:Entities.kt$ArticleEntity$val readAt: Long? = null + MaxLineLength:LinkReaderScreen.kt$loadDataWithBaseURL(url, wrapReaderHtml(currentExtractedContent, extractedTitle), "text/html", "UTF-8", null) + MaxLineLength:LinkReaderScreen.kt$webView.loadDataWithBaseURL(url, wrapReaderHtml(currentExtractedContent, extractedTitle), "text/html", "UTF-8", null) + MaxLineLength:ReaderScreen.kt$loadDataWithBaseURL(baseUrl, wrapReaderHtml(content, article?.title, article?.publishedAt, article?.imageUrl), "text/html", "UTF-8", null) + MaxLineLength:ReaderScreen.kt$webView.loadDataWithBaseURL(baseUrl, wrapReaderHtml(content, article?.title, article?.publishedAt, article?.imageUrl), "text/html", "UTF-8", null) + MaxLineLength:ReaderWebView.kt$"<figure><a href=\"https://www.youtube.com/watch?v=$id\"><img src=\"https://i.ytimg.com/vi/$id/hq720.jpg\" alt=\"YouTube video\"></a><figcaption>▶ Watch on YouTube</figcaption></figure>" + MaxLineLength:SettingsScreen.kt$DropdownMenuItem(text = { Text("Highlight") }, onClick = { newRuleAction = FilterAction.HIGHLIGHT; actionMenuExpanded = false }) + MaxLineLength:SettingsScreen.kt$DropdownMenuItem(text = { Text("Mute") }, onClick = { newRuleAction = FilterAction.MUTE; actionMenuExpanded = false }) + NestedBlockDepth:AppDatabase.kt$AppDatabase.Companion.<no name provided>$override fun migrate(db: SupportSQLiteDatabase) + NestedBlockDepth:FeedFetchWorker.kt$FeedFetchWorker$private suspend fun fetchOne(feed: FeedEntity, db: AppDatabase): Int + NestedBlockDepth:FeedParser.kt$FeedParser$private fun parseAtom(parser: XmlPullParser): ParsedFeed + NestedBlockDepth:FeedParser.kt$FeedParser$private fun parseRss(parser: XmlPullParser): ParsedFeed + NestedBlockDepth:Opml.kt$Opml$fun parse(input: InputStream): List<OpmlFeed> + ReturnCount:ReaderWebView.kt$<no name provided>$override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean + TooGenericExceptionCaught:AddFeedScreen.kt$e: Exception + TooGenericExceptionCaught:AppDatabase.kt$AppDatabase.Companion$e: Exception + TooGenericExceptionCaught:AppDatabase.kt$AppDatabase.Companion.<no name provided>$e: Exception + TooGenericExceptionCaught:FeedFetchWorker.kt$FeedFetchWorker$e: Exception + TooGenericExceptionCaught:MainActivity.kt$MainActivity$e: Exception + TooGenericExceptionCaught:MorningFetchReceiver.kt$MorningFetchReceiver$e: Exception + TooGenericExceptionCaught:MorningFetchReceiver.kt$MorningFetchReceiver.Companion$e: Exception + TooManyFunctions:Daos.kt$ArticleDao + + diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml new file mode 100644 index 0000000..442435a --- /dev/null +++ b/config/detekt/detekt.yml @@ -0,0 +1,16 @@ +# Builds on detekt's default ruleset (see app/build.gradle.kts: buildUponDefaultConfig = true). +# Only overrides/additions relative to the defaults belong here -- keep this file small so +# upgrading detekt pulls in sane new defaults automatically instead of us silently diverging. + +style: + MaxLineLength: + maxLineLength: 140 # several files carry long, deliberate rationale comments (see + # ReaderExtractor.kt) -- 120 (detekt's default) wrapped mid-explanation too aggressively. + ForbiddenComment: + active: false # this codebase deliberately doesn't use TODO/FIXME markers (see CLAUDE.md + # "why these checks") -- nothing to forbid. + +naming: + FunctionNaming: + ignoreAnnotated: ["Composable"] # Jetpack Compose's own naming convention is PascalCase for + # composable functions -- that's the correct style here, not a violation. -- cgit v1.2.3