pumuki 6.3.188 → 6.3.190
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/VERSION +1 -1
- package/core/facts/detectors/text/android.test.ts +77 -0
- package/core/facts/detectors/text/android.ts +18 -0
- package/core/facts/extractHeuristicFacts.ts +6 -0
- package/core/rules/presets/heuristics/android.test.ts +11 -1
- package/core/rules/presets/heuristics/android.ts +40 -0
- package/docs/codex-skills/android-enterprise-rules.md +8 -0
- package/integrations/config/skillsDetectorRegistry.ts +8 -0
- package/package.json +1 -1
- package/skills.lock.json +1 -1
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
v6.3.
|
|
1
|
+
v6.3.190
|
|
@@ -10,8 +10,10 @@ import {
|
|
|
10
10
|
hasKotlinDispatcherMainBoundaryLeakUsage,
|
|
11
11
|
hasKotlinGlobalScopeUsage,
|
|
12
12
|
hasKotlinHardcodedBackgroundDispatcherUsage,
|
|
13
|
+
hasKotlinJUnit4Usage,
|
|
13
14
|
hasKotlinLiveDataStateExposureUsage,
|
|
14
15
|
hasKotlinLifecycleScopeUsage,
|
|
16
|
+
hasKotlinProductionMockUsage,
|
|
15
17
|
hasKotlinSharedPreferencesUsage,
|
|
16
18
|
hasKotlinWithContextUsage,
|
|
17
19
|
hasKotlinManualCoroutineScopeInViewModelUsage,
|
|
@@ -275,6 +277,81 @@ class PreferencesStore {
|
|
|
275
277
|
assert.equal(hasKotlinSharedPreferencesUsage(source), false);
|
|
276
278
|
});
|
|
277
279
|
|
|
280
|
+
test('hasKotlinJUnit4Usage detecta imports y anotaciones JUnit4 en tests Kotlin Android', () => {
|
|
281
|
+
const importSource = `
|
|
282
|
+
import org.junit.Test
|
|
283
|
+
import org.junit.Assert
|
|
284
|
+
|
|
285
|
+
class OrdersViewModelTest {
|
|
286
|
+
@Test
|
|
287
|
+
fun loadsOrders() {
|
|
288
|
+
Assert.assertEquals(1, 1)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
`;
|
|
292
|
+
const runnerSource = `
|
|
293
|
+
import org.junit.runner.RunWith
|
|
294
|
+
|
|
295
|
+
@RunWith(AndroidJUnit4::class)
|
|
296
|
+
class OrdersInstrumentedTest
|
|
297
|
+
`;
|
|
298
|
+
|
|
299
|
+
assert.equal(hasKotlinJUnit4Usage(importSource), true);
|
|
300
|
+
assert.equal(hasKotlinJUnit4Usage(runnerSource), true);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test('hasKotlinJUnit4Usage ignora JUnit5, comentarios y strings', () => {
|
|
304
|
+
const source = `
|
|
305
|
+
import org.junit.jupiter.api.Test
|
|
306
|
+
// import org.junit.Test
|
|
307
|
+
val sample = "Assert.assertEquals(1, 1)"
|
|
308
|
+
|
|
309
|
+
class OrdersViewModelTest {
|
|
310
|
+
@Test
|
|
311
|
+
fun loadsOrders() {
|
|
312
|
+
assertEquals(1, 1)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
`;
|
|
316
|
+
assert.equal(hasKotlinJUnit4Usage(source), false);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test('hasKotlinProductionMockUsage detecta mocks y spies en Kotlin Android production', () => {
|
|
320
|
+
const mockkSource = `
|
|
321
|
+
class OrdersRepositoryFactory {
|
|
322
|
+
fun create(): OrdersRepository = mockk(relaxed = true)
|
|
323
|
+
}
|
|
324
|
+
`;
|
|
325
|
+
const mockitoSource = `
|
|
326
|
+
class OrdersRepositoryFactory {
|
|
327
|
+
private val api = Mockito.mock(OrdersApi::class.java)
|
|
328
|
+
}
|
|
329
|
+
`;
|
|
330
|
+
const annotationSource = `
|
|
331
|
+
class OrdersRepositoryFactory {
|
|
332
|
+
@Spy
|
|
333
|
+
lateinit var repository: OrdersRepository
|
|
334
|
+
}
|
|
335
|
+
`;
|
|
336
|
+
|
|
337
|
+
assert.equal(hasKotlinProductionMockUsage(mockkSource), true);
|
|
338
|
+
assert.equal(hasKotlinProductionMockUsage(mockitoSource), true);
|
|
339
|
+
assert.equal(hasKotlinProductionMockUsage(annotationSource), true);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test('hasKotlinProductionMockUsage ignora imports, comentarios, strings y nombres parciales', () => {
|
|
343
|
+
const source = `
|
|
344
|
+
import io.mockk.mockk
|
|
345
|
+
import org.mockito.Mockito
|
|
346
|
+
// val api = mockk<OrdersApi>()
|
|
347
|
+
val sample = "Mockito.mock(OrdersApi::class.java)"
|
|
348
|
+
class OrdersRepositoryFactory {
|
|
349
|
+
fun create() = mockkitoFactory()
|
|
350
|
+
}
|
|
351
|
+
`;
|
|
352
|
+
assert.equal(hasKotlinProductionMockUsage(source), false);
|
|
353
|
+
});
|
|
354
|
+
|
|
278
355
|
test('hasKotlinSupervisorScopeUsage detecta supervisorScope con parentesis y llaves', () => {
|
|
279
356
|
const parenthesesSource = `
|
|
280
357
|
class SyncOrdersUseCase {
|
|
@@ -271,6 +271,24 @@ export const hasKotlinSharedPreferencesUsage = (source: string): boolean => {
|
|
|
271
271
|
return collectKotlinRegexLines(source, /\b(?:SharedPreferences|PreferenceManager\s*\.|getSharedPreferences\s*\()/).length > 0;
|
|
272
272
|
};
|
|
273
273
|
|
|
274
|
+
export const hasKotlinJUnit4Usage = (source: string): boolean => {
|
|
275
|
+
return source.split(/\r?\n/).some((line) => {
|
|
276
|
+
const sanitized = stripKotlinLineForSemanticScan(line);
|
|
277
|
+
return (
|
|
278
|
+
/\bimport\s+org\.junit\.(?:Test|Before|After|BeforeClass|AfterClass|Assert|Rule|ClassRule|runner\.RunWith|rules\.)\b/.test(sanitized) ||
|
|
279
|
+
/@(?:RunWith|Rule|ClassRule)\b/.test(sanitized) ||
|
|
280
|
+
/\bAssert\s*\./.test(sanitized)
|
|
281
|
+
);
|
|
282
|
+
});
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
export const hasKotlinProductionMockUsage = (source: string): boolean => {
|
|
286
|
+
return collectKotlinRegexLines(
|
|
287
|
+
source,
|
|
288
|
+
/(?:\b(?:mockk|spyk|mockkObject|mockkStatic|mockkConstructor|mockito|mockitoSession|mockitoRule)\s*(?:<[^>\n]+>\s*)?\(|\bMockito\s*\.\s*(?:mock|spy)\s*\(|@(?:Mock|Spy|MockK|RelaxedMockK)\b)/
|
|
289
|
+
).length > 0;
|
|
290
|
+
};
|
|
291
|
+
|
|
274
292
|
export const hasKotlinSupervisorScopeUsage = (source: string): boolean => {
|
|
275
293
|
return collectKotlinRegexLines(source, /\bsupervisorScope\s*(?:<[^>\n]+>\s*)?(?:\(|\{)/).length > 0;
|
|
276
294
|
};
|
|
@@ -161,6 +161,10 @@ const isKotlinTestPath = (path: string): boolean => {
|
|
|
161
161
|
);
|
|
162
162
|
};
|
|
163
163
|
|
|
164
|
+
const isAndroidKotlinTestPath = (path: string): boolean => {
|
|
165
|
+
return isAndroidKotlinPath(path) && isKotlinTestPath(path);
|
|
166
|
+
};
|
|
167
|
+
|
|
164
168
|
const isExcludedProjectScanPath = (path: string): boolean => {
|
|
165
169
|
const normalized = path.replace(/\\/g, '/').toLowerCase();
|
|
166
170
|
return (
|
|
@@ -654,6 +658,8 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
|
|
|
654
658
|
{ 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.' },
|
|
655
659
|
{ platform: 'android', pathCheck: isAndroidLocalPropertiesPath, excludePaths: [], detect: detectsTrackedFilePresence, ruleId: 'heuristics.android.security.local-properties-tracked.ast', code: 'HEURISTICS_ANDROID_SECURITY_LOCAL_PROPERTIES_TRACKED_AST', message: 'AST heuristic detected tracked Android local.properties file.' },
|
|
656
660
|
{ platform: 'android', pathCheck: isAndroidKotlinPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasKotlinSharedPreferencesUsage, ruleId: 'heuristics.android.persistence.shared-preferences-usage.ast', code: 'HEURISTICS_ANDROID_PERSISTENCE_SHARED_PREFERENCES_USAGE_AST', message: 'AST heuristic detected SharedPreferences usage in Android production Kotlin code.' },
|
|
661
|
+
{ platform: 'android', pathCheck: isAndroidKotlinPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasKotlinProductionMockUsage, ruleId: 'heuristics.android.testing.production-mock-usage.ast', code: 'HEURISTICS_ANDROID_TESTING_PRODUCTION_MOCK_USAGE_AST', message: 'AST heuristic detected mock or spy usage in Android production Kotlin code.' },
|
|
662
|
+
{ platform: 'android', pathCheck: isAndroidKotlinTestPath, excludePaths: [], detect: TextAndroid.hasKotlinJUnit4Usage, ruleId: 'heuristics.android.testing.junit4-usage.ast', code: 'HEURISTICS_ANDROID_TESTING_JUNIT4_USAGE_AST', message: 'AST heuristic detected JUnit4 usage in Android Kotlin tests where JUnit5 is preferred.' },
|
|
657
663
|
{ platform: 'android', pathCheck: isAndroidPresentationPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasKotlinLiveDataStateExposureUsage, ruleId: 'heuristics.android.flow.livedata-state-exposure.ast', code: 'HEURISTICS_ANDROID_FLOW_LIVEDATA_STATE_EXPOSURE_AST', message: 'AST heuristic detected LiveData state exposure in Android presentation code where StateFlow or SharedFlow should be preferred.' },
|
|
658
664
|
{ platform: 'android', pathCheck: isAndroidPresentationPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasKotlinManualCoroutineScopeInViewModelUsage, ruleId: 'heuristics.android.coroutines.manual-scope-in-viewmodel.ast', code: 'HEURISTICS_ANDROID_COROUTINES_MANUAL_SCOPE_IN_VIEWMODEL_AST', message: 'AST heuristic detected manual CoroutineScope inside an Android ViewModel where viewModelScope should be preferred.' },
|
|
659
665
|
{ platform: 'android', pathCheck: isAndroidNonPresentationKotlinPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasKotlinDispatcherMainBoundaryLeakUsage, ruleId: 'heuristics.android.coroutines.dispatchers-main-boundary-leak.ast', code: 'HEURISTICS_ANDROID_COROUTINES_DISPATCHERS_MAIN_BOUNDARY_LEAK_AST', message: 'AST heuristic detected Dispatchers.Main outside Android presentation code.' },
|
|
@@ -3,7 +3,7 @@ import test from 'node:test';
|
|
|
3
3
|
import { androidRules } from './android';
|
|
4
4
|
|
|
5
5
|
test('androidRules define reglas heurísticas locked para plataforma android', () => {
|
|
6
|
-
assert.equal(androidRules.length,
|
|
6
|
+
assert.equal(androidRules.length, 20);
|
|
7
7
|
|
|
8
8
|
const ids = androidRules.map((rule) => rule.id);
|
|
9
9
|
assert.deepEqual(ids, [
|
|
@@ -13,6 +13,8 @@ test('androidRules define reglas heurísticas locked para plataforma android', (
|
|
|
13
13
|
'heuristics.android.flow.livedata-state-exposure.ast',
|
|
14
14
|
'heuristics.android.security.local-properties-tracked.ast',
|
|
15
15
|
'heuristics.android.persistence.shared-preferences-usage.ast',
|
|
16
|
+
'heuristics.android.testing.junit4-usage.ast',
|
|
17
|
+
'heuristics.android.testing.production-mock-usage.ast',
|
|
16
18
|
'heuristics.android.coroutines.manual-scope-in-viewmodel.ast',
|
|
17
19
|
'heuristics.android.coroutines.dispatchers-main-boundary-leak.ast',
|
|
18
20
|
'heuristics.android.coroutines.hardcoded-background-dispatcher.ast',
|
|
@@ -62,6 +64,14 @@ test('androidRules define reglas heurísticas locked para plataforma android', (
|
|
|
62
64
|
byId.get('heuristics.android.persistence.shared-preferences-usage.ast')?.then.code,
|
|
63
65
|
'HEURISTICS_ANDROID_PERSISTENCE_SHARED_PREFERENCES_USAGE_AST'
|
|
64
66
|
);
|
|
67
|
+
assert.equal(
|
|
68
|
+
byId.get('heuristics.android.testing.junit4-usage.ast')?.then.code,
|
|
69
|
+
'HEURISTICS_ANDROID_TESTING_JUNIT4_USAGE_AST'
|
|
70
|
+
);
|
|
71
|
+
assert.equal(
|
|
72
|
+
byId.get('heuristics.android.testing.production-mock-usage.ast')?.then.code,
|
|
73
|
+
'HEURISTICS_ANDROID_TESTING_PRODUCTION_MOCK_USAGE_AST'
|
|
74
|
+
);
|
|
65
75
|
assert.equal(
|
|
66
76
|
byId.get('heuristics.android.coroutines.manual-scope-in-viewmodel.ast')?.then.code,
|
|
67
77
|
'HEURISTICS_ANDROID_COROUTINES_MANUAL_SCOPE_IN_VIEWMODEL_AST'
|
|
@@ -115,6 +115,46 @@ export const androidRules: RuleSet = [
|
|
|
115
115
|
code: 'HEURISTICS_ANDROID_PERSISTENCE_SHARED_PREFERENCES_USAGE_AST',
|
|
116
116
|
},
|
|
117
117
|
},
|
|
118
|
+
{
|
|
119
|
+
id: 'heuristics.android.testing.junit4-usage.ast',
|
|
120
|
+
description:
|
|
121
|
+
'Detects JUnit4 usage in Android Kotlin tests where JUnit5 is preferred.',
|
|
122
|
+
severity: 'WARN',
|
|
123
|
+
platform: 'android',
|
|
124
|
+
locked: true,
|
|
125
|
+
when: {
|
|
126
|
+
kind: 'Heuristic',
|
|
127
|
+
where: {
|
|
128
|
+
ruleId: 'heuristics.android.testing.junit4-usage.ast',
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
then: {
|
|
132
|
+
kind: 'Finding',
|
|
133
|
+
message:
|
|
134
|
+
'AST heuristic detected JUnit4 usage in Android Kotlin tests.',
|
|
135
|
+
code: 'HEURISTICS_ANDROID_TESTING_JUNIT4_USAGE_AST',
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: 'heuristics.android.testing.production-mock-usage.ast',
|
|
140
|
+
description:
|
|
141
|
+
'Detects mock or spy usage in Android production Kotlin code.',
|
|
142
|
+
severity: 'WARN',
|
|
143
|
+
platform: 'android',
|
|
144
|
+
locked: true,
|
|
145
|
+
when: {
|
|
146
|
+
kind: 'Heuristic',
|
|
147
|
+
where: {
|
|
148
|
+
ruleId: 'heuristics.android.testing.production-mock-usage.ast',
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
then: {
|
|
152
|
+
kind: 'Finding',
|
|
153
|
+
message:
|
|
154
|
+
'AST heuristic detected mock or spy usage in Android production Kotlin code.',
|
|
155
|
+
code: 'HEURISTICS_ANDROID_TESTING_PRODUCTION_MOCK_USAGE_AST',
|
|
156
|
+
},
|
|
157
|
+
},
|
|
118
158
|
{
|
|
119
159
|
id: 'heuristics.android.coroutines.manual-scope-in-viewmodel.ast',
|
|
120
160
|
description:
|
|
@@ -146,6 +146,14 @@ app/
|
|
|
146
146
|
✅ Este baseline no obliga a que todo ViewModel use Flow; solo detecta una alternativa legacy concreta cuando aparece en código de presentación Android.
|
|
147
147
|
✅ `Flow`, `collectAsState`, `stateIn`, Room observable queries y SharedFlow events quedan fuera hasta que tengan detectores propios y regresiones dirigidas.
|
|
148
148
|
|
|
149
|
+
### Enforcement AST inicial de testing Android
|
|
150
|
+
✅ `skills.android.guideline.android.junit5-framework-de-testing-preferido-sobre-junit4` debe mapear a señal ejecutable de uso JUnit4 en tests Kotlin Android.
|
|
151
|
+
✅ Este baseline detecta imports/anotaciones JUnit4 (`org.junit.Test`, `org.junit.Assert`, `@RunWith`, reglas JUnit4) bajo `apps/android/**/test/**` y `apps/android/**/androidTest/**`.
|
|
152
|
+
✅ JUnit5/Jupiter queda preservado como caso limpio; migración completa de runner, Gradle y APIs de assertions queda fuera hasta tener detectores propios.
|
|
153
|
+
✅ `skills.android.guideline.android.en-produccio-n-ni-un-mocks-ni-un-spies-todo-real-de-apis-y-persistenci` debe mapear a señal ejecutable de mocks/spies en Kotlin Android production.
|
|
154
|
+
✅ Este baseline detecta usos explícitos de MockK/Mockito y anotaciones `@Mock`/`@Spy` fuera de `test` y `androidTest`; los dobles en tests siguen permitidos.
|
|
155
|
+
✅ La regla no prohíbe clases fake de test doubles en suites de test ni analiza wiring Gradle todavía.
|
|
156
|
+
|
|
149
157
|
### Dependency Injection (Hilt):
|
|
150
158
|
✅ **Hilt** - DI framework (NO manual factories)
|
|
151
159
|
✅ **@HiltAndroidApp** - Application class
|
|
@@ -255,6 +255,14 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
|
|
|
255
255
|
'android.persistence.shared-preferences',
|
|
256
256
|
['heuristics.android.persistence.shared-preferences-usage.ast']
|
|
257
257
|
),
|
|
258
|
+
'skills.android.guideline.android.junit5-framework-de-testing-preferido-sobre-junit4': heuristicDetector(
|
|
259
|
+
'android.testing.junit4-usage',
|
|
260
|
+
['heuristics.android.testing.junit4-usage.ast']
|
|
261
|
+
),
|
|
262
|
+
'skills.android.guideline.android.en-produccio-n-ni-un-mocks-ni-un-spies-todo-real-de-apis-y-persistenci': heuristicDetector(
|
|
263
|
+
'android.testing.production-mock-usage',
|
|
264
|
+
['heuristics.android.testing.production-mock-usage.ast']
|
|
265
|
+
),
|
|
258
266
|
'skills.android.guideline.android.stateflow-estado-mutable-observable': heuristicDetector(
|
|
259
267
|
'android.flow.state-exposure',
|
|
260
268
|
['heuristics.android.flow.livedata-state-exposure.ast']
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.190",
|
|
4
4
|
"description": "Enterprise-grade AST Intelligence System with multi-platform support (iOS, Android, Backend, Frontend) and Feature-First + DDD + Clean Architecture enforcement. Includes dynamic violations API for intelligent querying.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|