pumuki 6.3.127 → 6.3.129
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/VERSION +1 -1
- package/core/facts/detectors/text/android.test.ts +268 -0
- package/core/facts/detectors/text/android.ts +340 -0
- package/core/facts/extractHeuristicFacts.ts +29 -0
- package/core/rules/presets/heuristics/android.test.ts +56 -1
- package/core/rules/presets/heuristics/android.ts +200 -0
- package/docs/operations/RELEASE_NOTES.md +6 -0
- package/docs/product/CONFIGURATION.md +8 -0
- package/integrations/config/skillsCompilerTemplates.test.ts +14 -0
- package/integrations/config/skillsCompilerTemplates.ts +27 -0
- package/integrations/config/skillsDetectorRegistry.ts +36 -0
- package/integrations/config/skillsMarkdownRules.ts +177 -7
- package/integrations/config/skillsRuleSet.ts +54 -29
- package/integrations/evidence/rulesCoverage.ts +54 -0
- package/integrations/evidence/schema.ts +16 -0
- package/integrations/git/runPlatformGate.ts +55 -4
- package/integrations/lifecycle/audit.ts +51 -1
- package/integrations/policy/gitAtomicityEnforcement.ts +2 -2
- package/integrations/policy/heuristicsEnforcement.ts +2 -2
- package/integrations/policy/sddCompletenessEnforcement.ts +2 -2
- package/integrations/policy/skillsEnforcement.ts +2 -2
- package/integrations/policy/tddBddEnforcement.ts +2 -2
- package/package.json +1 -1
- package/scripts/framework-menu-consumer-actions-lib.ts +1 -1
- package/skills.lock.json +146 -527
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,14 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [6.3.129] - 2026-04-29
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **Nueva slice Android de singletons cerrada:** `skills.android.no-singleton-usar-inyeccio-n-de-dependencias-hilt-dagger` pasa a detector AST real y deja de depender de normalización genérica.
|
|
14
|
+
- **Exclusión correcta de módulos DI:** `@Module`, `@InstallIn` y `@EntryPoint` ya no disparan el detector de singleton cuando el `object` es un módulo de inyección legítimo.
|
|
15
|
+
- **Cobertura de regresión y lock recompilado:** la suite Android dirigida vuelve a verde y `skills.lock.json` se regenera con el binding canónico de la nueva skill.
|
|
16
|
+
|
|
9
17
|
## [6.3.127] - 2026-04-28
|
|
10
18
|
|
|
11
19
|
### Fixed
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
v6.3.
|
|
1
|
+
v6.3.128
|
|
@@ -6,6 +6,17 @@ import {
|
|
|
6
6
|
findKotlinLiskovSubstitutionMatch,
|
|
7
7
|
findKotlinOpenClosedWhenMatch,
|
|
8
8
|
findKotlinPresentationSrpMatch,
|
|
9
|
+
hasAndroidAsyncTaskUsage,
|
|
10
|
+
hasAndroidFindViewByIdUsage,
|
|
11
|
+
hasAndroidJavaSourceCode,
|
|
12
|
+
hasAndroidDispatcherUsage,
|
|
13
|
+
hasAndroidCoroutineTryCatchUsage,
|
|
14
|
+
hasAndroidHardcodedStringUsage,
|
|
15
|
+
hasAndroidNoConsoleLogUsage,
|
|
16
|
+
hasAndroidRxJavaUsage,
|
|
17
|
+
hasAndroidSingletonUsage,
|
|
18
|
+
hasAndroidWithContextUsage,
|
|
19
|
+
hasKotlinForceUnwrapUsage,
|
|
9
20
|
hasKotlinGlobalScopeUsage,
|
|
10
21
|
hasKotlinRunBlockingUsage,
|
|
11
22
|
hasKotlinThreadSleepCall,
|
|
@@ -91,6 +102,263 @@ fun main() {
|
|
|
91
102
|
assert.equal(hasKotlinRunBlockingUsage(partialSource), false);
|
|
92
103
|
});
|
|
93
104
|
|
|
105
|
+
test('hasKotlinForceUnwrapUsage detecta operador !! en codigo Kotlin real', () => {
|
|
106
|
+
const source = `
|
|
107
|
+
fun renderName(user: User?) {
|
|
108
|
+
val name = user!!.name
|
|
109
|
+
println(name)
|
|
110
|
+
}
|
|
111
|
+
`;
|
|
112
|
+
assert.equal(hasKotlinForceUnwrapUsage(source), true);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('hasKotlinForceUnwrapUsage ignora comentarios, strings y operador !=', () => {
|
|
116
|
+
const source = `
|
|
117
|
+
// val name = user!!.name
|
|
118
|
+
val debug = "user!!.name"
|
|
119
|
+
fun isDifferent(left: String?, right: String?) = left != right
|
|
120
|
+
`;
|
|
121
|
+
assert.equal(hasKotlinForceUnwrapUsage(source), false);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('hasAndroidJavaSourceCode detecta codigo Java real', () => {
|
|
125
|
+
const source = `
|
|
126
|
+
package com.acme.orders;
|
|
127
|
+
|
|
128
|
+
public class OrdersActivity {
|
|
129
|
+
}
|
|
130
|
+
`;
|
|
131
|
+
assert.equal(hasAndroidJavaSourceCode(source), true);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('hasAndroidJavaSourceCode ignora menciones Java en comentarios y strings', () => {
|
|
135
|
+
const source = `
|
|
136
|
+
// public class OrdersActivity {}
|
|
137
|
+
val sample = "class OrdersActivity"
|
|
138
|
+
`;
|
|
139
|
+
assert.equal(hasAndroidJavaSourceCode(source), false);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('hasAndroidAsyncTaskUsage detecta AsyncTask real en codigo Android', () => {
|
|
143
|
+
const source = `
|
|
144
|
+
import android.os.AsyncTask
|
|
145
|
+
|
|
146
|
+
class LoadDataTask : AsyncTask<Unit, Unit, String>() {
|
|
147
|
+
override fun doInBackground(vararg params: Unit): String = "ok"
|
|
148
|
+
}
|
|
149
|
+
`;
|
|
150
|
+
assert.equal(hasAndroidAsyncTaskUsage(source), true);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('hasAndroidAsyncTaskUsage ignora comentarios, strings y nombres parciales', () => {
|
|
154
|
+
const source = `
|
|
155
|
+
// AsyncTask should be removed
|
|
156
|
+
val sample = "AsyncTask"
|
|
157
|
+
class AsyncTaskRunner
|
|
158
|
+
`;
|
|
159
|
+
assert.equal(hasAndroidAsyncTaskUsage(source), false);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('hasAndroidFindViewByIdUsage detecta findViewById real en codigo Android', () => {
|
|
163
|
+
const source = `
|
|
164
|
+
class HomeActivity : AppCompatActivity() {
|
|
165
|
+
fun render() {
|
|
166
|
+
val title = findViewById<TextView>(R.id.title)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
`;
|
|
170
|
+
assert.equal(hasAndroidFindViewByIdUsage(source), true);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('hasAndroidFindViewByIdUsage ignora comentarios, strings y nombres parciales', () => {
|
|
174
|
+
const source = `
|
|
175
|
+
// findViewById<TextView>(R.id.title)
|
|
176
|
+
val sample = "findViewById"
|
|
177
|
+
class FindViewByIdHelper
|
|
178
|
+
`;
|
|
179
|
+
assert.equal(hasAndroidFindViewByIdUsage(source), false);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('hasAndroidRxJavaUsage detecta RxJava real en codigo Android', () => {
|
|
183
|
+
const source = `
|
|
184
|
+
import io.reactivex.rxjava3.core.Observable
|
|
185
|
+
|
|
186
|
+
class HomeRepository {
|
|
187
|
+
fun load() = Observable.just("ok")
|
|
188
|
+
}
|
|
189
|
+
`;
|
|
190
|
+
assert.equal(hasAndroidRxJavaUsage(source), true);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('hasAndroidRxJavaUsage ignora comentarios, strings y nombres parciales', () => {
|
|
194
|
+
const source = `
|
|
195
|
+
// Observable.just("ok")
|
|
196
|
+
val sample = "RxJava"
|
|
197
|
+
class ObservableHelper
|
|
198
|
+
`;
|
|
199
|
+
assert.equal(hasAndroidRxJavaUsage(source), false);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('hasAndroidDispatcherUsage detecta Dispatchers reales en codigo Android', () => {
|
|
203
|
+
const source = `
|
|
204
|
+
import kotlinx.coroutines.Dispatchers
|
|
205
|
+
import kotlinx.coroutines.withContext
|
|
206
|
+
|
|
207
|
+
suspend fun load() = withContext(Dispatchers.IO) {
|
|
208
|
+
"ok"
|
|
209
|
+
}
|
|
210
|
+
`;
|
|
211
|
+
assert.equal(hasAndroidDispatcherUsage(source), true);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('hasAndroidDispatcherUsage ignora comentarios, strings y nombres parciales', () => {
|
|
215
|
+
const source = `
|
|
216
|
+
// withContext(Dispatchers.IO)
|
|
217
|
+
val sample = "Dispatchers.Main"
|
|
218
|
+
class DispatchersHelper
|
|
219
|
+
`;
|
|
220
|
+
assert.equal(hasAndroidDispatcherUsage(source), false);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('hasAndroidWithContextUsage detecta withContext real en codigo Android', () => {
|
|
224
|
+
const source = `
|
|
225
|
+
import kotlinx.coroutines.Dispatchers
|
|
226
|
+
import kotlinx.coroutines.withContext
|
|
227
|
+
|
|
228
|
+
suspend fun load() = withContext(Dispatchers.IO) {
|
|
229
|
+
"ok"
|
|
230
|
+
}
|
|
231
|
+
`;
|
|
232
|
+
assert.equal(hasAndroidWithContextUsage(source), true);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test('hasAndroidWithContextUsage ignora comentarios, strings y nombres parciales', () => {
|
|
236
|
+
const source = `
|
|
237
|
+
// withContext(Dispatchers.IO)
|
|
238
|
+
val sample = "withContext"
|
|
239
|
+
class WithContextHelper
|
|
240
|
+
`;
|
|
241
|
+
assert.equal(hasAndroidWithContextUsage(source), false);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test('hasAndroidCoroutineTryCatchUsage detecta try-catch real en codigo coroutine Android', () => {
|
|
245
|
+
const source = `
|
|
246
|
+
import kotlinx.coroutines.Dispatchers
|
|
247
|
+
import kotlinx.coroutines.withContext
|
|
248
|
+
|
|
249
|
+
suspend fun load() {
|
|
250
|
+
try {
|
|
251
|
+
withContext(Dispatchers.IO) { }
|
|
252
|
+
} catch (e: Exception) {
|
|
253
|
+
throw e
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
`;
|
|
257
|
+
assert.equal(hasAndroidCoroutineTryCatchUsage(source), true);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test('hasAndroidCoroutineTryCatchUsage ignora comentarios, strings y codigo no coroutine', () => {
|
|
261
|
+
const source = `
|
|
262
|
+
// try { withContext(Dispatchers.IO) } catch (e: Exception) {}
|
|
263
|
+
val sample = "try catch"
|
|
264
|
+
fun notCoroutine() {
|
|
265
|
+
try {
|
|
266
|
+
println("ok")
|
|
267
|
+
} catch (e: Exception) {
|
|
268
|
+
println(e)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
`;
|
|
272
|
+
assert.equal(hasAndroidCoroutineTryCatchUsage(source), false);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('hasAndroidNoConsoleLogUsage detecta logs reales en codigo Android y permite debug guards', () => {
|
|
276
|
+
const source = `
|
|
277
|
+
import timber.log.Timber
|
|
278
|
+
|
|
279
|
+
fun render() {
|
|
280
|
+
Timber.d("visible in production")
|
|
281
|
+
if (BuildConfig.DEBUG) Timber.d("visible in debug only")
|
|
282
|
+
if (BuildConfig.DEBUG) {
|
|
283
|
+
Log.d("Tag", "debug only")
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
`;
|
|
287
|
+
assert.equal(hasAndroidNoConsoleLogUsage(source), true);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test('hasAndroidNoConsoleLogUsage ignora comentarios, strings y logs protegidos por BuildConfig.DEBUG', () => {
|
|
291
|
+
const source = `
|
|
292
|
+
// Timber.d("debug")
|
|
293
|
+
val sample = "Log.e(\\"Tag\\", \\"debug\\")"
|
|
294
|
+
fun render() {
|
|
295
|
+
if (BuildConfig.DEBUG) Timber.d("debug only")
|
|
296
|
+
if (BuildConfig.DEBUG) {
|
|
297
|
+
Log.e("Tag", "debug only")
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
`;
|
|
301
|
+
assert.equal(hasAndroidNoConsoleLogUsage(source), false);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test('hasAndroidHardcodedStringUsage detecta strings hardcodeadas en codigo Android', () => {
|
|
305
|
+
const source = `
|
|
306
|
+
fun render() {
|
|
307
|
+
val title = "Hola mundo"
|
|
308
|
+
}
|
|
309
|
+
`;
|
|
310
|
+
assert.equal(hasAndroidHardcodedStringUsage(source), true);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test('hasAndroidHardcodedStringUsage ignora comentarios y referencias a resources', () => {
|
|
314
|
+
const source = `
|
|
315
|
+
// "Hola mundo"
|
|
316
|
+
fun render() {
|
|
317
|
+
val title = R.string.app_name
|
|
318
|
+
}
|
|
319
|
+
`;
|
|
320
|
+
assert.equal(hasAndroidHardcodedStringUsage(source), false);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test('hasAndroidSingletonUsage detecta object declarations y companion singleton holders', () => {
|
|
324
|
+
const source = `
|
|
325
|
+
object SessionManager {
|
|
326
|
+
fun refresh() {}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
class HomeRepository private constructor() {
|
|
330
|
+
companion object {
|
|
331
|
+
@Volatile private var INSTANCE: HomeRepository? = null
|
|
332
|
+
|
|
333
|
+
fun getInstance(): HomeRepository {
|
|
334
|
+
return INSTANCE ?: HomeRepository()
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
`;
|
|
339
|
+
assert.equal(hasAndroidSingletonUsage(source), true);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test('hasAndroidSingletonUsage ignora anonymous objects y companion objects inocuos', () => {
|
|
343
|
+
const source = `
|
|
344
|
+
val listener = object : Runnable {
|
|
345
|
+
override fun run() {}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
@Module
|
|
349
|
+
object NetworkModule {
|
|
350
|
+
fun provideClient(): String = "ok"
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
class Themes {
|
|
354
|
+
companion object {
|
|
355
|
+
const val DEFAULT = "dark"
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
`;
|
|
359
|
+
assert.equal(hasAndroidSingletonUsage(source), false);
|
|
360
|
+
});
|
|
361
|
+
|
|
94
362
|
test('findKotlinPresentationSrpMatch devuelve payload semantico para SRP-Android en presentation', () => {
|
|
95
363
|
const source = `
|
|
96
364
|
import android.content.SharedPreferences
|
|
@@ -292,6 +292,346 @@ export const hasKotlinRunBlockingUsage = (source: string): boolean => {
|
|
|
292
292
|
});
|
|
293
293
|
};
|
|
294
294
|
|
|
295
|
+
export const hasKotlinForceUnwrapUsage = (source: string): boolean => {
|
|
296
|
+
return scanCodeLikeSource(source, ({ source: kotlinSource, index, current }) => {
|
|
297
|
+
if (current !== '!' || kotlinSource[index + 1] !== '!') {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return kotlinSource[index + 2] !== '=';
|
|
302
|
+
});
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
export const hasAndroidJavaSourceCode = (source: string): boolean => {
|
|
306
|
+
return scanCodeLikeSource(source, ({ source: javaSource, index, current }) => {
|
|
307
|
+
if (!/[a-zA-Z_]/.test(current)) {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const tail = javaSource.slice(index, index + 32);
|
|
312
|
+
return /^(package|import|class|interface|enum|record)\b/.test(tail);
|
|
313
|
+
});
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
export const hasAndroidAsyncTaskUsage = (source: string): boolean => {
|
|
317
|
+
return scanCodeLikeSource(source, ({ source: androidSource, index, current }) => {
|
|
318
|
+
if (current !== 'A') {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return hasIdentifierAt(androidSource, index, 'AsyncTask');
|
|
323
|
+
});
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
export const hasAndroidFindViewByIdUsage = (source: string): boolean => {
|
|
327
|
+
return scanCodeLikeSource(source, ({ source: androidSource, index, current }) => {
|
|
328
|
+
if (current !== 'f') {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return hasIdentifierAt(androidSource, index, 'findViewById');
|
|
333
|
+
});
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
export const hasAndroidRxJavaUsage = (source: string): boolean => {
|
|
337
|
+
return scanCodeLikeSource(source, ({ source: androidSource, index, current }) => {
|
|
338
|
+
if (current === 'i' && androidSource.startsWith('io.reactivex', index)) {
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (current === 'r' && androidSource.startsWith('rx.', index)) {
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const rxJavaIdentifiers = [
|
|
347
|
+
'Observable',
|
|
348
|
+
'Flowable',
|
|
349
|
+
'Single',
|
|
350
|
+
'Maybe',
|
|
351
|
+
'Completable',
|
|
352
|
+
'Disposable',
|
|
353
|
+
'CompositeDisposable',
|
|
354
|
+
'Subject',
|
|
355
|
+
'PublishSubject',
|
|
356
|
+
'BehaviorSubject',
|
|
357
|
+
'ReplaySubject',
|
|
358
|
+
'ConnectableObservable',
|
|
359
|
+
];
|
|
360
|
+
|
|
361
|
+
return rxJavaIdentifiers.some((identifier) => {
|
|
362
|
+
return current === identifier[0] && hasIdentifierAt(androidSource, index, identifier);
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
export const hasAndroidDispatcherUsage = (source: string): boolean => {
|
|
368
|
+
return scanCodeLikeSource(source, ({ source: androidSource, index, current }) => {
|
|
369
|
+
if (current !== 'D' || !hasIdentifierAt(androidSource, index, 'Dispatchers')) {
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const start = index + 'Dispatchers'.length;
|
|
374
|
+
const tail = androidSource.slice(start, start + 32);
|
|
375
|
+
return /^\s*\.(Main|IO|Default)\b/.test(tail);
|
|
376
|
+
});
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
export const hasAndroidWithContextUsage = (source: string): boolean => {
|
|
380
|
+
return scanCodeLikeSource(source, ({ source: androidSource, index, current }) => {
|
|
381
|
+
if (current !== 'w' || !hasIdentifierAt(androidSource, index, 'withContext')) {
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const start = index + 'withContext'.length;
|
|
386
|
+
const tail = androidSource.slice(start, start + 48);
|
|
387
|
+
return /^\s*(<[^>\n]+>\s*)?\(/.test(tail);
|
|
388
|
+
});
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
export const hasAndroidCoroutineTryCatchUsage = (source: string): boolean => {
|
|
392
|
+
const hasCoroutineContextHint = scanCodeLikeSource(source, ({ source: androidSource, index, current }) => {
|
|
393
|
+
if (current === 'w' && hasIdentifierAt(androidSource, index, 'withContext')) {
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (current === 'D' && hasIdentifierAt(androidSource, index, 'Dispatchers')) {
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (current === 'v' && hasIdentifierAt(androidSource, index, 'viewModelScope')) {
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (current === 'l' && hasIdentifierAt(androidSource, index, 'lifecycleScope')) {
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (current === 'c' && hasIdentifierAt(androidSource, index, 'coroutineScope')) {
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (current === 's' && hasIdentifierAt(androidSource, index, 'supervisorScope')) {
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (current === 'l' && hasIdentifierAt(androidSource, index, 'launch')) {
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (current === 'a' && hasIdentifierAt(androidSource, index, 'async')) {
|
|
422
|
+
return true;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (current === 's' && hasIdentifierAt(androidSource, index, 'suspend')) {
|
|
426
|
+
const tail = androidSource.slice(index + 'suspend'.length, index + 'suspend'.length + 16);
|
|
427
|
+
return /^\s+fun\b/.test(tail);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return false;
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
if (!hasCoroutineContextHint) {
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return scanCodeLikeSource(source, ({ source: androidSource, index, current }) => {
|
|
438
|
+
if (current !== 't' || !hasIdentifierAt(androidSource, index, 'try')) {
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const tail = androidSource.slice(index + 'try'.length, index + 'try'.length + 192);
|
|
443
|
+
return /\bcatch\b/.test(tail);
|
|
444
|
+
});
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const isAndroidDebugLogGuardWindow = (lines: readonly string[], lineIndex: number): boolean => {
|
|
448
|
+
const guardPattern = /\bif\s*\(\s*BuildConfig\.DEBUG\s*\)/;
|
|
449
|
+
const windowStart = Math.max(0, lineIndex - 2);
|
|
450
|
+
const window = lines
|
|
451
|
+
.slice(windowStart, lineIndex + 1)
|
|
452
|
+
.map((line) => stripKotlinLineForSemanticScan(line ?? ''))
|
|
453
|
+
.join(' ');
|
|
454
|
+
return guardPattern.test(window);
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
const hasAndroidLogCallInLine = (line: string): boolean => {
|
|
458
|
+
return /\b(?:Timber|Log)\s*\.\s*(?:v|d|i|w|e)\s*\(/.test(line);
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
export const hasAndroidNoConsoleLogUsage = (source: string): boolean => {
|
|
462
|
+
const lines = source.split(/\r?\n/);
|
|
463
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
464
|
+
const sanitizedLine = stripKotlinLineForSemanticScan(lines[index] ?? '');
|
|
465
|
+
if (sanitizedLine.trimStart().startsWith('import ')) {
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
if (!hasAndroidLogCallInLine(sanitizedLine)) {
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
if (isAndroidDebugLogGuardWindow(lines, index)) {
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return false;
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
export const hasAndroidHardcodedStringUsage = (source: string): boolean => {
|
|
481
|
+
let inLineComment = false;
|
|
482
|
+
let inBlockComment = 0;
|
|
483
|
+
let inString = false;
|
|
484
|
+
let inMultilineString = false;
|
|
485
|
+
let stringStartIndex = -1;
|
|
486
|
+
let lastNonWhitespace = '';
|
|
487
|
+
|
|
488
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
489
|
+
const current = source[index];
|
|
490
|
+
const next = source[index + 1];
|
|
491
|
+
const nextTwo = source[index + 2];
|
|
492
|
+
|
|
493
|
+
if (inLineComment) {
|
|
494
|
+
if (current === '\n') {
|
|
495
|
+
inLineComment = false;
|
|
496
|
+
}
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (inBlockComment > 0) {
|
|
501
|
+
if (current === '/' && next === '*') {
|
|
502
|
+
inBlockComment += 1;
|
|
503
|
+
index += 1;
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
if (current === '*' && next === '/') {
|
|
507
|
+
inBlockComment -= 1;
|
|
508
|
+
index += 1;
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (inMultilineString) {
|
|
515
|
+
if (current === '"' && next === '"' && nextTwo === '"') {
|
|
516
|
+
return true;
|
|
517
|
+
}
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (inString) {
|
|
522
|
+
if (current === '\\') {
|
|
523
|
+
index += 1;
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
if (current === '"') {
|
|
527
|
+
const previousChar = lastNonWhitespace;
|
|
528
|
+
const isInterpolationOrResource = previousChar === '.' || previousChar === ')';
|
|
529
|
+
if (!isInterpolationOrResource) {
|
|
530
|
+
return true;
|
|
531
|
+
}
|
|
532
|
+
inString = false;
|
|
533
|
+
stringStartIndex = -1;
|
|
534
|
+
}
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (current === '/' && next === '/') {
|
|
539
|
+
inLineComment = true;
|
|
540
|
+
index += 1;
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (current === '/' && next === '*') {
|
|
545
|
+
inBlockComment = 1;
|
|
546
|
+
index += 1;
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (current === '"' && next === '"' && nextTwo === '"') {
|
|
551
|
+
inMultilineString = true;
|
|
552
|
+
stringStartIndex = index;
|
|
553
|
+
return true;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (current === '"') {
|
|
557
|
+
const tail = source.slice(Math.max(0, index - 24), index + 1);
|
|
558
|
+
if (/\bR\.(?:string|plurals|array)\s*\.\s*[A-Za-z_]/.test(tail)) {
|
|
559
|
+
inString = false;
|
|
560
|
+
stringStartIndex = -1;
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
inString = true;
|
|
564
|
+
stringStartIndex = index;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (!/\s/.test(current)) {
|
|
568
|
+
lastNonWhitespace = current;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return false;
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
const hasAndroidSingletonModuleAnnotation = (window: string): boolean => {
|
|
576
|
+
return /@(Module|InstallIn|EntryPoint)\b/.test(window);
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
const hasAndroidSingletonCompanionPattern = (block: string): boolean => {
|
|
580
|
+
return (
|
|
581
|
+
/\b(?:getInstance|INSTANCE|instance)\b/.test(block) ||
|
|
582
|
+
/@Volatile\b/.test(block) ||
|
|
583
|
+
/\bprivate\s+(?:val|var)\s+(?:INSTANCE|instance)\b/.test(block)
|
|
584
|
+
);
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
export const hasAndroidSingletonUsage = (source: string): boolean => {
|
|
588
|
+
const lines = source.split(/\r?\n/);
|
|
589
|
+
|
|
590
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
591
|
+
const sanitizedLine = stripKotlinLineForSemanticScan(lines[index] ?? '');
|
|
592
|
+
if (sanitizedLine.trimStart().startsWith('import ')) {
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const namedObjectMatch = sanitizedLine.match(
|
|
597
|
+
/^\s*(?:internal\s+|private\s+|public\s+)?(?:data\s+)?object\s+([A-Za-z_][A-Za-z0-9_]*)\b/
|
|
598
|
+
);
|
|
599
|
+
if (namedObjectMatch?.[1]) {
|
|
600
|
+
const annotationWindow = lines
|
|
601
|
+
.slice(Math.max(0, index - 3), index + 1)
|
|
602
|
+
.map((line) => stripKotlinLineForSemanticScan(line ?? ''))
|
|
603
|
+
.join(' ');
|
|
604
|
+
if (!hasAndroidSingletonModuleAnnotation(annotationWindow)) {
|
|
605
|
+
return true;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (!/\bcompanion\s+object\b/.test(sanitizedLine)) {
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
let braceDepth =
|
|
614
|
+
countTokenOccurrences(sanitizedLine, '{') - countTokenOccurrences(sanitizedLine, '}');
|
|
615
|
+
const blockLines = [sanitizedLine];
|
|
616
|
+
|
|
617
|
+
for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
|
|
618
|
+
const candidateLine = stripKotlinLineForSemanticScan(lines[cursor] ?? '');
|
|
619
|
+
blockLines.push(candidateLine);
|
|
620
|
+
braceDepth += countTokenOccurrences(candidateLine, '{');
|
|
621
|
+
braceDepth -= countTokenOccurrences(candidateLine, '}');
|
|
622
|
+
if (braceDepth <= 0) {
|
|
623
|
+
break;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (hasAndroidSingletonCompanionPattern(blockLines.join(' '))) {
|
|
628
|
+
return true;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return false;
|
|
633
|
+
};
|
|
634
|
+
|
|
295
635
|
export const findKotlinPresentationSrpMatch = (
|
|
296
636
|
source: string
|
|
297
637
|
): KotlinPresentationSrpMatch | undefined => {
|
|
@@ -91,6 +91,14 @@ const isAndroidKotlinPath = (path: string): boolean => {
|
|
|
91
91
|
return (path.endsWith('.kt') || path.endsWith('.kts')) && path.startsWith('apps/android/');
|
|
92
92
|
};
|
|
93
93
|
|
|
94
|
+
const isAndroidJavaPath = (path: string): boolean => {
|
|
95
|
+
return path.endsWith('.java') && path.startsWith('apps/android/');
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const isAndroidSourcePath = (path: string): boolean => {
|
|
99
|
+
return isAndroidKotlinPath(path) || isAndroidJavaPath(path);
|
|
100
|
+
};
|
|
101
|
+
|
|
94
102
|
const isAndroidPresentationPath = (path: string): boolean => {
|
|
95
103
|
return isAndroidKotlinPath(path) && path.includes('/presentation/');
|
|
96
104
|
};
|
|
@@ -146,6 +154,16 @@ const isKotlinTestPath = (path: string): boolean => {
|
|
|
146
154
|
);
|
|
147
155
|
};
|
|
148
156
|
|
|
157
|
+
const isJavaTestPath = (path: string): boolean => {
|
|
158
|
+
const normalized = path.toLowerCase();
|
|
159
|
+
return (
|
|
160
|
+
normalized.includes('/test/') ||
|
|
161
|
+
normalized.includes('/androidtest/') ||
|
|
162
|
+
normalized.endsWith('test.java') ||
|
|
163
|
+
normalized.endsWith('tests.java')
|
|
164
|
+
);
|
|
165
|
+
};
|
|
166
|
+
|
|
149
167
|
const isExcludedProjectScanPath = (path: string): boolean => {
|
|
150
168
|
const normalized = path.replace(/\\/g, '/').toLowerCase();
|
|
151
169
|
return (
|
|
@@ -636,6 +654,17 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
|
|
|
636
654
|
{ platform: 'android', pathCheck: isAndroidKotlinPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasKotlinThreadSleepCall, ruleId: 'heuristics.android.thread-sleep.ast', code: 'HEURISTICS_ANDROID_THREAD_SLEEP_AST', message: 'AST heuristic detected Thread.sleep usage in production Kotlin code.' },
|
|
637
655
|
{ platform: 'android', pathCheck: isAndroidKotlinPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasKotlinGlobalScopeUsage, ruleId: 'heuristics.android.globalscope.ast', code: 'HEURISTICS_ANDROID_GLOBAL_SCOPE_AST', message: 'AST heuristic detected GlobalScope coroutine usage in production Kotlin code.' },
|
|
638
656
|
{ platform: 'android', pathCheck: isAndroidKotlinPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasKotlinRunBlockingUsage, ruleId: 'heuristics.android.run-blocking.ast', code: 'HEURISTICS_ANDROID_RUN_BLOCKING_AST', message: 'AST heuristic detected runBlocking usage in production Kotlin code.' },
|
|
657
|
+
{ platform: 'android', pathCheck: isAndroidKotlinPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasKotlinForceUnwrapUsage, ruleId: 'heuristics.android.force-unwrap.ast', code: 'HEURISTICS_ANDROID_FORCE_UNWRAP_AST', message: 'AST heuristic detected Kotlin force unwrap (!!) usage in production code.' },
|
|
658
|
+
{ platform: 'android', pathCheck: isAndroidJavaPath, excludePaths: [isJavaTestPath], detect: TextAndroid.hasAndroidJavaSourceCode, ruleId: 'heuristics.android.java-source.ast', code: 'HEURISTICS_ANDROID_JAVA_SOURCE_AST', message: 'AST heuristic detected Java source in Android production code where Kotlin is required for new code.' },
|
|
659
|
+
{ platform: 'android', pathCheck: isAndroidSourcePath, excludePaths: [isKotlinTestPath, isJavaTestPath], detect: TextAndroid.hasAndroidAsyncTaskUsage, ruleId: 'heuristics.android.asynctask-deprecated.ast', code: 'HEURISTICS_ANDROID_ASYNCTASK_DEPRECATED_AST', message: 'AST heuristic detected AsyncTask usage in Android production code where Coroutines are required.' },
|
|
660
|
+
{ platform: 'android', pathCheck: isAndroidSourcePath, excludePaths: [isKotlinTestPath, isJavaTestPath], detect: TextAndroid.hasAndroidFindViewByIdUsage, ruleId: 'heuristics.android.findviewbyid.ast', code: 'HEURISTICS_ANDROID_FINDVIEWBYID_AST', message: 'AST heuristic detected findViewById usage in Android production code where View Binding or Compose is required.' },
|
|
661
|
+
{ platform: 'android', pathCheck: isAndroidSourcePath, excludePaths: [isKotlinTestPath, isJavaTestPath], detect: TextAndroid.hasAndroidRxJavaUsage, ruleId: 'heuristics.android.rxjava-new-code.ast', code: 'HEURISTICS_ANDROID_RXJAVA_NEW_CODE_AST', message: 'AST heuristic detected RxJava usage in Android production code where Flow is required for new code.' },
|
|
662
|
+
{ platform: 'android', pathCheck: isAndroidKotlinPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasAndroidDispatcherUsage, ruleId: 'heuristics.android.dispatchers-main-ui-io-network-disk-default-cpu.ast', code: 'HEURISTICS_ANDROID_DISPATCHERS_MAIN_UI_IO_NETWORK_DISK_DEFAULT_CPU_AST', message: 'AST heuristic detected explicit Dispatchers.Main/IO/Default usage in Android production code where dispatcher selection must remain intentional.' },
|
|
663
|
+
{ platform: 'android', pathCheck: isAndroidKotlinPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasAndroidWithContextUsage, ruleId: 'heuristics.android.withcontext-change-dispatcher.ast', code: 'HEURISTICS_ANDROID_WITHCONTEXT_CHANGE_DISPATCHER_AST', message: 'AST heuristic detected withContext usage in Android production code where dispatcher switching is intentional.' },
|
|
664
|
+
{ platform: 'android', pathCheck: isAndroidSourcePath, excludePaths: [isKotlinTestPath, isJavaTestPath], detect: TextAndroid.hasAndroidNoConsoleLogUsage, ruleId: 'heuristics.android.no-console-log.ast', code: 'HEURISTICS_ANDROID_NO_CONSOLE_LOG_AST', message: 'AST heuristic detected Android logging usage in production code without a debug-only guard.' },
|
|
665
|
+
{ platform: 'android', pathCheck: isAndroidSourcePath, excludePaths: [isKotlinTestPath, isJavaTestPath], detect: TextAndroid.hasAndroidHardcodedStringUsage, ruleId: 'heuristics.android.hardcoded-strings.ast', code: 'HEURISTICS_ANDROID_HARDCODED_STRINGS_AST', message: 'AST heuristic detected hardcoded string literal usage in Android production code where strings.xml should be used.' },
|
|
666
|
+
{ platform: 'android', pathCheck: isAndroidKotlinPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasAndroidSingletonUsage, ruleId: 'heuristics.android.no-singleton.ast', code: 'HEURISTICS_ANDROID_NO_SINGLETON_AST', message: 'AST heuristic detected Kotlin singleton object or companion singleton holder usage in Android production code where Hilt or Dagger DI should be used.' },
|
|
667
|
+
{ platform: 'android', pathCheck: isAndroidKotlinPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasAndroidCoroutineTryCatchUsage, ruleId: 'heuristics.android.try-catch-manejo-de-errores-en-coroutines.ast', code: 'HEURISTICS_ANDROID_TRY_CATCH_MANEJO_DE_ERRORES_EN_COROUTINES_AST', message: 'AST heuristic detected try/catch usage in Android coroutine code where error handling must remain explicit.' },
|
|
639
668
|
];
|
|
640
669
|
|
|
641
670
|
const extractWorkflowHeuristicFacts = (
|