pumuki 6.3.270 → 6.3.272
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 +538 -0
- package/core/facts/detectors/text/android.ts +436 -0
- package/core/facts/detectors/text/ios.test.ts +328 -1
- package/core/facts/detectors/text/ios.ts +241 -0
- package/core/facts/detectors/typescript/index.test.ts +393 -0
- package/core/facts/detectors/typescript/index.ts +316 -0
- package/core/facts/extractHeuristicFacts.ts +70 -1
- package/core/rules/presets/heuristics/android.test.ts +91 -1
- package/core/rules/presets/heuristics/android.ts +360 -0
- package/core/rules/presets/heuristics/ios.test.ts +54 -1
- package/core/rules/presets/heuristics/ios.ts +243 -2
- package/core/rules/presets/heuristics/typescript.test.ts +50 -2
- package/core/rules/presets/heuristics/typescript.ts +162 -0
- package/docs/operations/RELEASE_NOTES.md +8 -0
- package/integrations/config/skillsDetectorRegistry.ts +501 -0
- package/integrations/config/skillsRuleClassification.ts +127 -3
- package/integrations/git/runPlatformGate.ts +4 -1
- package/integrations/lifecycle/preWriteAutomation.ts +5 -4
- package/integrations/lifecycle/preWriteLease.ts +41 -4
- package/package.json +1 -1
- package/scripts/classify-skills-rules.ts +2 -2
- package/scripts/framework-menu-consumer-actions-lib.ts +9 -9
- package/scripts/framework-menu-consumer-runtime-actions.ts +53 -117
- package/scripts/framework-menu-consumer-runtime-audit.ts +66 -0
- package/scripts/framework-menu-consumer-runtime-menu.ts +4 -4
- package/scripts/framework-menu-gate-lib.ts +86 -1
- package/scripts/framework-menu-layout-data.ts +3 -3
- package/scripts/framework-menu-legacy-audit-render-sections.ts +6 -0
- package/scripts/framework-menu.ts +10 -6
- package/scripts/package-install-smoke-consumer-npm-lib.ts +10 -4
- package/scripts/package-install-smoke-lifecycle-lib.ts +19 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [6.3.272] - 2026-05-18
|
|
4
|
+
|
|
5
|
+
- Fix lifecycle PRE_WRITE validated-diff lease materialization: `pumuki sdd validate --stage=PRE_WRITE` refreshes evidence with PRE_WRITE semantics instead of self-blocking through PRE_COMMIT/PRE_PUSH before the lease exists.
|
|
6
|
+
|
|
7
|
+
## [6.3.271] - 2026-05-18
|
|
8
|
+
|
|
9
|
+
- Lifecycle: PRE_WRITE can now materialize a `validated-diff` lease after SDD and AI Gate allow an already staged slice; PRE_COMMIT/PRE_PUSH accept it only while the current code paths match the validated diff, fixing RuralGo `PUMUKI-INC-142` without opening a bypass.
|
|
10
|
+
|
|
3
11
|
## [6.3.267] - 2026-05-14
|
|
4
12
|
|
|
5
13
|
- iOS: `skills.ios.no-dispatchsemaphore` now emits actionable AST-style evidence for `DispatchSemaphore` usage, including exact lines, primary/related nodes and remediation toward `TaskGroup`, `AsyncStream` or explicit async boundaries.
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
v6.3.
|
|
1
|
+
v6.3.272
|
|
@@ -6,17 +6,36 @@ import {
|
|
|
6
6
|
findKotlinLiskovSubstitutionMatch,
|
|
7
7
|
findKotlinOpenClosedWhenMatch,
|
|
8
8
|
findKotlinPresentationSrpMatch,
|
|
9
|
+
hasAndroidAsyncTaskUsage,
|
|
9
10
|
hasKotlinCoroutineTryCatchUsage,
|
|
10
11
|
hasKotlinDispatcherMainBoundaryLeakUsage,
|
|
11
12
|
hasKotlinGlobalScopeUsage,
|
|
13
|
+
hasKotlinGodActivityUsage,
|
|
12
14
|
hasKotlinHardcodedBackgroundDispatcherUsage,
|
|
15
|
+
hasKotlinIncompleteMaterialThemeUsage,
|
|
13
16
|
hasKotlinJUnit4Usage,
|
|
17
|
+
hasKotlinHardcodedUiStringUsage,
|
|
18
|
+
collectKotlinHardcodedUiStringLines,
|
|
19
|
+
hasKotlinImperativeNavigationUsage,
|
|
20
|
+
hasKotlinLaunchedEffectBusyLoopUsage,
|
|
14
21
|
hasKotlinLiveDataStateExposureUsage,
|
|
15
22
|
hasKotlinLifecycleScopeUsage,
|
|
23
|
+
hasKotlinLegacyBottomNavigationUsage,
|
|
24
|
+
hasKotlinComposableObjectCreationWithoutRememberUsage,
|
|
25
|
+
hasKotlinComposableStateCreationWithoutRememberUsage,
|
|
26
|
+
hasKotlinForceUnwrapUsage,
|
|
27
|
+
hasKotlinFontScaleDisabledUsage,
|
|
16
28
|
hasKotlinProductionMockUsage,
|
|
29
|
+
hasKotlinNonLazyScrollableCollectionUsage,
|
|
30
|
+
hasKotlinProductionLoggingUsage,
|
|
17
31
|
hasKotlinSharedPreferencesUsage,
|
|
32
|
+
hasKotlinSharedFlowUsedAsStateUsage,
|
|
33
|
+
hasKotlinUnstableLaunchedEffectKeyUsage,
|
|
34
|
+
hasKotlinViewModelFlowWithoutStateInUsage,
|
|
18
35
|
hasKotlinWithContextUsage,
|
|
19
36
|
hasKotlinManualCoroutineScopeInViewModelUsage,
|
|
37
|
+
hasKotlinMissingContentDescriptionUsage,
|
|
38
|
+
hasKotlinModifierBackgroundBeforePaddingUsage,
|
|
20
39
|
hasKotlinRunBlockingUsage,
|
|
21
40
|
hasKotlinSupervisorScopeUsage,
|
|
22
41
|
hasKotlinThreadSleepCall,
|
|
@@ -102,6 +121,446 @@ fun main() {
|
|
|
102
121
|
assert.equal(hasKotlinRunBlockingUsage(partialSource), false);
|
|
103
122
|
});
|
|
104
123
|
|
|
124
|
+
test('hasAndroidAsyncTaskUsage detecta AsyncTask legacy en Kotlin y Java', () => {
|
|
125
|
+
const kotlinSource = `
|
|
126
|
+
class LegacySyncTask : AsyncTask<Unit, Unit, Unit>() {
|
|
127
|
+
override fun doInBackground(vararg params: Unit) = Unit
|
|
128
|
+
}
|
|
129
|
+
`;
|
|
130
|
+
const javaSource = `
|
|
131
|
+
class LegacySyncTask extends android.os.AsyncTask<Void, Void, Void> {
|
|
132
|
+
}
|
|
133
|
+
`;
|
|
134
|
+
const executorSource = `
|
|
135
|
+
fun executeLegacy(task: Runnable) {
|
|
136
|
+
AsyncTask.THREAD_POOL_EXECUTOR.execute(task)
|
|
137
|
+
}
|
|
138
|
+
`;
|
|
139
|
+
assert.equal(hasAndroidAsyncTaskUsage(kotlinSource), true);
|
|
140
|
+
assert.equal(hasAndroidAsyncTaskUsage(javaSource), true);
|
|
141
|
+
assert.equal(hasAndroidAsyncTaskUsage(executorSource), true);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('hasAndroidAsyncTaskUsage ignora imports, comentarios y strings', () => {
|
|
145
|
+
const source = `
|
|
146
|
+
import android.os.AsyncTask
|
|
147
|
+
// class LegacySyncTask : AsyncTask<Unit, Unit, Unit>()
|
|
148
|
+
val sample = "AsyncTask.THREAD_POOL_EXECUTOR"
|
|
149
|
+
class ModernSyncTask {
|
|
150
|
+
suspend fun execute() = coroutineScope { }
|
|
151
|
+
}
|
|
152
|
+
`;
|
|
153
|
+
assert.equal(hasAndroidAsyncTaskUsage(source), false);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('hasKotlinGodActivityUsage detecta Activity con UI, red y persistencia mezcladas', () => {
|
|
157
|
+
const source = `
|
|
158
|
+
class CheckoutActivity : ComponentActivity() {
|
|
159
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
160
|
+
super.onCreate(savedInstanceState)
|
|
161
|
+
setContent { CheckoutScreen() }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
fun loadRemoteCheckout() {
|
|
165
|
+
OkHttpClient().newCall(request).execute()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
fun cacheCheckoutState() {
|
|
169
|
+
getSharedPreferences("checkout", MODE_PRIVATE).edit().apply()
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
`;
|
|
173
|
+
assert.equal(hasKotlinGodActivityUsage(source), true);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('hasKotlinGodActivityUsage permite Activity entrypoint delgada', () => {
|
|
177
|
+
const source = `
|
|
178
|
+
class MainActivity : ComponentActivity() {
|
|
179
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
180
|
+
super.onCreate(savedInstanceState)
|
|
181
|
+
setContent { AppRoot() }
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
`;
|
|
185
|
+
assert.equal(hasKotlinGodActivityUsage(source), false);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('hasKotlinNonLazyScrollableCollectionUsage detecta Column scrollable con iteracion de coleccion', () => {
|
|
189
|
+
const source = `
|
|
190
|
+
@Composable
|
|
191
|
+
fun OrdersScreen(orders: List<Order>) {
|
|
192
|
+
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
|
193
|
+
orders.forEach { order ->
|
|
194
|
+
Text(order.name)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
`;
|
|
199
|
+
assert.equal(hasKotlinNonLazyScrollableCollectionUsage(source), true);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('hasKotlinNonLazyScrollableCollectionUsage permite LazyColumn y Column estatica', () => {
|
|
203
|
+
const lazySource = `
|
|
204
|
+
@Composable
|
|
205
|
+
fun OrdersScreen(orders: List<Order>) {
|
|
206
|
+
LazyColumn {
|
|
207
|
+
items(orders) { order -> Text(order.name) }
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
`;
|
|
211
|
+
const staticSource = `
|
|
212
|
+
@Composable
|
|
213
|
+
fun EmptyState() {
|
|
214
|
+
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
|
215
|
+
Text("One")
|
|
216
|
+
Text("Two")
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
`;
|
|
220
|
+
assert.equal(hasKotlinNonLazyScrollableCollectionUsage(lazySource), false);
|
|
221
|
+
assert.equal(hasKotlinNonLazyScrollableCollectionUsage(staticSource), false);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('hasKotlinUnstableLaunchedEffectKeyUsage detecta claves constantes o ausentes', () => {
|
|
225
|
+
const unitSource = `
|
|
226
|
+
@Composable
|
|
227
|
+
fun OrdersScreen(viewModel: OrdersViewModel) {
|
|
228
|
+
LaunchedEffect(Unit) { viewModel.load() }
|
|
229
|
+
}
|
|
230
|
+
`;
|
|
231
|
+
const trueSource = `
|
|
232
|
+
@Composable
|
|
233
|
+
fun OrdersScreen(viewModel: OrdersViewModel) {
|
|
234
|
+
LaunchedEffect(true) { viewModel.load() }
|
|
235
|
+
}
|
|
236
|
+
`;
|
|
237
|
+
const emptySource = `
|
|
238
|
+
@Composable
|
|
239
|
+
fun OrdersScreen(viewModel: OrdersViewModel) {
|
|
240
|
+
LaunchedEffect() { viewModel.load() }
|
|
241
|
+
}
|
|
242
|
+
`;
|
|
243
|
+
assert.equal(hasKotlinUnstableLaunchedEffectKeyUsage(unitSource), true);
|
|
244
|
+
assert.equal(hasKotlinUnstableLaunchedEffectKeyUsage(trueSource), true);
|
|
245
|
+
assert.equal(hasKotlinUnstableLaunchedEffectKeyUsage(emptySource), true);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('hasKotlinUnstableLaunchedEffectKeyUsage permite claves de estado estables e ignora comentarios', () => {
|
|
249
|
+
const source = `
|
|
250
|
+
// LaunchedEffect(Unit) { debug() }
|
|
251
|
+
val sample = "LaunchedEffect(true) { debug() }"
|
|
252
|
+
@Composable
|
|
253
|
+
fun OrdersScreen(orderId: String, viewModel: OrdersViewModel) {
|
|
254
|
+
LaunchedEffect(orderId) { viewModel.load(orderId) }
|
|
255
|
+
}
|
|
256
|
+
`;
|
|
257
|
+
assert.equal(hasKotlinUnstableLaunchedEffectKeyUsage(source), false);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test('hasKotlinLaunchedEffectBusyLoopUsage detecta bucle no cooperativo dentro de LaunchedEffect', () => {
|
|
261
|
+
const source = `
|
|
262
|
+
@Composable
|
|
263
|
+
fun SyncPulse(viewModel: SyncViewModel) {
|
|
264
|
+
LaunchedEffect(viewModel.sessionId) {
|
|
265
|
+
while (true) {
|
|
266
|
+
viewModel.poll()
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
`;
|
|
271
|
+
assert.equal(hasKotlinLaunchedEffectBusyLoopUsage(source), true);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test('hasKotlinLaunchedEffectBusyLoopUsage permite bucles cooperativos e ignora comentarios y strings', () => {
|
|
275
|
+
const source = `
|
|
276
|
+
// LaunchedEffect(Unit) { while (true) { poll() } }
|
|
277
|
+
val sample = "LaunchedEffect(Unit) { while (isActive) { poll() } }"
|
|
278
|
+
@Composable
|
|
279
|
+
fun SyncPulse(viewModel: SyncViewModel) {
|
|
280
|
+
LaunchedEffect(viewModel.sessionId) {
|
|
281
|
+
while (isActive) {
|
|
282
|
+
viewModel.poll()
|
|
283
|
+
delay(1000)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
`;
|
|
288
|
+
assert.equal(hasKotlinLaunchedEffectBusyLoopUsage(source), false);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test('hasKotlinProductionLoggingUsage detecta logs directos de produccion', () => {
|
|
292
|
+
const source = `
|
|
293
|
+
class CheckoutRepository {
|
|
294
|
+
fun load() {
|
|
295
|
+
println("loading")
|
|
296
|
+
System.out.println("debug")
|
|
297
|
+
Log.d("Checkout", "loading")
|
|
298
|
+
Timber.e(error)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
`;
|
|
302
|
+
assert.equal(hasKotlinProductionLoggingUsage(source), true);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test('hasKotlinProductionLoggingUsage permite imports, comentarios, strings y logs guardados por debug', () => {
|
|
306
|
+
const source = `
|
|
307
|
+
import android.util.Log
|
|
308
|
+
// Log.d("Checkout", "debug")
|
|
309
|
+
val sample = "println(\\"debug\\")"
|
|
310
|
+
class CheckoutRepository {
|
|
311
|
+
fun load() {
|
|
312
|
+
if (BuildConfig.DEBUG) Timber.d("loading")
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
`;
|
|
316
|
+
assert.equal(hasKotlinProductionLoggingUsage(source), false);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test('hasKotlinModifierBackgroundBeforePaddingUsage detecta background antes de padding', () => {
|
|
320
|
+
const source = `
|
|
321
|
+
@Composable
|
|
322
|
+
fun CheckoutCard() {
|
|
323
|
+
Box(
|
|
324
|
+
modifier = Modifier
|
|
325
|
+
.fillMaxWidth()
|
|
326
|
+
.background(Color.Red)
|
|
327
|
+
.padding(16.dp)
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
`;
|
|
331
|
+
assert.equal(hasKotlinModifierBackgroundBeforePaddingUsage(source), true);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test('hasKotlinModifierBackgroundBeforePaddingUsage permite padding antes de background e ignora comentarios', () => {
|
|
335
|
+
const source = `
|
|
336
|
+
// Modifier.background(Color.Red).padding(16.dp)
|
|
337
|
+
val sample = "Modifier.background(Color.Red).padding(16.dp)"
|
|
338
|
+
@Composable
|
|
339
|
+
fun CheckoutCard() {
|
|
340
|
+
Box(
|
|
341
|
+
modifier = Modifier
|
|
342
|
+
.padding(16.dp)
|
|
343
|
+
.background(Color.Red)
|
|
344
|
+
)
|
|
345
|
+
}
|
|
346
|
+
`;
|
|
347
|
+
assert.equal(hasKotlinModifierBackgroundBeforePaddingUsage(source), false);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test('hasKotlinMissingContentDescriptionUsage detecta Image e Icon sin contentDescription', () => {
|
|
351
|
+
const source = `
|
|
352
|
+
@Composable
|
|
353
|
+
fun CheckoutIcon() {
|
|
354
|
+
Image(painter = painterResource(R.drawable.checkout), modifier = Modifier.size(24.dp))
|
|
355
|
+
Icon(Icons.Default.Warning, tint = Color.Red)
|
|
356
|
+
}
|
|
357
|
+
`;
|
|
358
|
+
assert.equal(hasKotlinMissingContentDescriptionUsage(source), true);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test('hasKotlinMissingContentDescriptionUsage permite contentDescription explicita e ignora comentarios', () => {
|
|
362
|
+
const source = `
|
|
363
|
+
// Image(painter = painterResource(R.drawable.checkout))
|
|
364
|
+
val sample = "Icon(Icons.Default.Warning)"
|
|
365
|
+
@Composable
|
|
366
|
+
fun CheckoutIcon() {
|
|
367
|
+
Image(
|
|
368
|
+
painter = painterResource(R.drawable.checkout),
|
|
369
|
+
contentDescription = stringResource(R.string.checkout_icon)
|
|
370
|
+
)
|
|
371
|
+
Icon(
|
|
372
|
+
imageVector = Icons.Default.Warning,
|
|
373
|
+
contentDescription = null
|
|
374
|
+
)
|
|
375
|
+
}
|
|
376
|
+
`;
|
|
377
|
+
assert.equal(hasKotlinMissingContentDescriptionUsage(source), false);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test('hasKotlinFontScaleDisabledUsage detecta desactivacion del fontScale del sistema', () => {
|
|
381
|
+
const source = `
|
|
382
|
+
@Composable
|
|
383
|
+
fun FixedTextScale(content: @Composable () -> Unit) {
|
|
384
|
+
val density = LocalDensity.current
|
|
385
|
+
CompositionLocalProvider(LocalDensity provides Density(density.density, fontScale = 1f)) {
|
|
386
|
+
content()
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
`;
|
|
390
|
+
assert.equal(hasKotlinFontScaleDisabledUsage(source), true);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
test('hasKotlinFontScaleDisabledUsage permite LocalDensity sin forzar fontScale e ignora comentarios', () => {
|
|
394
|
+
const source = `
|
|
395
|
+
// Density(density.density, fontScale = 1f)
|
|
396
|
+
val sample = "fontScale = 1f"
|
|
397
|
+
@Composable
|
|
398
|
+
fun ScalableText(content: @Composable () -> Unit) {
|
|
399
|
+
val density = LocalDensity.current
|
|
400
|
+
CompositionLocalProvider(LocalDensity provides density) {
|
|
401
|
+
content()
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
`;
|
|
405
|
+
assert.equal(hasKotlinFontScaleDisabledUsage(source), false);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test('hasKotlinIncompleteMaterialThemeUsage detecta MaterialTheme incompleto', () => {
|
|
409
|
+
const source = `
|
|
410
|
+
@Composable
|
|
411
|
+
fun AppTheme(content: @Composable () -> Unit) {
|
|
412
|
+
MaterialTheme(
|
|
413
|
+
colorScheme = AppColorScheme,
|
|
414
|
+
content = content
|
|
415
|
+
)
|
|
416
|
+
}
|
|
417
|
+
`;
|
|
418
|
+
assert.equal(hasKotlinIncompleteMaterialThemeUsage(source), true);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
test('hasKotlinIncompleteMaterialThemeUsage permite MaterialTheme con colorScheme typography y shapes', () => {
|
|
422
|
+
const source = `
|
|
423
|
+
// MaterialTheme(colorScheme = AppColorScheme)
|
|
424
|
+
val sample = "MaterialTheme(colorScheme = AppColorScheme)"
|
|
425
|
+
@Composable
|
|
426
|
+
fun AppTheme(content: @Composable () -> Unit) {
|
|
427
|
+
MaterialTheme(
|
|
428
|
+
colorScheme = AppColorScheme,
|
|
429
|
+
typography = AppTypography,
|
|
430
|
+
shapes = AppShapes,
|
|
431
|
+
content = content
|
|
432
|
+
)
|
|
433
|
+
}
|
|
434
|
+
`;
|
|
435
|
+
assert.equal(hasKotlinIncompleteMaterialThemeUsage(source), false);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
test('hasKotlinLegacyBottomNavigationUsage detecta BottomNavigation legacy de Material 2', () => {
|
|
439
|
+
const source = `
|
|
440
|
+
@Composable
|
|
441
|
+
fun LegacyBottomBar() {
|
|
442
|
+
BottomNavigation {
|
|
443
|
+
BottomNavigationItem(selected = true, onClick = {}, icon = { Text("Home") })
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
`;
|
|
447
|
+
assert.equal(hasKotlinLegacyBottomNavigationUsage(source), true);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
test('hasKotlinLegacyBottomNavigationUsage permite NavigationBar Material 3 e ignora comentarios', () => {
|
|
451
|
+
const source = `
|
|
452
|
+
// BottomNavigation { }
|
|
453
|
+
val sample = "BottomNavigationItem(selected = true)"
|
|
454
|
+
@Composable
|
|
455
|
+
fun ModernBottomBar() {
|
|
456
|
+
NavigationBar {
|
|
457
|
+
NavigationBarItem(selected = true, onClick = {}, icon = { Text("Home") })
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
`;
|
|
461
|
+
assert.equal(hasKotlinLegacyBottomNavigationUsage(source), false);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
test('hasKotlinImperativeNavigationUsage detecta navegacion imperative legacy', () => {
|
|
465
|
+
const source = `
|
|
466
|
+
class CheckoutActivity : ComponentActivity() {
|
|
467
|
+
fun openOrder() {
|
|
468
|
+
startActivity(Intent(this, OrderActivity::class.java))
|
|
469
|
+
supportFragmentManager.beginTransaction().replace(R.id.container, OrderFragment()).commit()
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
`;
|
|
473
|
+
assert.equal(hasKotlinImperativeNavigationUsage(source), true);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
test('hasKotlinImperativeNavigationUsage permite Navigation Compose e ignora comentarios y strings', () => {
|
|
477
|
+
const source = `
|
|
478
|
+
// startActivity(Intent(this, LegacyActivity::class.java))
|
|
479
|
+
val sample = "supportFragmentManager.beginTransaction()"
|
|
480
|
+
@Composable
|
|
481
|
+
fun AppNav() {
|
|
482
|
+
val navController = rememberNavController()
|
|
483
|
+
NavHost(navController = navController, startDestination = "home") { }
|
|
484
|
+
}
|
|
485
|
+
`;
|
|
486
|
+
assert.equal(hasKotlinImperativeNavigationUsage(source), false);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
test('hasKotlinComposableObjectCreationWithoutRememberUsage detecta objetos recreados en Composable', () => {
|
|
490
|
+
const source = `
|
|
491
|
+
@Composable
|
|
492
|
+
fun PriceLabel(value: BigDecimal) {
|
|
493
|
+
val formatter = DecimalFormat("#.00")
|
|
494
|
+
Text(formatter.format(value))
|
|
495
|
+
}
|
|
496
|
+
`;
|
|
497
|
+
assert.equal(hasKotlinComposableObjectCreationWithoutRememberUsage(source), true);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
test('hasKotlinComposableObjectCreationWithoutRememberUsage permite remember e ignora codigo no Composable', () => {
|
|
501
|
+
const source = `
|
|
502
|
+
fun buildFormatter() {
|
|
503
|
+
val formatter = DecimalFormat("#.00")
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
@Composable
|
|
507
|
+
fun PriceLabel(value: BigDecimal) {
|
|
508
|
+
val formatter = remember { DecimalFormat("#.00") }
|
|
509
|
+
Text(formatter.format(value))
|
|
510
|
+
}
|
|
511
|
+
`;
|
|
512
|
+
assert.equal(hasKotlinComposableObjectCreationWithoutRememberUsage(source), false);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
test('hasKotlinComposableStateCreationWithoutRememberUsage detecta estado creado sin remember', () => {
|
|
516
|
+
const source = `
|
|
517
|
+
@Composable
|
|
518
|
+
fun SearchBox() {
|
|
519
|
+
val query = mutableStateOf("")
|
|
520
|
+
val filtered = derivedStateOf { query.value.trim() }
|
|
521
|
+
Text(filtered.value)
|
|
522
|
+
}
|
|
523
|
+
`;
|
|
524
|
+
assert.equal(hasKotlinComposableStateCreationWithoutRememberUsage(source), true);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
test('hasKotlinComposableStateCreationWithoutRememberUsage permite remember e ignora codigo no Composable', () => {
|
|
528
|
+
const source = `
|
|
529
|
+
fun buildState() {
|
|
530
|
+
val query = mutableStateOf("")
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
@Composable
|
|
534
|
+
fun SearchBox() {
|
|
535
|
+
val query = remember { mutableStateOf("") }
|
|
536
|
+
val filtered = remember { derivedStateOf { query.value.trim() } }
|
|
537
|
+
Text(filtered.value)
|
|
538
|
+
}
|
|
539
|
+
`;
|
|
540
|
+
assert.equal(hasKotlinComposableStateCreationWithoutRememberUsage(source), false);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
test('hasKotlinForceUnwrapUsage detecta force unwrap Kotlin', () => {
|
|
544
|
+
const source = `
|
|
545
|
+
class CheckoutState {
|
|
546
|
+
fun title(value: String?) = value!!.trim()
|
|
547
|
+
}
|
|
548
|
+
`;
|
|
549
|
+
assert.equal(hasKotlinForceUnwrapUsage(source), true);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
test('hasKotlinForceUnwrapUsage ignora comentarios, strings y operadores no unwrap', () => {
|
|
553
|
+
const source = `
|
|
554
|
+
// value!!.trim()
|
|
555
|
+
val sample = "value!!.trim()"
|
|
556
|
+
class CheckoutState {
|
|
557
|
+
fun same(left: String, right: String) = left !== right
|
|
558
|
+
fun different(left: String, right: String) = left != right
|
|
559
|
+
}
|
|
560
|
+
`;
|
|
561
|
+
assert.equal(hasKotlinForceUnwrapUsage(source), false);
|
|
562
|
+
});
|
|
563
|
+
|
|
105
564
|
test('hasKotlinLiveDataStateExposureUsage detecta LiveData y MutableLiveData como estado observable legacy', () => {
|
|
106
565
|
const source = `
|
|
107
566
|
class OrdersViewModel : ViewModel() {
|
|
@@ -124,6 +583,52 @@ class OrdersViewModel : ViewModel() {
|
|
|
124
583
|
assert.equal(hasKotlinLiveDataStateExposureUsage(source), false);
|
|
125
584
|
});
|
|
126
585
|
|
|
586
|
+
test('hasKotlinViewModelFlowWithoutStateInUsage detecta Flow expuesto desde ViewModel sin stateIn', () => {
|
|
587
|
+
const source = `
|
|
588
|
+
class OrdersViewModel : ViewModel() {
|
|
589
|
+
val state: Flow<OrdersUiState> = repository.observeOrders().map { OrdersUiState(it) }
|
|
590
|
+
}
|
|
591
|
+
`;
|
|
592
|
+
assert.equal(hasKotlinViewModelFlowWithoutStateInUsage(source), true);
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
test('hasKotlinViewModelFlowWithoutStateInUsage permite stateIn e ignora imports, comentarios y strings', () => {
|
|
596
|
+
const source = `
|
|
597
|
+
import kotlinx.coroutines.flow.Flow
|
|
598
|
+
// val state: Flow<OrdersUiState> = repository.observeOrders()
|
|
599
|
+
val sample = "val state: Flow<OrdersUiState>"
|
|
600
|
+
class OrdersViewModel : ViewModel() {
|
|
601
|
+
val state: StateFlow<OrdersUiState> = repository.observeOrders()
|
|
602
|
+
.map { OrdersUiState(it) }
|
|
603
|
+
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), OrdersUiState())
|
|
604
|
+
}
|
|
605
|
+
`;
|
|
606
|
+
assert.equal(hasKotlinViewModelFlowWithoutStateInUsage(source), false);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
test('hasKotlinSharedFlowUsedAsStateUsage detecta SharedFlow usado como estado de ViewModel', () => {
|
|
610
|
+
const source = `
|
|
611
|
+
class OrdersViewModel : ViewModel() {
|
|
612
|
+
val uiState: SharedFlow<OrdersUiState> = MutableSharedFlow()
|
|
613
|
+
private val _screenState: MutableSharedFlow<CheckoutState> = MutableSharedFlow()
|
|
614
|
+
}
|
|
615
|
+
`;
|
|
616
|
+
assert.equal(hasKotlinSharedFlowUsedAsStateUsage(source), true);
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
test('hasKotlinSharedFlowUsedAsStateUsage permite SharedFlow de eventos y StateFlow de estado', () => {
|
|
620
|
+
const source = `
|
|
621
|
+
import kotlinx.coroutines.flow.SharedFlow
|
|
622
|
+
// val uiState: SharedFlow<OrdersUiState> = MutableSharedFlow()
|
|
623
|
+
val sample = "val state: SharedFlow<OrdersUiState>"
|
|
624
|
+
class OrdersViewModel : ViewModel() {
|
|
625
|
+
val uiState: StateFlow<OrdersUiState> = MutableStateFlow(OrdersUiState())
|
|
626
|
+
val events: SharedFlow<OrdersEvent> = MutableSharedFlow()
|
|
627
|
+
}
|
|
628
|
+
`;
|
|
629
|
+
assert.equal(hasKotlinSharedFlowUsedAsStateUsage(source), false);
|
|
630
|
+
});
|
|
631
|
+
|
|
127
632
|
test('hasKotlinManualCoroutineScopeInViewModelUsage detecta CoroutineScope manual dentro de ViewModel', () => {
|
|
128
633
|
const source = `
|
|
129
634
|
class OrdersViewModel : ViewModel() {
|
|
@@ -352,6 +857,39 @@ class OrdersRepositoryFactory {
|
|
|
352
857
|
assert.equal(hasKotlinProductionMockUsage(source), false);
|
|
353
858
|
});
|
|
354
859
|
|
|
860
|
+
test('hasKotlinHardcodedUiStringUsage detecta textos UI hardcodeados en Compose', () => {
|
|
861
|
+
const source = `
|
|
862
|
+
@Composable
|
|
863
|
+
fun CheckoutHeader() {
|
|
864
|
+
Text("Comprar ahora")
|
|
865
|
+
Icon(
|
|
866
|
+
imageVector = Icons.Default.Warning,
|
|
867
|
+
contentDescription = "Aviso"
|
|
868
|
+
)
|
|
869
|
+
}
|
|
870
|
+
`;
|
|
871
|
+
assert.equal(hasKotlinHardcodedUiStringUsage(source), true);
|
|
872
|
+
assert.deepEqual(collectKotlinHardcodedUiStringLines(source), [4, 7]);
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
test('hasKotlinHardcodedUiStringUsage ignora imports, comentarios y strings localizados', () => {
|
|
876
|
+
const source = `
|
|
877
|
+
import androidx.compose.material3.Text
|
|
878
|
+
// Text("Debug")
|
|
879
|
+
val sample = "Text(\\"Debug\\")"
|
|
880
|
+
@Composable
|
|
881
|
+
fun CheckoutHeader() {
|
|
882
|
+
Text(stringResource(R.string.checkout_title))
|
|
883
|
+
Icon(
|
|
884
|
+
imageVector = Icons.Default.Warning,
|
|
885
|
+
contentDescription = stringResource(R.string.warning)
|
|
886
|
+
)
|
|
887
|
+
}
|
|
888
|
+
`;
|
|
889
|
+
assert.equal(hasKotlinHardcodedUiStringUsage(source), false);
|
|
890
|
+
assert.deepEqual(collectKotlinHardcodedUiStringLines(source), []);
|
|
891
|
+
});
|
|
892
|
+
|
|
355
893
|
test('hasKotlinSupervisorScopeUsage detecta supervisorScope con parentesis y llaves', () => {
|
|
356
894
|
const parenthesesSource = `
|
|
357
895
|
class SyncOrdersUseCase {
|