build.gradle.kts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. import org.gradle.api.DefaultTask
  2. import org.gradle.api.file.DirectoryProperty
  3. import org.gradle.api.provider.MapProperty
  4. import org.gradle.api.tasks.Input
  5. import org.gradle.api.tasks.InputDirectory
  6. import org.gradle.api.tasks.PathSensitive
  7. import org.gradle.api.tasks.PathSensitivity
  8. import org.gradle.api.tasks.TaskAction
  9. import org.gradle.work.DisableCachingByDefault
  10. @DisableCachingByDefault(because = "架构验证任务没有输出文件")
  11. abstract class VerifyConfigArchitectureTask : DefaultTask() {
  12. @get:InputDirectory
  13. @get:PathSensitive(PathSensitivity.RELATIVE)
  14. abstract val sourceRoot: DirectoryProperty
  15. @get:Input
  16. abstract val legacyPreferenceCallBaseline: MapProperty<String, Int>
  17. @get:Input
  18. abstract val legacyDaoInjectionBaseline: MapProperty<String, Int>
  19. @get:Input
  20. abstract val legacyUiDaoAccessBaseline: MapProperty<String, Int>
  21. @TaskAction
  22. fun verify() {
  23. val sourceRootDir = sourceRoot.get().asFile
  24. val kotlinFiles = sourceRootDir.walkTopDown()
  25. .filter { it.isFile && it.extension == "kt" }
  26. .toList()
  27. val preferenceBaseline = legacyPreferenceCallBaseline.get()
  28. val daoInjectionBaseline = legacyDaoInjectionBaseline.get()
  29. val uiDaoAccessBaseline = legacyUiDaoAccessBaseline.get()
  30. val violations = mutableListOf<String>()
  31. val forbiddenConfigImport = Regex(
  32. """^import io\.legado\.app\.(?:help\.config\.AppConfig|ui\.config\..*Config)$""",
  33. RegexOption.MULTILINE,
  34. )
  35. val preferenceCall = Regex("""\b(?:getPref|putPref)[A-Za-z0-9_]*\s*\(""")
  36. val daoImport = Regex(
  37. """^import io\.legado\.app\.data\.dao\.[A-Za-z0-9_*]+$""",
  38. RegexOption.MULTILINE,
  39. )
  40. val appDbDaoAccess = Regex(
  41. """(?:\bappDb|io\.legado\.app\.data\.appDb)\.[A-Za-z0-9_]*Dao\b"""
  42. )
  43. val readBookConfigWrite = Regex(
  44. """\bReadBookConfig\.[a-z_][A-Za-z0-9_]*(?:\.[a-z_][A-Za-z0-9_]*)?\s*="""
  45. )
  46. val readBookConfigMutationCall = Regex(
  47. """\bReadBookConfig\.durConfig\.set[A-Za-z0-9_]*\s*\("""
  48. )
  49. val settingsUpdateDeclaration = Regex(
  50. """\b(?:class|interface|object|typealias)\s+[A-Za-z0-9_]*SettingsUpdate\b"""
  51. )
  52. val updateAllDeclaration = Regex("""\bfun\s+(?:<[^>\n]+>\s*)?updateAll\s*\(""")
  53. val injectedConfigFiles = setOf(
  54. "io/legado/app/help/config/AppConfig.kt",
  55. "io/legado/app/help/config/ReadBookConfig.kt",
  56. "io/legado/app/help/config/ThemePackageManager.kt",
  57. )
  58. kotlinFiles.forEach { file ->
  59. val text = file.readText()
  60. val relativePath = file.relativeTo(sourceRootDir).invariantSeparatorsPath
  61. val displayPath = "app/src/main/java/$relativePath"
  62. if ("prefDelegate" in text || "prefStateDelegate" in text ||
  63. "Snapshot.withMutableSnapshot" in text
  64. ) {
  65. violations += "$displayPath: 禁止 Snapshot 配置桥"
  66. }
  67. if ((relativePath.startsWith("io/legado/app/data/") ||
  68. relativePath.startsWith("io/legado/app/domain/")) &&
  69. forbiddenConfigImport.containsMatchIn(text)
  70. ) {
  71. violations += "$displayPath: data/domain 禁止导入全局 Config"
  72. }
  73. if (("@Composable" in text || "import androidx.compose" in text) &&
  74. forbiddenConfigImport.containsMatchIn(text)
  75. ) {
  76. violations += "$displayPath: Composable 禁止读取兼容 Config"
  77. }
  78. if (file.name.endsWith("Config.kt") &&
  79. ("mutableStateOf(" in text || "Snapshot.withMutableSnapshot" in text ||
  80. "import androidx.compose.runtime.State" in text ||
  81. "import androidx.compose.runtime.MutableState" in text)
  82. ) {
  83. violations += "$displayPath: 配置门面禁止持有 Compose State"
  84. }
  85. if (relativePath !=
  86. "io/legado/app/data/repository/ReadBookStyleConfigRepository.kt" &&
  87. (readBookConfigWrite.containsMatchIn(text) ||
  88. readBookConfigMutationCall.containsMatchIn(text))
  89. ) {
  90. violations += "$displayPath: ReadBookConfig 写入必须经过 ReadStyleGateway"
  91. }
  92. if (relativePath in injectedConfigFiles && "GlobalContext" in text) {
  93. violations += "$displayPath: 配置所有者必须显式注入依赖,禁止 GlobalContext"
  94. }
  95. if (settingsUpdateDeclaration.containsMatchIn(text)) {
  96. violations += "$displayPath: 设置网关禁止重新引入 *SettingsUpdate 分发类型"
  97. }
  98. if (relativePath.startsWith("io/legado/app/domain/gateway/") &&
  99. file.name.endsWith("SettingsGateway.kt") &&
  100. updateAllDeclaration.containsMatchIn(text)
  101. ) {
  102. violations += "$displayPath: 设置网关批量修改必须使用单次 update { copy(...) }"
  103. }
  104. val preferenceCalls = preferenceCall.findAll(text).count()
  105. val allowedCalls = preferenceBaseline[relativePath] ?: 0
  106. if (preferenceCalls > allowedCalls) {
  107. violations += "$displayPath: 新增了 ${preferenceCalls - allowedCalls} 个旧偏好调用"
  108. }
  109. if (relativePath.startsWith("io/legado/app/ui/") &&
  110. file.name.contains("ViewModel")
  111. ) {
  112. val daoDependencies = daoImport.findAll(text).count() +
  113. appDbDaoAccess.findAll(text).count()
  114. val allowedDaoDependencies = daoInjectionBaseline[relativePath] ?: 0
  115. if (daoDependencies > allowedDaoDependencies) {
  116. violations += "$displayPath: ViewModel 新增了 ${daoDependencies - allowedDaoDependencies} 个 DAO 直连"
  117. } else if (daoDependencies < allowedDaoDependencies) {
  118. violations += "$displayPath: 已减少 DAO 直连,请将基线从 $allowedDaoDependencies 下调到 $daoDependencies"
  119. }
  120. }
  121. if (relativePath.startsWith("io/legado/app/ui/") &&
  122. !file.name.contains("ViewModel")
  123. ) {
  124. val daoDependencies = daoImport.findAll(text).count() +
  125. appDbDaoAccess.findAll(text).count()
  126. val allowedDaoDependencies = uiDaoAccessBaseline[relativePath] ?: 0
  127. if (daoDependencies > allowedDaoDependencies) {
  128. violations += "$displayPath: UI 层新增了 ${daoDependencies - allowedDaoDependencies} 个 DAO 直连"
  129. } else if (daoDependencies < allowedDaoDependencies) {
  130. violations += "$displayPath: 已减少 DAO 直连,请将基线从 $allowedDaoDependencies 下调到 $daoDependencies"
  131. }
  132. }
  133. }
  134. val sourcePaths = kotlinFiles.mapTo(hashSetOf()) {
  135. it.relativeTo(sourceRootDir).invariantSeparatorsPath
  136. }
  137. (daoInjectionBaseline.keys - sourcePaths).forEach { relativePath ->
  138. violations += "app/src/main/java/$relativePath: 文件已移除,请删除 DAO 直连基线"
  139. }
  140. (uiDaoAccessBaseline.keys - sourcePaths).forEach { relativePath ->
  141. violations += "app/src/main/java/$relativePath: 文件已移除,请删除 UI DAO 直连基线"
  142. }
  143. check(violations.isEmpty()) {
  144. violations.joinToString(prefix = "配置架构护栏失败:\n", separator = "\n")
  145. }
  146. }
  147. }
  148. buildscript {
  149. extra.apply {
  150. set("compile_sdk_version", 36)
  151. set("build_tool_version", "34.0.0")
  152. }
  153. }
  154. plugins {
  155. alias(libs.plugins.compose.compiler) apply false
  156. alias(libs.plugins.android.application) apply false
  157. alias(libs.plugins.android.library) apply false
  158. alias(libs.plugins.kotlin.parcelize) apply false
  159. alias(libs.plugins.kotlin.serialization) apply false
  160. alias(libs.plugins.ksp) apply false
  161. alias(libs.plugins.google.services) apply false
  162. alias(libs.plugins.room) apply false
  163. alias(libs.plugins.download) apply false
  164. alias(libs.plugins.android.test) apply false
  165. alias(libs.plugins.baselineprofile) apply false
  166. }
  167. tasks.register<Delete>("clean") {
  168. delete(rootProject.layout.buildDirectory)
  169. }
  170. val verifyConfigArchitecture = tasks.register<VerifyConfigArchitectureTask>(
  171. "verifyConfigArchitecture"
  172. ) {
  173. group = "verification"
  174. description = "禁止配置架构回退、UI 层(ViewModel 及其它)新增 DAO 直连和新增旧偏好调用"
  175. sourceRoot.set(layout.projectDirectory.dir("app/src/main/java"))
  176. legacyPreferenceCallBaseline.set(
  177. mapOf(
  178. "io/legado/app/App.kt" to 3,
  179. "io/legado/app/base/BaseActivity.kt" to 2,
  180. "io/legado/app/base/BaseService.kt" to 1,
  181. "io/legado/app/data/repository/CoverAlbumRepository.kt" to 4,
  182. "io/legado/app/data/repository/HighlightRuleRepository.kt" to 9,
  183. "io/legado/app/data/repository/HomeDashboardRepository.kt" to 3,
  184. "io/legado/app/data/repository/ReadRecordRepository.kt" to 1,
  185. "io/legado/app/data/repository/SettingsRepository.kt" to 7,
  186. "io/legado/app/help/config/LocalConfig.kt" to 3,
  187. "io/legado/app/help/config/ThemeConfigStore.kt" to 8,
  188. "io/legado/app/help/storage/Restore.kt" to 2,
  189. "io/legado/app/receiver/MediaButtonReceiver.kt" to 2,
  190. "io/legado/app/service/WebService.kt" to 2,
  191. "io/legado/app/ui/association/ImportReplaceRuleDialog.kt" to 1,
  192. "io/legado/app/ui/book/explore/ExploreShowViewModel.kt" to 2,
  193. "io/legado/app/ui/book/read/ReadBookViewModel.kt" to 2,
  194. "io/legado/app/ui/book/readRecord/ReadRecordViewModel.kt" to 1,
  195. "io/legado/app/ui/book/search/SearchViewModel.kt" to 3,
  196. "io/legado/app/ui/config/CheckSourceConfig.kt" to 1,
  197. "io/legado/app/ui/config/otherConfig/OtherConfigViewModel.kt" to 1,
  198. "io/legado/app/ui/replace/ReplaceRuleViewModel.kt" to 2,
  199. "io/legado/app/utils/ContextExtensions.kt" to 12,
  200. "io/legado/app/web/socket/BookSearchWebSocket.kt" to 2,
  201. )
  202. )
  203. legacyDaoInjectionBaseline.set(
  204. mapOf(
  205. "io/legado/app/ui/book/read/ReadBookViewModel.kt" to 52,
  206. )
  207. )
  208. // 非 ViewModel 的 UI 层文件直连 DAO 的历史债,只冻结不修复;
  209. // 清理时逐条下调/删除。护栏会自动要求"减少了就下调基线",防止回退。
  210. legacyUiDaoAccessBaseline.set(
  211. mapOf(
  212. "io/legado/app/ui/association/AddToBookshelfDialog.kt" to 5,
  213. "io/legado/app/ui/association/ImportBookSourceDialog.kt" to 1,
  214. "io/legado/app/ui/association/ImportReplaceRuleDialog.kt" to 1,
  215. "io/legado/app/ui/association/ImportRssSourceDialog.kt" to 1,
  216. "io/legado/app/ui/book/audio/AudioPlayActivity.kt" to 1,
  217. "io/legado/app/ui/book/bookmark/BookmarkDialog.kt" to 2,
  218. "io/legado/app/ui/book/changesource/ChangeBookSourceDialog.kt" to 1,
  219. "io/legado/app/ui/book/group/GroupManageDialog.kt" to 2,
  220. "io/legado/app/ui/book/group/GroupSelectDialog.kt" to 1,
  221. "io/legado/app/ui/book/manga/ReadMangaActivity.kt" to 1,
  222. "io/legado/app/ui/book/read/ReadBookController.kt" to 3,
  223. "io/legado/app/ui/book/read/page/provider/TextChapterLayout.kt" to 1,
  224. "io/legado/app/ui/book/search/SearchScope.kt" to 4,
  225. "io/legado/app/ui/book/search/SearchScopeDialog.kt" to 3,
  226. "io/legado/app/ui/book/source/debug/BookSourceDebugModel.kt" to 1,
  227. "io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt" to 1,
  228. "io/legado/app/ui/book/source/manage/BookSourceActivity.kt" to 10,
  229. "io/legado/app/ui/book/source/manage/GroupManageDialog.kt" to 1,
  230. "io/legado/app/ui/config/bookshelfConfig/BookshelfManageScreenConfig.kt" to 1,
  231. "io/legado/app/ui/main/MainNavGraph.kt" to 2,
  232. "io/legado/app/ui/rss/article/RssArticlesCompose.kt" to 1,
  233. "io/legado/app/ui/rss/read/RssJsExtensions.kt" to 8,
  234. "io/legado/app/ui/rss/source/debug/RssSourceDebugModel.kt" to 1,
  235. "io/legado/app/ui/widget/dialog/BottomWebViewDialog.kt" to 1,
  236. "io/legado/app/ui/widget/keyboard/KeyboardAssistsConfig.kt" to 7,
  237. "io/legado/app/ui/widget/keyboard/KeyboardToolPop.kt" to 1,
  238. )
  239. )
  240. }
  241. subprojects {
  242. tasks.configureEach {
  243. if (name.startsWith("assemble") || name.startsWith("compile")) {
  244. dependsOn(verifyConfigArchitecture)
  245. }
  246. }
  247. }