1 /*
2  * Copyright 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 @file:Suppress("UnstableApiUsage")
18 
19 package androidx.compose.lint
20 
21 import androidx.collection.MutableObjectList
22 import androidx.collection.MutableScatterMap
23 import androidx.collection.MutableScatterSet
24 import androidx.collection.ObjectList
25 import androidx.collection.ScatterMap
26 import androidx.collection.ScatterSet
27 import androidx.collection.scatterSetOf
28 import com.android.tools.lint.client.api.UElementHandler
29 import com.android.tools.lint.detector.api.Category
30 import com.android.tools.lint.detector.api.Detector
31 import com.android.tools.lint.detector.api.Implementation
32 import com.android.tools.lint.detector.api.Issue
33 import com.android.tools.lint.detector.api.JavaContext
34 import com.android.tools.lint.detector.api.Scope
35 import com.android.tools.lint.detector.api.Severity
36 import com.android.tools.lint.detector.api.SourceCodeScanner
37 import com.intellij.psi.impl.source.PsiClassReferenceType
38 import java.util.EnumSet
39 import org.jetbrains.uast.UCallExpression
40 import org.jetbrains.uast.UElement
41 
42 /**
43  * Using [ScatterMap.asMap], [ScatterSet.asSet], [ObjectList.asList], or their mutable counterparts
44  * indicates that the developer may be using the collection incorrectly. Using the interfaces is
45  * slower access. It is best to use those only for when it touches public API.
46  */
47 class AsCollectionDetector : Detector(), SourceCodeScanner {
getApplicableUastTypesnull48     override fun getApplicableUastTypes() = listOf<Class<out UElement>>(UCallExpression::class.java)
49 
50     override fun createUastHandler(context: JavaContext) =
51         object : UElementHandler() {
52             override fun visitCallExpression(node: UCallExpression) {
53                 val methodName = node.methodName ?: return
54                 if (methodName in MethodNames) {
55                     val receiverType = node.receiverType as? PsiClassReferenceType ?: return
56                     val qualifiedName = receiverType.canonicalText
57                     val indexOfAngleBracket = qualifiedName.indexOf('<')
58                     if (
59                         indexOfAngleBracket > 0 &&
60                             qualifiedName.substring(0, indexOfAngleBracket) in CollectionClasses
61                     ) {
62                         context.report(
63                             ISSUE,
64                             node,
65                             context.getLocation(node),
66                             "Use method $methodName() only for public API usage"
67                         )
68                     }
69                 }
70             }
71         }
72 
73     companion object {
74         private val MethodNames =
75             scatterSetOf(
76                 "asMap",
77                 "asMutableMap",
78                 "asSet",
79                 "asMutableSet",
80                 "asList",
81                 "asMutableList"
82             )
83         private val CollectionClasses =
84             scatterSetOf(
85                 ScatterMap::class.qualifiedName,
86                 MutableScatterMap::class.qualifiedName,
87                 ScatterSet::class.qualifiedName,
88                 MutableScatterSet::class.qualifiedName,
89                 ObjectList::class.qualifiedName,
90                 MutableObjectList::class.qualifiedName,
91             )
92 
93         private val AsCollectionDetectorId = "AsCollectionCall"
94 
95         val ISSUE =
96             Issue.create(
97                 id = AsCollectionDetectorId,
98                 briefDescription =
99                     "High performance collections don't implement standard collection " +
100                         "interfaces so that they can remain high performance. Converting to standard " +
101                         "collections wraps the classes with another object. Use these interface " +
102                         "wrappers only for exposing to public API.",
103                 explanation =
104                     "ScatterMap, ScatterSet, and AnyList are written for high " +
105                         "performance access. Using the standard collection interfaces for these classes " +
106                         "forces slower performance access to these collections. The methods returning " +
107                         "these interfaces should be limited to public API, where standard collection " +
108                         "interfaces are expected.",
109                 category = Category.PERFORMANCE,
110                 priority = 3,
111                 severity = Severity.ERROR,
112                 implementation =
113                     Implementation(AsCollectionDetector::class.java, EnumSet.of(Scope.JAVA_FILE))
114             )
115     }
116 }
117