build.gradle.kts 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  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. @TaskAction
  18. fun verify() {
  19. val sourceRootDir = sourceRoot.get().asFile
  20. val kotlinFiles = sourceRootDir.walkTopDown()
  21. .filter { it.isFile && it.extension == "kt" }
  22. .toList()
  23. val preferenceBaseline = legacyPreferenceCallBaseline.get()
  24. val violations = mutableListOf<String>()
  25. val forbiddenConfigImport = Regex(
  26. """^import io\.legado\.app\.(?:help\.config\.AppConfig|ui\.config\..*Config)$""",
  27. RegexOption.MULTILINE,
  28. )
  29. val preferenceCall = Regex("""\b(?:getPref|putPref)[A-Za-z0-9_]*\s*\(""")
  30. val readBookConfigWrite = Regex(
  31. """\bReadBookConfig\.[a-z_][A-Za-z0-9_]*(?:\.[a-z_][A-Za-z0-9_]*)?\s*="""
  32. )
  33. val readBookConfigMutationCall = Regex(
  34. """\bReadBookConfig\.durConfig\.set[A-Za-z0-9_]*\s*\("""
  35. )
  36. val settingsUpdateDeclaration = Regex(
  37. """\b(?:class|interface|object|typealias)\s+[A-Za-z0-9_]*SettingsUpdate\b"""
  38. )
  39. val updateAllDeclaration = Regex("""\bfun\s+(?:<[^>\n]+>\s*)?updateAll\s*\(""")
  40. val injectedConfigFiles = setOf(
  41. "io/legado/app/help/config/AppConfig.kt",
  42. "io/legado/app/help/config/ReadBookConfig.kt",
  43. "io/legado/app/help/config/ThemePackageManager.kt",
  44. )
  45. kotlinFiles.forEach { file ->
  46. val text = file.readText()
  47. val relativePath = file.relativeTo(sourceRootDir).invariantSeparatorsPath
  48. val displayPath = "app/src/main/java/$relativePath"
  49. if ("prefDelegate" in text || "prefStateDelegate" in text ||
  50. "Snapshot.withMutableSnapshot" in text
  51. ) {
  52. violations += "$displayPath: 禁止 Snapshot 配置桥"
  53. }
  54. if ((relativePath.startsWith("io/legado/app/data/") ||
  55. relativePath.startsWith("io/legado/app/domain/")) &&
  56. forbiddenConfigImport.containsMatchIn(text)
  57. ) {
  58. violations += "$displayPath: data/domain 禁止导入全局 Config"
  59. }
  60. if (("@Composable" in text || "import androidx.compose" in text) &&
  61. forbiddenConfigImport.containsMatchIn(text)
  62. ) {
  63. violations += "$displayPath: Composable 禁止读取兼容 Config"
  64. }
  65. if (file.name.endsWith("Config.kt") &&
  66. ("mutableStateOf(" in text || "Snapshot.withMutableSnapshot" in text ||
  67. "import androidx.compose.runtime.State" in text ||
  68. "import androidx.compose.runtime.MutableState" in text)
  69. ) {
  70. violations += "$displayPath: 配置门面禁止持有 Compose State"
  71. }
  72. if (relativePath !=
  73. "io/legado/app/data/repository/ReadBookStyleConfigRepository.kt" &&
  74. (readBookConfigWrite.containsMatchIn(text) ||
  75. readBookConfigMutationCall.containsMatchIn(text))
  76. ) {
  77. violations += "$displayPath: ReadBookConfig 写入必须经过 ReadStyleGateway"
  78. }
  79. if (relativePath in injectedConfigFiles && "GlobalContext" in text) {
  80. violations += "$displayPath: 配置所有者必须显式注入依赖,禁止 GlobalContext"
  81. }
  82. if (settingsUpdateDeclaration.containsMatchIn(text)) {
  83. violations += "$displayPath: 设置网关禁止重新引入 *SettingsUpdate 分发类型"
  84. }
  85. if (relativePath.startsWith("io/legado/app/domain/gateway/") &&
  86. file.name.endsWith("SettingsGateway.kt") &&
  87. updateAllDeclaration.containsMatchIn(text)
  88. ) {
  89. violations += "$displayPath: 设置网关批量修改必须使用单次 update { copy(...) }"
  90. }
  91. val preferenceCalls = preferenceCall.findAll(text).count()
  92. val allowedCalls = preferenceBaseline[relativePath] ?: 0
  93. if (preferenceCalls > allowedCalls) {
  94. violations += "$displayPath: 新增了 ${preferenceCalls - allowedCalls} 个旧偏好调用"
  95. }
  96. }
  97. check(violations.isEmpty()) {
  98. violations.joinToString(prefix = "配置架构护栏失败:\n", separator = "\n")
  99. }
  100. }
  101. }
  102. buildscript {
  103. extra.apply {
  104. set("compile_sdk_version", 36)
  105. set("build_tool_version", "34.0.0")
  106. }
  107. }
  108. plugins {
  109. alias(libs.plugins.compose.compiler) apply false
  110. alias(libs.plugins.android.application) apply false
  111. alias(libs.plugins.android.library) apply false
  112. alias(libs.plugins.kotlin.parcelize) apply false
  113. alias(libs.plugins.kotlin.serialization) apply false
  114. alias(libs.plugins.ksp) apply false
  115. alias(libs.plugins.google.services) apply false
  116. alias(libs.plugins.room) apply false
  117. alias(libs.plugins.download) apply false
  118. alias(libs.plugins.android.test) apply false
  119. alias(libs.plugins.baselineprofile) apply false
  120. }
  121. tasks.register<Delete>("clean") {
  122. delete(rootProject.layout.buildDirectory)
  123. }
  124. val verifyConfigArchitecture = tasks.register<VerifyConfigArchitectureTask>(
  125. "verifyConfigArchitecture"
  126. ) {
  127. group = "verification"
  128. description = "禁止配置 Snapshot 桥、SettingsUpdate 分发、跨层全局 Config 直读和新增旧偏好调用"
  129. sourceRoot.set(layout.projectDirectory.dir("app/src/main/java"))
  130. legacyPreferenceCallBaseline.set(
  131. mapOf(
  132. "io/legado/app/App.kt" to 3,
  133. "io/legado/app/base/BaseActivity.kt" to 2,
  134. "io/legado/app/base/BaseService.kt" to 1,
  135. "io/legado/app/data/repository/CoverAlbumRepository.kt" to 4,
  136. "io/legado/app/data/repository/HighlightRuleRepository.kt" to 9,
  137. "io/legado/app/data/repository/HomeDashboardRepository.kt" to 3,
  138. "io/legado/app/data/repository/ReadRecordRepository.kt" to 1,
  139. "io/legado/app/data/repository/SettingsRepository.kt" to 7,
  140. "io/legado/app/help/config/LocalConfig.kt" to 3,
  141. "io/legado/app/help/config/ThemeConfigStore.kt" to 8,
  142. "io/legado/app/help/storage/Restore.kt" to 2,
  143. "io/legado/app/receiver/MediaButtonReceiver.kt" to 2,
  144. "io/legado/app/service/WebService.kt" to 2,
  145. "io/legado/app/ui/association/ImportReplaceRuleDialog.kt" to 1,
  146. "io/legado/app/ui/book/explore/ExploreShowViewModel.kt" to 2,
  147. "io/legado/app/ui/book/read/ReadBookViewModel.kt" to 2,
  148. "io/legado/app/ui/book/readRecord/ReadRecordViewModel.kt" to 1,
  149. "io/legado/app/ui/book/search/SearchViewModel.kt" to 3,
  150. "io/legado/app/ui/config/CheckSourceConfig.kt" to 1,
  151. "io/legado/app/ui/config/otherConfig/OtherConfigViewModel.kt" to 1,
  152. "io/legado/app/ui/replace/ReplaceRuleViewModel.kt" to 2,
  153. "io/legado/app/utils/ContextExtensions.kt" to 12,
  154. "io/legado/app/web/socket/BookSearchWebSocket.kt" to 2,
  155. )
  156. )
  157. }
  158. subprojects {
  159. tasks.configureEach {
  160. if (name.startsWith("assemble") || name.startsWith("compile")) {
  161. dependsOn(verifyConfigArchitecture)
  162. }
  163. }
  164. }