1+ /*
2+ * Copyright (c) 2014-2025 Stream.io Inc. All rights reserved.
3+ *
4+ * Licensed under the Stream 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+ * https://github.com/GetStream/stream-core-android/blob/main/LICENSE
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+ */
116package io.getstream.android.core.lint.detectors
217
318import com.android.tools.lint.client.api.UElementHandler
@@ -8,38 +23,38 @@ import org.jetbrains.uast.*
823
924class StreamApiExplicitMarkerDetector : Detector (), Detector.UastScanner {
1025
11- override fun getApplicableUastTypes () = listOf (
12- UClass ::class .java, UMethod ::class .java, UField ::class .java, UFile ::class .java
13- )
26+ override fun getApplicableUastTypes () =
27+ listOf (UClass ::class .java, UMethod ::class .java, UField ::class .java, UFile ::class .java)
1428
15- override fun createUastHandler (context : JavaContext ) = object : UElementHandler () {
16- override fun visitClass (node : UClass ) {
17- val uFile = node.getContainingUFileOrNull() ? : return
18- if (! context.packageMatchesConfig(uFile.packageName)) return
19- checkUAnnotated(context, node, node.sourcePsi as ? KtDeclaration )
20- }
29+ override fun createUastHandler (context : JavaContext ) =
30+ object : UElementHandler () {
31+ override fun visitClass (node : UClass ) {
32+ val uFile = node.getContainingUFileOrNull() ? : return
33+ if (! context.packageMatchesConfig(uFile.packageName)) return
34+ checkUAnnotated(context, node, node.sourcePsi as ? KtDeclaration )
35+ }
2136
22- override fun visitMethod (node : UMethod ) {
23- val uFile = node.getContainingUFileOrNull() ? : return
24- if (! context.packageMatchesConfig(uFile.packageName)) return
25- checkUAnnotated(context, node, node.sourcePsi as ? KtNamedFunction )
26- }
37+ override fun visitMethod (node : UMethod ) {
38+ val uFile = node.getContainingUFileOrNull() ? : return
39+ if (! context.packageMatchesConfig(uFile.packageName)) return
40+ checkUAnnotated(context, node, node.sourcePsi as ? KtNamedFunction )
41+ }
2742
28- override fun visitField (node : UField ) {
29- val uFile = node.getContainingUFileOrNull() ? : return
30- if (! context.packageMatchesConfig(uFile.packageName)) return
31- checkUAnnotated(context, node, node.sourcePsi as ? KtProperty )
32- }
43+ override fun visitField (node : UField ) {
44+ val uFile = node.getContainingUFileOrNull() ? : return
45+ if (! context.packageMatchesConfig(uFile.packageName)) return
46+ checkUAnnotated(context, node, node.sourcePsi as ? KtProperty )
47+ }
3348
34- override fun visitFile (node : UFile ) {
35- val ktFile = node.sourcePsi as ? KtFile ? : return
36- val pkg = ktFile.packageFqName.asString()
37- if (! context.packageMatchesConfig(pkg)) return
38- ktFile.declarations.filterIsInstance<KtTypeAlias >().forEach { alias ->
39- checkTypeAlias(context, alias)
49+ override fun visitFile (node : UFile ) {
50+ val ktFile = node.sourcePsi as ? KtFile ? : return
51+ val pkg = ktFile.packageFqName.asString()
52+ if (! context.packageMatchesConfig(pkg)) return
53+ ktFile.declarations.filterIsInstance<KtTypeAlias >().forEach { alias ->
54+ checkTypeAlias(context, alias)
55+ }
4056 }
4157 }
42- }
4358
4459 private fun checkUAnnotated (context : JavaContext , u : UElement , kt : KtDeclaration ? ) {
4560 kt ? : return
@@ -58,33 +73,69 @@ class StreamApiExplicitMarkerDetector : Detector(), Detector.UastScanner {
5873
5974 private fun reportWithDualFix (context : JavaContext , node : KtTypeAlias , decl : KtDeclaration ) {
6075 val loc = context.getLocation(decl)
61- val fixPublished = LintFix .create().name(" Annotate with @$PUBLISHED_SIMPLE " )
62- .replace().range(loc).pattern(" ^" ).with (" @$PUBLISHED_FQ \n " )
63- .reformat(true ).shortenNames().autoFix().build()
64- val fixInternal = LintFix .create().name(" Annotate with @$INTERNAL_SIMPLE " )
65- .replace().range(loc).pattern(" ^" ).with (" @$INTERNAL_FQ \n " )
66- .reformat(true ).shortenNames().autoFix().build()
76+ val fixPublished =
77+ LintFix .create()
78+ .name(" Annotate with @$PUBLISHED_SIMPLE " )
79+ .replace()
80+ .range(loc)
81+ .pattern(" ^" )
82+ .with (" @$PUBLISHED_FQ \n " )
83+ .reformat(true )
84+ .shortenNames()
85+ .autoFix()
86+ .build()
87+ val fixInternal =
88+ LintFix .create()
89+ .name(" Annotate with @$INTERNAL_SIMPLE " )
90+ .replace()
91+ .range(loc)
92+ .pattern(" ^" )
93+ .with (" @$INTERNAL_FQ \n " )
94+ .reformat(true )
95+ .shortenNames()
96+ .autoFix()
97+ .build()
6798
6899 context.report(
69- ISSUE , node, loc,
100+ ISSUE ,
101+ node,
102+ loc,
70103 " Public API must be explicitly marked with @$PUBLISHED_SIMPLE or @$INTERNAL_SIMPLE ." ,
71- LintFix .create().group(fixPublished, fixInternal)
104+ LintFix .create().group(fixPublished, fixInternal),
72105 )
73106 }
74107
75108 private fun reportWithDualFix (context : JavaContext , node : UElement , decl : KtDeclaration ) {
76109 val loc = context.getLocation(decl)
77- val fixPublished = LintFix .create().name(" Annotate with @$PUBLISHED_SIMPLE " )
78- .replace().range(loc).pattern(" ^" ).with (" @$PUBLISHED_FQ \n " )
79- .reformat(true ).shortenNames().autoFix().build()
80- val fixInternal = LintFix .create().name(" Annotate with @$INTERNAL_SIMPLE " )
81- .replace().range(loc).pattern(" ^" ).with (" @$INTERNAL_FQ \n " )
82- .reformat(true ).shortenNames().autoFix().build()
110+ val fixPublished =
111+ LintFix .create()
112+ .name(" Annotate with @$PUBLISHED_SIMPLE " )
113+ .replace()
114+ .range(loc)
115+ .pattern(" ^" )
116+ .with (" @$PUBLISHED_FQ \n " )
117+ .reformat(true )
118+ .shortenNames()
119+ .autoFix()
120+ .build()
121+ val fixInternal =
122+ LintFix .create()
123+ .name(" Annotate with @$INTERNAL_SIMPLE " )
124+ .replace()
125+ .range(loc)
126+ .pattern(" ^" )
127+ .with (" @$INTERNAL_FQ \n " )
128+ .reformat(true )
129+ .shortenNames()
130+ .autoFix()
131+ .build()
83132
84133 context.report(
85- ISSUE , node, loc,
134+ ISSUE ,
135+ node,
136+ loc,
86137 " Public API must be explicitly marked with @$PUBLISHED_SIMPLE or @$INTERNAL_SIMPLE ." ,
87- LintFix .create().group(fixPublished, fixInternal)
138+ LintFix .create().group(fixPublished, fixInternal),
88139 )
89140 }
90141
@@ -94,16 +145,13 @@ class StreamApiExplicitMarkerDetector : Detector(), Detector.UastScanner {
94145 val patterns = configuredPackageGlobs()
95146
96147 // Default if not configured → only io.getstream.android.core.*
97- val effectivePatterns = patterns.ifEmpty {
98- listOf (" io.getstream.android.core.api" )
99- }
148+ val effectivePatterns = patterns.ifEmpty { listOf (" io.getstream.android.core.api" ) }
100149
101150 val included = effectivePatterns.any { pkgMatchesGlob(pkg, it) }
102151 val excluded = packageMatchesExcludeConfig(pkg)
103152 return included && ! excluded
104153 }
105154
106-
107155 private fun JavaContext.packageMatchesExcludeConfig (pkg : String ): Boolean {
108156 val raw = configuration.getOption(ISSUE , OPTION_PACKAGES_EXCLUDE .name, " " )?.trim().orEmpty()
109157 if (raw.isEmpty()) return false
@@ -142,7 +190,7 @@ class StreamApiExplicitMarkerDetector : Detector(), Detector.UastScanner {
142190 private fun KtAnnotated.hasAnyAnnotationPsi (simpleNames : Set <String >): Boolean =
143191 annotationEntries.any { entry ->
144192 entry.shortName?.asString() in simpleNames ||
145- entry.typeReference?.text in simpleNames // handles rare fully-qualified usage
193+ entry.typeReference?.text in simpleNames // handles rare fully-qualified usage
146194 }
147195
148196 private fun UElement.getContainingUFileOrNull (): UFile ? {
@@ -157,10 +205,11 @@ class StreamApiExplicitMarkerDetector : Detector(), Detector.UastScanner {
157205 private fun KtDeclaration.isTopLevelPublic (): Boolean {
158206 if (parent !is KtFile ) return false
159207 val mods = modifierList
160- val isPublic = mods?.hasModifier(KtTokens .PUBLIC_KEYWORD ) == true ||
208+ val isPublic =
209+ mods?.hasModifier(KtTokens .PUBLIC_KEYWORD ) == true ||
161210 ! (mods?.hasModifier(KtTokens .PRIVATE_KEYWORD ) == true ||
162- mods?.hasModifier(KtTokens .PROTECTED_KEYWORD ) == true ||
163- mods?.hasModifier(KtTokens .INTERNAL_KEYWORD ) == true )
211+ mods?.hasModifier(KtTokens .PROTECTED_KEYWORD ) == true ||
212+ mods?.hasModifier(KtTokens .INTERNAL_KEYWORD ) == true )
164213 return isPublic
165214 }
166215
@@ -172,40 +221,50 @@ class StreamApiExplicitMarkerDetector : Detector(), Detector.UastScanner {
172221 private val MARKERS = setOf (PUBLISHED_FQ , INTERNAL_FQ )
173222 private val MARKERS_SIMPLE = setOf (PUBLISHED_SIMPLE , INTERNAL_SIMPLE )
174223
175- private val OPTION_PACKAGES = StringOption (
176- name = " packages" ,
177- description = " Comma-separated package **glob** patterns where the rule applies." ,
178- explanation = """
224+ private val OPTION_PACKAGES =
225+ StringOption (
226+ name = " packages" ,
227+ description = " Comma-separated package **glob** patterns where the rule applies." ,
228+ explanation =
229+ """
179230 Supports wildcards: '*' (any sequence) and '?' (single char).
180231 Examples:
181232 - 'io.getstream.android.core.api'
182233 - 'io.getstream.android.core.*.api'
183234 - 'io.getstream.android.*'
184- """ .trimIndent()
185- )
186-
187- private val OPTION_PACKAGES_EXCLUDE = StringOption (
188- name = " exclude_packages" ,
189- description = " Comma-separated package **glob** patterns to exclude from the rule." ,
190- explanation = """
235+ """
236+ .trimIndent(),
237+ )
238+
239+ private val OPTION_PACKAGES_EXCLUDE =
240+ StringOption (
241+ name = " exclude_packages" ,
242+ description = " Comma-separated package **glob** patterns to exclude from the rule." ,
243+ explanation =
244+ """
191245 Same glob syntax as 'packages'. Evaluated after includes.
192- """ .trimIndent()
193- )
246+ """
247+ .trimIndent(),
248+ )
194249
195- private val IMPLEMENTATION = Implementation (
196- StreamApiExplicitMarkerDetector ::class .java,
197- Scope .JAVA_FILE_SCOPE
198- )
250+ private val IMPLEMENTATION =
251+ Implementation (StreamApiExplicitMarkerDetector ::class .java, Scope .JAVA_FILE_SCOPE )
199252
200253 @JvmField
201- val ISSUE : Issue = Issue .create(
202- " StreamApiExplicitMarkerMissing" ,
203- " Public API must be explicitly marked" ,
204- """
254+ val ISSUE : Issue =
255+ Issue .create(
256+ " StreamApiExplicitMarkerMissing" ,
257+ " Public API must be explicitly marked" ,
258+ """
205259 To prevent accidental exposure, all top-level public declarations must be explicitly \
206260 marked as @StreamPublishedApi (allowed to leak) or @StreamInternalApi (not allowed to leak).
207- """ .trimIndent(),
208- Category .CORRECTNESS , 7 , Severity .ERROR , IMPLEMENTATION
209- ).setOptions(listOf (OPTION_PACKAGES , OPTION_PACKAGES_EXCLUDE ))
261+ """
262+ .trimIndent(),
263+ Category .CORRECTNESS ,
264+ 7 ,
265+ Severity .ERROR ,
266+ IMPLEMENTATION ,
267+ )
268+ .setOptions(listOf (OPTION_PACKAGES , OPTION_PACKAGES_EXCLUDE ))
210269 }
211270}
0 commit comments