1 /* <lambda>null2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 * 16 */ 17 18 package com.android.internal.systemui.lint 19 20 import com.android.tools.lint.client.api.UElementHandler 21 import com.android.tools.lint.detector.api.Category 22 import com.android.tools.lint.detector.api.Detector 23 import com.android.tools.lint.detector.api.Implementation 24 import com.android.tools.lint.detector.api.Issue 25 import com.android.tools.lint.detector.api.JavaContext 26 import com.android.tools.lint.detector.api.Scope 27 import com.android.tools.lint.detector.api.Severity 28 import org.jetbrains.uast.UElement 29 import org.jetbrains.uast.UFile 30 import org.jetbrains.uast.UImportStatement 31 32 /** 33 * Detects violations of the Dependency Rule of Clean Architecture. 34 * 35 * The rule states that code in each layer may only depend on code in the same layer or the layer 36 * directly "beneath" that layer in the layer diagram. 37 * 38 * In System UI, we have three layers; from top to bottom, they are: ui, domain, and data. As a 39 * convention, was used packages with those names to place code in the appropriate layer. We also 40 * make an exception and allow for shared models to live under a separate package named "shared" to 41 * avoid code duplication. 42 * 43 * For more information, please see go/sysui-arch. 44 */ 45 @Suppress("UnstableApiUsage") 46 class CleanArchitectureDependencyViolationDetector : Detector(), Detector.UastScanner { 47 override fun getApplicableUastTypes(): List<Class<out UElement>> { 48 return listOf(UFile::class.java) 49 } 50 51 override fun createUastHandler(context: JavaContext): UElementHandler { 52 return object : UElementHandler() { 53 override fun visitFile(node: UFile) { 54 // Check which Clean Architecture layer this file belongs to: 55 matchingLayer(node.packageName)?.let { layer -> 56 // The file matches with a Clean Architecture layer. Let's check all of its 57 // imports. 58 node.imports.forEach { importStatement -> 59 visitImportStatement(context, layer, importStatement) 60 } 61 } 62 } 63 } 64 } 65 66 private fun visitImportStatement( 67 context: JavaContext, 68 layer: Layer, 69 importStatement: UImportStatement, 70 ) { 71 val importText = importStatement.importReference?.asSourceString() ?: return 72 val importedLayer = matchingLayer(importText) ?: return 73 74 // Now check whether the layer of the file may depend on the layer of the import. 75 if (!layer.mayDependOn(importedLayer)) { 76 context.report( 77 issue = ISSUE, 78 scope = importStatement, 79 location = context.getLocation(importStatement), 80 message = 81 "The ${layer.packageNamePart} layer may not depend on" + 82 " the ${importedLayer.packageNamePart} layer.", 83 ) 84 } 85 } 86 87 private fun matchingLayer(packageName: String): Layer? { 88 val packageNameParts = packageName.split(".").toSet() 89 return Layer.values() 90 .filter { layer -> packageNameParts.contains(layer.packageNamePart) } 91 .takeIf { it.size == 1 } 92 ?.first() 93 } 94 95 private enum class Layer( 96 val packageNamePart: String, 97 val canDependOn: Set<Layer>, 98 ) { 99 SHARED( 100 packageNamePart = "shared", 101 canDependOn = emptySet(), // The shared layer may not depend on any other layer. 102 ), 103 DATA( 104 packageNamePart = "data", 105 canDependOn = setOf(SHARED), 106 ), 107 DOMAIN( 108 packageNamePart = "domain", 109 canDependOn = setOf(SHARED, DATA), 110 ), 111 UI( 112 packageNamePart = "ui", 113 canDependOn = setOf(DOMAIN, SHARED), 114 ), 115 ; 116 117 fun mayDependOn(otherLayer: Layer): Boolean { 118 return this == otherLayer || canDependOn.contains(otherLayer) 119 } 120 } 121 122 companion object { 123 @JvmStatic 124 val ISSUE = 125 Issue.create( 126 id = "CleanArchitectureDependencyViolation", 127 briefDescription = "Violation of the Clean Architecture Dependency Rule.", 128 explanation = 129 """ 130 Following the \"Dependency Rule\" from Clean Architecture, every layer of code \ 131 can only depend code in its own layer or code in the layer directly \ 132 \"beneath\" it. Therefore, the UI layer can only depend on the" Domain layer \ 133 and the Domain layer can only depend on the Data layer. We" do make an \ 134 exception to allow shared models to exist and be shared across layers by \ 135 placing them under shared/model, which should be done with care. For more \ 136 information about Clean Architecture in System UI, please see go/sysui-arch. \ 137 NOTE: if your code is not using Clean Architecture, please feel free to ignore \ 138 this warning. 139 """, 140 category = Category.CORRECTNESS, 141 priority = 8, 142 severity = Severity.WARNING, 143 implementation = 144 Implementation( 145 CleanArchitectureDependencyViolationDetector::class.java, 146 Scope.JAVA_FILE_SCOPE, 147 ), 148 ) 149 } 150 } 151