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