pumuki 6.3.171 → 6.3.173

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.
Files changed (120) hide show
  1. package/AGENTS.md +1 -16
  2. package/CHANGELOG.md +0 -101
  3. package/README.md +10 -14
  4. package/VERSION +1 -1
  5. package/core/facts/detectors/text/android.test.ts +0 -2827
  6. package/core/facts/detectors/text/android.ts +182 -5121
  7. package/core/facts/detectors/text/ios.test.ts +12 -290
  8. package/core/facts/detectors/text/ios.ts +28 -301
  9. package/core/facts/detectors/typescript/index.test.ts +139 -3733
  10. package/core/facts/detectors/typescript/index.ts +264 -4959
  11. package/core/facts/extractHeuristicFacts.ts +11 -328
  12. package/core/gate/evaluateRules.test.ts +0 -7
  13. package/core/gate/evaluateRules.ts +2 -1
  14. package/core/rules/presets/heuristics/android.test.ts +1 -399
  15. package/core/rules/presets/heuristics/android.ts +1 -1481
  16. package/core/rules/presets/heuristics/ios.test.ts +1 -11
  17. package/core/rules/presets/heuristics/ios.ts +0 -36
  18. package/core/rules/presets/heuristics/typescript.test.ts +2 -158
  19. package/core/rules/presets/heuristics/typescript.ts +0 -508
  20. package/core/rules/presets/iosEnterpriseRuleSet.test.ts +0 -5
  21. package/core/rules/presets/iosEnterpriseRuleSet.ts +5 -5
  22. package/docs/README.md +3 -3
  23. package/docs/operations/RELEASE_NOTES.md +1 -94
  24. package/docs/operations/framework-menu-consumer-walkthrough.md +15 -18
  25. package/docs/product/API_REFERENCE.md +1 -1
  26. package/docs/product/CONFIGURATION.md +0 -7
  27. package/docs/product/USAGE.md +1 -1
  28. package/docs/validation/README.md +1 -3
  29. package/docs/validation/ios-avdlee-parity-matrix.md +1 -1
  30. package/integrations/config/skillsCompilerTemplates.test.ts +0 -145
  31. package/integrations/config/skillsCompilerTemplates.ts +2 -1013
  32. package/integrations/config/skillsDetectorRegistry.ts +8 -523
  33. package/integrations/config/skillsMarkdownRules.ts +8 -1088
  34. package/integrations/config/skillsRuleSet.ts +3 -44
  35. package/integrations/evidence/buildEvidence.ts +5 -34
  36. package/integrations/evidence/platformSummary.test.ts +9 -73
  37. package/integrations/evidence/platformSummary.ts +7 -165
  38. package/integrations/evidence/repoState.ts +0 -3
  39. package/integrations/evidence/rulesCoverage.ts +0 -83
  40. package/integrations/evidence/schema.ts +0 -29
  41. package/integrations/evidence/writeEvidence.test.ts +0 -4
  42. package/integrations/evidence/writeEvidence.ts +2 -41
  43. package/integrations/gate/evaluateAiGate.ts +8 -312
  44. package/integrations/gate/remediationCatalog.ts +2 -20
  45. package/integrations/gate/stagePolicies.ts +18 -24
  46. package/integrations/git/astIntelligenceDualValidation.ts +2 -2
  47. package/integrations/git/gitAtomicity.ts +39 -284
  48. package/integrations/git/resolveGitRefs.ts +6 -35
  49. package/integrations/git/runPlatformGate.ts +143 -512
  50. package/integrations/git/runPlatformGateOutput.ts +8 -13
  51. package/integrations/git/stageRunners.ts +41 -26
  52. package/integrations/lifecycle/adapter.ts +0 -24
  53. package/integrations/lifecycle/audit.ts +49 -14
  54. package/integrations/lifecycle/cli.ts +20 -37
  55. package/integrations/lifecycle/cliSdd.ts +3 -4
  56. package/integrations/lifecycle/doctor.ts +1 -1
  57. package/integrations/lifecycle/packageInfo.ts +1 -118
  58. package/integrations/lifecycle/policyReconcile.ts +4 -27
  59. package/integrations/lifecycle/preWriteAutomation.ts +5 -5
  60. package/integrations/lifecycle/state.ts +1 -8
  61. package/integrations/lifecycle/watch.ts +8 -28
  62. package/integrations/mcp/aiGateCheck.ts +10 -194
  63. package/integrations/mcp/autoExecuteAiStart.ts +4 -7
  64. package/integrations/mcp/enterpriseServer.ts +3 -19
  65. package/integrations/mcp/preFlightCheck.ts +10 -89
  66. package/integrations/policy/gitAtomicityEnforcement.ts +2 -2
  67. package/integrations/policy/heuristicsEnforcement.ts +2 -2
  68. package/integrations/policy/policyProfiles.ts +18 -24
  69. package/integrations/policy/preWriteEnforcement.ts +1 -1
  70. package/integrations/policy/sddCompletenessEnforcement.ts +2 -2
  71. package/integrations/policy/skillsEnforcement.ts +47 -1
  72. package/integrations/policy/tddBddEnforcement.ts +2 -2
  73. package/integrations/sdd/evidenceScaffold.ts +8 -124
  74. package/integrations/tdd/contract.ts +0 -1
  75. package/integrations/tdd/enforcement.ts +0 -103
  76. package/integrations/tdd/types.ts +0 -6
  77. package/package.json +1 -1
  78. package/scripts/check-tracking-single-active.sh +1 -1
  79. package/scripts/framework-menu-advanced-view-lib.ts +0 -49
  80. package/scripts/framework-menu-consumer-actions-lib.ts +32 -32
  81. package/scripts/framework-menu-consumer-preflight-render.ts +0 -10
  82. package/scripts/framework-menu-consumer-preflight-run.ts +5 -31
  83. package/scripts/framework-menu-consumer-preflight-types.ts +0 -12
  84. package/scripts/framework-menu-consumer-runtime-actions.ts +5 -11
  85. package/scripts/framework-menu-consumer-runtime-audit.ts +28 -0
  86. package/scripts/framework-menu-consumer-runtime-evidence-classic.ts +42 -118
  87. package/scripts/framework-menu-consumer-runtime-lib.ts +0 -38
  88. package/scripts/framework-menu-consumer-runtime-menu.ts +15 -55
  89. package/scripts/framework-menu-consumer-runtime-types.ts +0 -4
  90. package/scripts/framework-menu-evidence-summary-read.ts +1 -17
  91. package/scripts/framework-menu-evidence-summary-types.ts +0 -3
  92. package/scripts/framework-menu-layout-data.ts +23 -3
  93. package/scripts/framework-menu-system-notifications-cause.ts +1 -24
  94. package/scripts/framework-menu-system-notifications-env.ts +0 -8
  95. package/scripts/framework-menu-system-notifications-gate.ts +2 -9
  96. package/scripts/framework-menu-system-notifications-macos-applescript-dialog.ts +1 -1
  97. package/scripts/framework-menu-system-notifications-macos-dialog-payload.ts +2 -14
  98. package/scripts/framework-menu-system-notifications-macos-swift-source.ts +1 -1
  99. package/scripts/framework-menu-system-notifications-payloads-blocked.ts +4 -128
  100. package/scripts/framework-menu-system-notifications-payloads.ts +1 -8
  101. package/scripts/framework-menu-system-notifications-remediation.ts +1 -15
  102. package/scripts/framework-menu-system-notifications-text.ts +1 -7
  103. package/scripts/framework-menu.ts +2 -37
  104. package/scripts/package-install-smoke-consumer-git-repo-lib.ts +1 -10
  105. package/scripts/package-install-smoke-consumer-npm-lib.ts +9 -46
  106. package/skills.lock.json +1244 -807
  107. package/integrations/evidence/trackingContract.ts +0 -17
  108. package/integrations/gate/blockingCause.ts +0 -40
  109. package/integrations/gate/governanceActionCatalog.ts +0 -296
  110. package/integrations/gate/runPlatformGateConfig.ts +0 -55
  111. package/integrations/gate/runPlatformGateDefaults.ts +0 -19
  112. package/integrations/lifecycle/bootstrapManifest.ts +0 -248
  113. package/integrations/lifecycle/cliGovernanceConsole.ts +0 -69
  114. package/integrations/lifecycle/governanceNextAction.ts +0 -181
  115. package/integrations/lifecycle/governanceObservationSnapshot.ts +0 -376
  116. package/integrations/lifecycle/trackingState.ts +0 -403
  117. package/integrations/mcp/alignedPlatformGate.ts +0 -248
  118. package/integrations/mcp/readMcpPrePushStdin.ts +0 -7
  119. package/scripts/build-ruralgo-s1-evidence-pack.ts +0 -85
  120. package/scripts/ruralgo-s1-evidence-pack-lib.ts +0 -200
@@ -1,145 +1,11 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import test from 'node:test';
3
3
  import {
4
- hasAndroidAndroidEntryPointUsage,
5
- hasAndroidComposableFunctionUsage,
6
- hasAndroidArgumentsUsage,
7
- hasAndroidCoroutineCallbackUsage,
8
- hasAndroidGodActivityUsage,
9
- hasAndroidStateFlowUsage,
10
- hasAndroidSingleSourceOfTruthUsage,
11
- hasAndroidSharedFlowUsage,
12
- hasAndroidFlowBuilderUsage,
13
- hasAndroidFlowCollectUsage,
14
- hasAndroidCollectAsStateUsage,
15
- hasAndroidRememberUsage,
16
- hasAndroidDerivedStateOfUsage,
17
- hasAndroidLaunchedEffectUsage,
18
- hasAndroidLaunchedEffectKeysUsage,
19
- hasAndroidDisposableEffectUsage,
20
- hasAndroidPreviewUsage,
21
- hasAndroidAdaptiveLayoutsUsage,
22
- hasAndroidExistingStructureUsage,
23
- hasAndroidThemeUsage,
24
- hasAndroidDarkThemeUsage,
25
- hasAndroidAccessibilityUsage,
26
- hasAndroidContentDescriptionUsage,
27
- hasAndroidTalkBackUsage,
28
- hasAndroidTextScalingUsage,
29
- hasAndroidTouchTargetsUsage,
30
- hasAndroidRecompositionUsage,
31
- hasAndroidUiStateUsage,
32
- hasAndroidStateHoistingUsage,
33
- hasAndroidRepositoryPatternUsage,
34
- hasAndroidOrdersRepUsage,
35
- hasAndroidUseCaseUsage,
36
- hasAndroidViewModelUsage,
37
- hasAndroidViewModelScopeUsage,
38
- hasAndroidSupervisorScopeUsage,
39
- hasAndroidAppStartupUsage,
40
- hasAndroidAnalyticsUsage,
41
- findAndroidProfilerMatch,
42
- hasAndroidProfilerUsage,
43
- hasAndroidBaselineProfilesUsage,
44
- hasAndroidSkipRecompositionUsage,
45
- hasAndroidStabilityUsage,
46
4
  findKotlinConcreteDependencyDipMatch,
47
5
  findKotlinInterfaceSegregationMatch,
48
6
  findKotlinLiskovSubstitutionMatch,
49
7
  findKotlinOpenClosedWhenMatch,
50
8
  findKotlinPresentationSrpMatch,
51
- hasAndroidAsyncTaskUsage,
52
- hasAndroidFindViewByIdUsage,
53
- hasAndroidHiltAndroidAppUsage,
54
- hasAndroidHiltDependencyUsage,
55
- hasAndroidWorkManagerDependencyUsage,
56
- hasAndroidWorkManagerBackgroundTaskUsage,
57
- hasAndroidHiltFrameworkUsage,
58
- hasAndroidInjectConstructorUsage,
59
- hasAndroidJavaSourceCode,
60
- hasAndroidDispatcherUsage,
61
- hasAndroidCoroutineTryCatchUsage,
62
- hasAndroidHardcodedStringUsage,
63
- hasAndroidBuildConfigConstantUsage,
64
- findAndroidStringsXmlMatch,
65
- findAndroidStringFormattingMatch,
66
- hasAndroidStringsXmlUsage,
67
- hasAndroidStringFormattingUsage,
68
- findAndroidPluralsXmlMatch,
69
- hasAndroidPluralsXmlUsage,
70
- hasAndroidNoConsoleLogUsage,
71
- hasAndroidTimberUsage,
72
- hasAndroidModuleInstallInUsage,
73
- hasAndroidBindsUsage,
74
- hasAndroidProvidesUsage,
75
- hasAndroidRxJavaUsage,
76
- hasAndroidSingletonUsage,
77
- hasAndroidInstrumentedTestUsage,
78
- hasAndroidAaaPatternUsage,
79
- hasAndroidGivenWhenThenUsage,
80
- hasAndroidJvmUnitTestUsage,
81
- hasAndroidVersionCatalogUsage,
82
- hasAndroidDaoSuspendFunctionsUsage,
83
- hasAndroidTransactionUsage,
84
- hasAndroidSuspendFunctionsApiServiceUsage,
85
- hasAndroidSuspendFunctionsAsyncUsage,
86
- hasAndroidAsyncAwaitParallelismUsage,
87
- hasAndroidSingleActivityComposeShellUsage,
88
- hasAndroidWithContextUsage,
89
- hasAndroidViewModelScopedUsage,
90
- findAndroidComposableFunctionMatch,
91
- findAndroidArgumentsMatch,
92
- findAndroidCoroutineCallbackMatch,
93
- findAndroidGodActivityMatch,
94
- findAndroidDaoSuspendFunctionsMatch,
95
- findAndroidSuspendFunctionsApiServiceMatch,
96
- findAndroidSuspendFunctionsAsyncMatch,
97
- findAndroidAsyncAwaitParallelismMatch,
98
- findAndroidStateFlowMatch,
99
- findAndroidSingleSourceOfTruthMatch,
100
- findAndroidSharedFlowMatch,
101
- findAndroidFlowBuilderMatch,
102
- findAndroidFlowCollectMatch,
103
- findAndroidCollectAsStateMatch,
104
- findAndroidRememberMatch,
105
- findAndroidDerivedStateOfMatch,
106
- findAndroidLaunchedEffectMatch,
107
- findAndroidLaunchedEffectKeysMatch,
108
- findAndroidDisposableEffectMatch,
109
- findAndroidPreviewMatch,
110
- findAndroidAdaptiveLayoutsMatch,
111
- findAndroidExistingStructureMatch,
112
- findAndroidThemeMatch,
113
- findAndroidDarkThemeMatch,
114
- findAndroidAccessibilityMatch,
115
- findAndroidContentDescriptionMatch,
116
- findAndroidTalkBackMatch,
117
- findAndroidTextScalingMatch,
118
- findAndroidTouchTargetsMatch,
119
- findAndroidRecompositionMatch,
120
- findAndroidUiStateMatch,
121
- findAndroidStateHoistingMatch,
122
- findAndroidRepositoryPatternMatch,
123
- findAndroidOrdersRepMatch,
124
- findAndroidUseCaseMatch,
125
- findAndroidViewModelMatch,
126
- findAndroidViewModelScopeMatch,
127
- findAndroidSupervisorScopeMatch,
128
- findAndroidAppStartupMatch,
129
- findAndroidAnalyticsMatch,
130
- findAndroidBaselineProfilesMatch,
131
- findAndroidSkipRecompositionMatch,
132
- findAndroidStabilityMatch,
133
- findAndroidInstrumentedTestMatch,
134
- findAndroidAaaPatternMatch,
135
- findAndroidGivenWhenThenMatch,
136
- findAndroidJvmUnitTestMatch,
137
- findAndroidVersionCatalogMatch,
138
- findAndroidWorkManagerDependencyMatch,
139
- findAndroidWorkManagerBackgroundTaskMatch,
140
- findAndroidSingleActivityComposeShellMatch,
141
- findAndroidTransactionMatch,
142
- hasKotlinForceUnwrapUsage,
143
9
  hasKotlinGlobalScopeUsage,
144
10
  hasKotlinRunBlockingUsage,
145
11
  hasKotlinThreadSleepCall,
@@ -225,2699 +91,6 @@ fun main() {
225
91
  assert.equal(hasKotlinRunBlockingUsage(partialSource), false);
226
92
  });
227
93
 
228
- test('findAndroidCoroutineCallbackMatch devuelve payload semantico para callbacks Android', () => {
229
- const source = `
230
- fun loadRemoteData() {
231
- service.enqueue()
232
- task.addOnSuccessListener { }
233
- task.addOnCompleteListener { }
234
- }
235
- `;
236
-
237
- const match = findAndroidCoroutineCallbackMatch(source);
238
-
239
- assert.ok(match);
240
- assert.deepEqual(match.primary_node, {
241
- kind: 'call',
242
- name: 'enqueue',
243
- lines: [3],
244
- });
245
- assert.deepEqual(match.related_nodes, [
246
- { kind: 'call', name: 'addOnSuccessListener', lines: [4] },
247
- { kind: 'call', name: 'addOnCompleteListener', lines: [5] },
248
- ]);
249
- assert.match(match.why, /callback/i);
250
- assert.match(match.impact, /callback|error|cancel/i);
251
- assert.match(match.expected_fix, /coroutines|Flow|suspend/i);
252
- });
253
-
254
- test('hasAndroidCoroutineCallbackUsage ignora comentarios, strings y llamadas no callback', () => {
255
- const source = `
256
- // service.enqueue()
257
- val sample = "addOnSuccessListener()"
258
- fun loadRemoteData() {
259
- loadCoroutine()
260
- }
261
- `;
262
-
263
- assert.equal(hasAndroidCoroutineCallbackUsage(source), false);
264
- });
265
-
266
- test('findAndroidStateFlowMatch devuelve payload semantico para StateFlow en ViewModel', () => {
267
- const source = `
268
- class CatalogViewModel : ViewModel() {
269
- private val _uiState = MutableStateFlow(CatalogUiState())
270
- val uiState: StateFlow<CatalogUiState> = _uiState.asStateFlow()
271
- }
272
- `;
273
-
274
- const match = findAndroidStateFlowMatch(source);
275
-
276
- assert.ok(match);
277
- assert.deepEqual(match.primary_node, {
278
- kind: 'class',
279
- name: 'CatalogViewModel',
280
- lines: [2],
281
- });
282
- assert.deepEqual(match.related_nodes, [
283
- { kind: 'property', name: 'MutableStateFlow', lines: [3] },
284
- { kind: 'property', name: 'StateFlow', lines: [4] },
285
- { kind: 'call', name: 'asStateFlow', lines: [4] },
286
- ]);
287
- assert.match(match.why, /StateFlow/i);
288
- assert.match(match.impact, /estado|UI|Compose/i);
289
- assert.match(match.expected_fix, /MutableStateFlow|StateFlow|ViewModel/i);
290
- });
291
-
292
- test('findAndroidSingleSourceOfTruthMatch devuelve payload semantico para Single source of truth en ViewModel', () => {
293
- const source = `
294
- class CatalogViewModel : ViewModel() {
295
- private val _uiState = MutableStateFlow(CatalogUiState())
296
- val uiState: StateFlow<CatalogUiState> = _uiState.asStateFlow()
297
- }
298
- `;
299
-
300
- const match = findAndroidSingleSourceOfTruthMatch(source);
301
-
302
- assert.ok(match);
303
- assert.deepEqual(match.primary_node, {
304
- kind: 'class',
305
- name: 'CatalogViewModel',
306
- lines: [2],
307
- });
308
- assert.deepEqual(match.related_nodes, [
309
- { kind: 'property', name: 'MutableStateFlow', lines: [3] },
310
- { kind: 'property', name: 'StateFlow', lines: [4] },
311
- { kind: 'call', name: 'asStateFlow', lines: [4] },
312
- ]);
313
- assert.match(match.why, /fuente de verdad|single source/i);
314
- assert.match(match.impact, /estado|UI|ViewModel/i);
315
- assert.match(match.expected_fix, /MutableStateFlow|StateFlow|ViewModel/i);
316
- });
317
-
318
- test('findAndroidSharedFlowMatch devuelve payload semantico para SharedFlow de eventos', () => {
319
- const source = `
320
- class CatalogViewModel : ViewModel() {
321
- private val _events = MutableSharedFlow<UiEvent>()
322
- val events: SharedFlow<UiEvent> = _events.asSharedFlow()
323
-
324
- fun onRetry() {
325
- _events.tryEmit(UiEvent.Retry)
326
- }
327
- }
328
- `;
329
-
330
- const match = findAndroidSharedFlowMatch(source);
331
-
332
- assert.ok(match);
333
- assert.deepEqual(match.primary_node, {
334
- kind: 'call',
335
- name: 'MutableSharedFlow',
336
- lines: [3],
337
- });
338
- assert.deepEqual(match.related_nodes, [
339
- { kind: 'call', name: 'SharedFlow', lines: [4] },
340
- { kind: 'call', name: 'asSharedFlow', lines: [4] },
341
- { kind: 'call', name: 'tryEmit', lines: [7] },
342
- ]);
343
- assert.match(match.why, /SharedFlow/i);
344
- assert.match(match.impact, /evento|stream|callback/i);
345
- assert.match(match.expected_fix, /MutableSharedFlow|SharedFlow|tryEmit/i);
346
- });
347
-
348
- test('findAndroidFlowBuilderMatch devuelve payload semantico para builders de Flow', () => {
349
- const source = `
350
- fun observeCatalog(): Flow<List<Int>> = flow {
351
- emit(listOf(1))
352
- }
353
-
354
- val flowValues = flowOf(1, 2, 3)
355
- val stream = listOf(4, 5).asFlow()
356
- `;
357
-
358
- const match = findAndroidFlowBuilderMatch(source);
359
-
360
- assert.ok(match);
361
- assert.deepEqual(match.primary_node, {
362
- kind: 'call',
363
- name: 'flow { emit() }',
364
- lines: [2],
365
- });
366
- assert.deepEqual(match.related_nodes, [
367
- { kind: 'call', name: 'flowOf', lines: [6] },
368
- { kind: 'call', name: 'asFlow', lines: [7] },
369
- ]);
370
- assert.match(match.why, /Flow/i);
371
- assert.match(match.impact, /stream|declarative|test/i);
372
- assert.match(match.expected_fix, /flow \{ emit|flowOf|asFlow/i);
373
- });
374
-
375
- test('findAndroidFlowCollectMatch devuelve payload semantico para terminal operator collect', () => {
376
- const source = `
377
- fun observeCatalog(scope: CoroutineScope, flow: Flow<List<Int>>) {
378
- flow.collect { items -> render(items) }
379
- flow.collectLatest { items -> renderLatest(items) }
380
- flow.launchIn(scope)
381
- }
382
- `;
383
-
384
- const match = findAndroidFlowCollectMatch(source);
385
-
386
- assert.ok(match);
387
- assert.deepEqual(match.primary_node, {
388
- kind: 'call',
389
- name: 'collect',
390
- lines: [3],
391
- });
392
- assert.deepEqual(match.related_nodes, [
393
- { kind: 'call', name: 'collectLatest', lines: [4] },
394
- { kind: 'call', name: 'launchIn', lines: [5] },
395
- ]);
396
- assert.match(match.why, /consume|Flow/i);
397
- assert.match(match.impact, /consumidor|terminal|observa/i);
398
- assert.match(match.expected_fix, /collect|collectLatest|launchIn/i);
399
- });
400
-
401
- test('findAndroidCollectAsStateMatch devuelve payload semantico para Compose collectAsState', () => {
402
- const source = `
403
- @Composable fun CatalogScreen(viewModel: CatalogViewModel) {
404
- val state by viewModel.uiState.collectAsState()
405
- val lifecycleState by viewModel.uiState.collectAsStateWithLifecycle()
406
- }
407
- `;
408
-
409
- const match = findAndroidCollectAsStateMatch(source);
410
-
411
- assert.ok(match);
412
- assert.deepEqual(match.primary_node, {
413
- kind: 'member',
414
- name: '@Composable fun CatalogScreen',
415
- lines: [2],
416
- });
417
- assert.deepEqual(match.related_nodes, [
418
- { kind: 'call', name: 'collectAsState', lines: [3] },
419
- { kind: 'call', name: 'collectAsStateWithLifecycle', lines: [4] },
420
- ]);
421
- assert.match(match.why, /collectAsState|Compose/i);
422
- assert.match(match.impact, /UI|estado|Flow/i);
423
- assert.match(match.expected_fix, /collectAsState|collectAsStateWithLifecycle/i);
424
- });
425
-
426
- test('findAndroidUiStateMatch devuelve payload semantico para sealed class UiState', () => {
427
- const source = `
428
- sealed class CatalogUiState {
429
- data object Loading : CatalogUiState()
430
- data class Success(val items: List<String>) : CatalogUiState()
431
- data class Error(val message: String) : CatalogUiState()
432
- }
433
- `;
434
-
435
- const match = findAndroidUiStateMatch(source);
436
-
437
- assert.ok(match);
438
- assert.deepEqual(match.primary_node, {
439
- kind: 'class',
440
- name: 'CatalogUiState',
441
- lines: [2],
442
- });
443
- assert.deepEqual(match.related_nodes, [
444
- { kind: 'member', name: 'Loading', lines: [3] },
445
- { kind: 'member', name: 'Success', lines: [4] },
446
- { kind: 'member', name: 'Error', lines: [5] },
447
- ]);
448
- assert.match(match.why, /UiState|Loading|Success|Error/i);
449
- assert.match(match.impact, /estado|UI|tipado|cerrado/i);
450
- assert.match(match.expected_fix, /sealed class|Loading|Success|Error/i);
451
- });
452
-
453
- test('findAndroidStateHoistingMatch devuelve payload semantico para composable con estado local', () => {
454
- const source = `
455
- @Composable fun CounterScreen() {
456
- var count by rememberSaveable { mutableStateOf(0) }
457
- }
458
- `;
459
-
460
- const match = findAndroidStateHoistingMatch(source);
461
-
462
- assert.ok(match);
463
- assert.deepEqual(match.primary_node, {
464
- kind: 'member',
465
- name: '@Composable fun CounterScreen',
466
- lines: [2],
467
- });
468
- assert.deepEqual(match.related_nodes, [
469
- { kind: 'call', name: 'rememberSaveable', lines: [3] },
470
- { kind: 'call', name: 'mutableStateOf', lines: [3] },
471
- ]);
472
- assert.match(match.why, /estado/i);
473
- assert.match(match.impact, /fuente|composable|UI/i);
474
- assert.match(match.expected_fix, /eleva|ViewModel|callbacks/i);
475
- });
476
-
477
- test('hasAndroidUiStateUsage detecta UiState sealed class completa y descarta estados incompletos', () => {
478
- const source = `
479
- sealed class CatalogUiState {
480
- data object Loading : CatalogUiState()
481
- data class Success(val items: List<String>) : CatalogUiState()
482
- data class Error(val message: String) : CatalogUiState()
483
- }
484
- `;
485
-
486
- assert.equal(hasAndroidUiStateUsage(source), true);
487
- assert.equal(
488
- hasAndroidUiStateUsage(`
489
- sealed class CatalogUiState {
490
- data object Loading : CatalogUiState()
491
- data class Success(val items: List<String>) : CatalogUiState()
492
- }
493
- `),
494
- false
495
- );
496
- });
497
-
498
- test('findAndroidUseCaseMatch devuelve payload semantico para UseCase Android', () => {
499
- const source = `
500
- class CatalogUseCase(
501
- private val catalogRepository: CatalogRepository,
502
- ) {
503
- suspend operator fun invoke(): List<String> {
504
- return catalogRepository.loadCatalog()
505
- }
506
- }
507
- `;
508
-
509
- const match = findAndroidUseCaseMatch(source);
510
-
511
- assert.ok(match);
512
- assert.deepEqual(match.primary_node, {
513
- kind: 'class',
514
- name: 'CatalogUseCase',
515
- lines: [2],
516
- });
517
- assert.deepEqual(match.related_nodes, [
518
- { kind: 'property', name: 'use case dependency', lines: [3] },
519
- { kind: 'member', name: 'use case operation', lines: [5] },
520
- ]);
521
- assert.match(match.why, /UseCase|l[oó]gica de negocio/i);
522
- assert.match(match.impact, /testear|reutilizar|ViewModel/i);
523
- assert.match(match.expected_fix, /UseCase|operaci[oó]n|dependencias/i);
524
- });
525
-
526
- test('findAndroidRepositoryPatternMatch devuelve payload semantico para Repository Android', () => {
527
- const source = `
528
- class CatalogRepository @Inject constructor(
529
- private val api: CatalogApi,
530
- private val catalogDataSource: CatalogDataSource,
531
- ) {
532
- suspend fun loadCatalog(): List<String> {
533
- return api.loadCatalog()
534
- }
535
-
536
- fun saveCatalog(items: List<String>) {
537
- catalogDataSource.save(items)
538
- }
539
- }
540
- `;
541
-
542
- const match = findAndroidRepositoryPatternMatch(source);
543
-
544
- assert.ok(match);
545
- assert.deepEqual(match.primary_node, {
546
- kind: 'class',
547
- name: 'CatalogRepository',
548
- lines: [2],
549
- });
550
- assert.deepEqual(match.related_nodes, [
551
- { kind: 'property', name: 'repository dependency', lines: [3, 4] },
552
- { kind: 'member', name: 'repository operation', lines: [6, 10] },
553
- ]);
554
- assert.match(match.why, /repository|acceso a datos/i);
555
- assert.match(match.impact, /contrato|persistencia|red/i);
556
- assert.match(match.expected_fix, /Repository|fachada|dependencias/i);
557
- assert.equal(hasAndroidRepositoryPatternUsage(source), true);
558
- });
559
-
560
- test('findAndroidOrdersRepMatch devuelve payload semantico para OrdersRep Android', () => {
561
- const source = `
562
- class OrdersRep @Inject constructor(
563
- private val remoteDataSource: OrdersRemoteDataSource,
564
- ) {
565
- suspend fun loadOrders(): List<String> {
566
- return remoteDataSource.loadOrders()
567
- }
568
- }
569
- `;
570
-
571
- const match = findAndroidOrdersRepMatch(source);
572
-
573
- assert.ok(match);
574
- assert.deepEqual(match.primary_node, {
575
- kind: 'class',
576
- name: 'OrdersRep',
577
- lines: [2],
578
- });
579
- assert.deepEqual(match.related_nodes, [
580
- { kind: 'property', name: 'repository dependency', lines: [3] },
581
- { kind: 'member', name: 'repository operation', lines: [5] },
582
- ]);
583
- assert.match(match.why, /OrdersRep|pedidos|acceso a datos/i);
584
- assert.match(match.impact, /pedidos|contrato|red/i);
585
- assert.match(match.expected_fix, /OrdersRep|fachada|dependencias/i);
586
- assert.equal(hasAndroidOrdersRepUsage(source), true);
587
- });
588
-
589
- test('hasAndroidUseCaseUsage detecta UseCase real y descarta clases sin operacion publica', () => {
590
- const source = `
591
- class CatalogUseCase(
592
- private val catalogRepository: CatalogRepository,
593
- ) {
594
- private fun helper() {}
595
- }
596
- `;
597
-
598
- assert.equal(hasAndroidUseCaseUsage(source), false);
599
- assert.equal(
600
- hasAndroidUseCaseUsage(`
601
- class CatalogUseCase(
602
- private val catalogRepository: CatalogRepository,
603
- ) {
604
- suspend operator fun invoke(): List<String> {
605
- return catalogRepository.loadCatalog()
606
- }
607
- }
608
- `),
609
- true
610
- );
611
- });
612
-
613
- test('hasAndroidRepositoryPatternUsage detecta Repository real y descarta helpers sin acceso a datos', () => {
614
- const source = `
615
- class CatalogHelper {
616
- fun build() {}
617
- }
618
- `;
619
-
620
- assert.equal(hasAndroidRepositoryPatternUsage(source), false);
621
- assert.equal(
622
- hasAndroidRepositoryPatternUsage(`
623
- interface CatalogRepository {
624
- suspend fun loadCatalog(): List<String>
625
- }
626
- `),
627
- true
628
- );
629
- });
630
-
631
- test('hasAndroidOrdersRepUsage detecta OrdersRep real y descarta helpers sin repositorio', () => {
632
- const source = `
633
- class OrdersRepHelper {
634
- fun build() {}
635
- }
636
- `;
637
-
638
- assert.equal(hasAndroidOrdersRepUsage(source), false);
639
- assert.equal(
640
- hasAndroidOrdersRepUsage(`
641
- class OrdersRep @Inject constructor(
642
- private val remoteDataSource: OrdersRemoteDataSource,
643
- ) {
644
- suspend fun loadOrders(): List<String> = remoteDataSource.loadOrders()
645
- }
646
- `),
647
- true
648
- );
649
- });
650
-
651
- test('findAndroidViewModelMatch devuelve payload semantico para ViewModel AndroidX', () => {
652
- const source = `
653
- class CatalogViewModel : ViewModel()
654
- `;
655
-
656
- const match = findAndroidViewModelMatch(source);
657
-
658
- assert.ok(match);
659
- assert.deepEqual(match.primary_node, {
660
- kind: 'class',
661
- name: 'CatalogViewModel',
662
- lines: [2],
663
- });
664
- assert.deepEqual(match.related_nodes, [
665
- { kind: 'member', name: 'androidx.lifecycle.ViewModel', lines: [2] },
666
- ]);
667
- assert.match(match.why, /ViewModel/i);
668
- assert.match(match.impact, /configuraci[oó]n|estado|ViewModel/i);
669
- assert.match(match.expected_fix, /ViewModel|estado|configuraci[oó]n/i);
670
- });
671
-
672
- test('hasAndroidStateFlowUsage ignora comentarios, strings y ViewModels sin estado observable', () => {
673
- const source = `
674
- // MutableStateFlow(value)
675
- val sample = "StateFlow"
676
- class CatalogViewModel : ViewModel() {
677
- fun load() {}
678
- }
679
- `;
680
-
681
- assert.equal(hasAndroidStateFlowUsage(source), false);
682
- });
683
-
684
- test('hasAndroidSingleSourceOfTruthUsage detecta ViewModel con estado de fuente unica y descarta helpers sin estado', () => {
685
- const source = `
686
- class CatalogHelper {
687
- fun load() {}
688
- }
689
- `;
690
-
691
- assert.equal(hasAndroidSingleSourceOfTruthUsage(source), false);
692
- assert.equal(
693
- hasAndroidSingleSourceOfTruthUsage(`
694
- class CatalogViewModel : ViewModel() {
695
- private val _uiState = MutableStateFlow(CatalogUiState())
696
- val uiState: StateFlow<CatalogUiState> = _uiState.asStateFlow()
697
- }
698
- `),
699
- true
700
- );
701
- });
702
-
703
- test('hasAndroidSharedFlowUsage ignora comentarios, strings y ViewModels sin eventos', () => {
704
- const source = `
705
- // MutableSharedFlow(value)
706
- val sample = "SharedFlow"
707
- class CatalogViewModel : ViewModel() {
708
- fun load() {}
709
- }
710
- `;
711
-
712
- assert.equal(hasAndroidSharedFlowUsage(source), false);
713
- });
714
-
715
- test('hasAndroidFlowBuilderUsage ignora comentarios, strings y archivos sin builders de Flow', () => {
716
- const source = `
717
- // flow { emit(1) }
718
- val sample = "flowOf(1, 2, 3)"
719
- fun load() {
720
- val value = listOf(1, 2, 3)
721
- }
722
- `;
723
-
724
- assert.equal(hasAndroidFlowBuilderUsage(source), false);
725
- });
726
-
727
- test('hasAndroidFlowCollectUsage ignora comentarios, strings y archivos sin terminal operators de Flow', () => {
728
- const source = `
729
- // flow.collect { }
730
- val sample = "collectLatest"
731
- fun render() {
732
- println("no flow collection here")
733
- }
734
- `;
735
-
736
- assert.equal(hasAndroidFlowCollectUsage(source), false);
737
- });
738
-
739
- test('hasAndroidCollectAsStateUsage ignora comentarios, strings y archivos sin collectAsState', () => {
740
- const source = `
741
- // collectAsState()
742
- val sample = "collectAsStateWithLifecycle"
743
- fun render() {
744
- println("no compose state collection here")
745
- }
746
- `;
747
-
748
- assert.equal(hasAndroidCollectAsStateUsage(source), false);
749
- });
750
-
751
- test('findAndroidRememberMatch devuelve payload semantico para remember en Compose', () => {
752
- const source = `
753
- @Composable fun ChartScreen() {
754
- val formatter = remember { java.text.DecimalFormat("#.##") }
755
- }
756
- `;
757
-
758
- const match = findAndroidRememberMatch(source);
759
-
760
- assert.ok(match);
761
- assert.deepEqual(match.primary_node, {
762
- kind: 'member',
763
- name: '@Composable fun ChartScreen',
764
- lines: [2],
765
- });
766
- assert.deepEqual(match.related_nodes, [{ kind: 'call', name: 'remember', lines: [3] }]);
767
- assert.match(match.why, /remember|recrear/i);
768
- assert.match(match.impact, /estables|recomponer/i);
769
- assert.match(match.expected_fix, /remember|memoizar/i);
770
- });
771
-
772
- test('hasAndroidRememberUsage ignora comentarios, strings y rememberSaveable', () => {
773
- const source = `
774
- // remember { }
775
- val sample = "remember { }"
776
- @Composable fun Sample() {
777
- val state = rememberSaveable { 1 }
778
- }
779
- `;
780
-
781
- assert.equal(hasAndroidRememberUsage(source), false);
782
- });
783
-
784
- test('findAndroidDerivedStateOfMatch devuelve payload semantico para derivedStateOf en Compose', () => {
785
- const source = `
786
- @Composable fun SearchScreen(query: String) {
787
- val hasQuery by derivedStateOf { query.isNotBlank() }
788
- }
789
- `;
790
-
791
- const match = findAndroidDerivedStateOfMatch(source);
792
-
793
- assert.ok(match);
794
- assert.deepEqual(match.primary_node, {
795
- kind: 'member',
796
- name: '@Composable fun SearchScreen',
797
- lines: [2],
798
- });
799
- assert.deepEqual(match.related_nodes, [{ kind: 'call', name: 'derivedStateOf', lines: [3] }]);
800
- assert.match(match.why, /derivedStateOf|caro/i);
801
- assert.match(match.impact, /recomput|estado/i);
802
- assert.match(match.expected_fix, /derivedStateOf|derivar/i);
803
- });
804
-
805
- test('hasAndroidDerivedStateOfUsage ignora comentarios y strings', () => {
806
- assert.equal(
807
- hasAndroidDerivedStateOfUsage(`
808
- // derivedStateOf { }
809
- val sample = "derivedStateOf"
810
- @Composable fun Sample() {
811
- Text("ok")
812
- }
813
- `),
814
- false
815
- );
816
- });
817
-
818
- test('findAndroidLaunchedEffectMatch devuelve payload semantico para LaunchedEffect en Compose', () => {
819
- const source = `
820
- @Composable fun Screen(viewModel: ScreenViewModel) {
821
- LaunchedEffect(viewModel.userId) {
822
- viewModel.refresh()
823
- }
824
- }
825
- `;
826
-
827
- const match = findAndroidLaunchedEffectMatch(source);
828
-
829
- assert.ok(match);
830
- assert.deepEqual(match.primary_node, {
831
- kind: 'member',
832
- name: '@Composable fun Screen',
833
- lines: [2],
834
- });
835
- assert.deepEqual(match.related_nodes, [{ kind: 'call', name: 'LaunchedEffect', lines: [3] }]);
836
- assert.match(match.why, /LaunchedEffect|lifecycle/i);
837
- assert.match(match.impact, /cancelado|relanzado|composición/i);
838
- assert.match(match.expected_fix, /LaunchedEffect|lifecycle|claves/i);
839
- });
840
-
841
- test('hasAndroidLaunchedEffectUsage ignora comentarios y strings', () => {
842
- assert.equal(
843
- hasAndroidLaunchedEffectUsage(`\n// LaunchedEffect(Unit)\nval sample = "LaunchedEffect(Unit)"\n@Composable fun Sample() {\n Text("ok")\n}\n`),
844
- false
845
- );
846
- });
847
-
848
- test('findAndroidLaunchedEffectKeysMatch devuelve payload semantico para LaunchedEffect keys en Compose', () => {
849
- const source = `
850
- @Composable fun Screen(viewModel: ScreenViewModel) {
851
- LaunchedEffect(viewModel.userId, viewModel.refreshTrigger) {
852
- viewModel.refresh()
853
- }
854
- }
855
- `;
856
-
857
- const match = findAndroidLaunchedEffectKeysMatch(source);
858
-
859
- assert.ok(match);
860
- assert.deepEqual(match.primary_node, {
861
- kind: 'member',
862
- name: '@Composable fun Screen',
863
- lines: [2],
864
- });
865
- assert.deepEqual(match.related_nodes, [{ kind: 'call', name: 'LaunchedEffect keys', lines: [3] }]);
866
- assert.match(match.why, /LaunchedEffect|keys|relanza/i);
867
- assert.match(match.impact, /keys|relanzado|input/i);
868
- assert.match(match.expected_fix, /claves|LaunchedEffect|relanzar/i);
869
- });
870
-
871
- test('hasAndroidLaunchedEffectKeysUsage ignora comentarios y strings', () => {
872
- assert.equal(
873
- hasAndroidLaunchedEffectKeysUsage(`\n// LaunchedEffect(viewModel.userId)\nval sample = "LaunchedEffect(viewModel.userId)"\n@Composable fun Sample() {\n Text("ok")\n}\n`),
874
- false
875
- );
876
- });
877
-
878
- test('findAndroidDisposableEffectMatch devuelve payload semantico para DisposableEffect en Compose', () => {
879
- const source = `
880
- @Composable fun Screen() {
881
- DisposableEffect(Unit) {
882
- onDispose { }
883
- }
884
- }
885
- `;
886
-
887
- const match = findAndroidDisposableEffectMatch(source);
888
-
889
- assert.ok(match);
890
- assert.deepEqual(match.primary_node, {
891
- kind: 'member',
892
- name: '@Composable fun Screen',
893
- lines: [2],
894
- });
895
- assert.deepEqual(match.related_nodes, [{ kind: 'call', name: 'DisposableEffect', lines: [3] }]);
896
- assert.match(match.why, /DisposableEffect|limpiar|composición/i);
897
- assert.match(match.impact, /libera|recursos|Compose/i);
898
- assert.match(match.expected_fix, /DisposableEffect|limpiar|lifecycle/i);
899
- });
900
-
901
- test('hasAndroidDisposableEffectUsage ignora comentarios y strings', () => {
902
- assert.equal(
903
- hasAndroidDisposableEffectUsage(`\n// DisposableEffect(Unit)\nval sample = "DisposableEffect(Unit)"\n@Composable fun Sample() {\n Text("ok")\n}\n`),
904
- false
905
- );
906
- });
907
-
908
- test('findAndroidPreviewMatch devuelve payload semantico para @Preview en Compose', () => {
909
- const source = `
910
- data class PreviewUiState(val label: String)
911
- @Preview(showBackground = true)
912
- @Composable fun PreviewCounter(state: PreviewUiState) {
913
- Text(text = state.label)
914
- }
915
- `;
916
-
917
- const match = findAndroidPreviewMatch(source);
918
-
919
- assert.ok(match);
920
- assert.deepEqual(match.primary_node, {
921
- kind: 'member',
922
- name: '@Composable fun PreviewCounter',
923
- lines: [4],
924
- });
925
- assert.deepEqual(match.related_nodes, [{ kind: 'call', name: '@Preview', lines: [3] }]);
926
- assert.match(match.why, /Preview|UI|app/i);
927
- assert.match(match.impact, /renderizar|Android Studio|Compose/i);
928
- assert.match(match.expected_fix, /Preview|composable|UI/i);
929
- });
930
-
931
- test('hasAndroidPreviewUsage ignora comentarios y strings', () => {
932
- assert.equal(
933
- hasAndroidPreviewUsage(`\n// @Preview(showBackground = true)\nval sample = "@Preview(showBackground = true)"\n@Composable fun Sample() {\n Text("ok")\n}\n`),
934
- false
935
- );
936
- });
937
-
938
- test('findAndroidAdaptiveLayoutsMatch devuelve payload semantico para WindowSizeClass en Compose', () => {
939
- const source = `
940
- fun ResponsiveScreen(activity: Activity) {
941
- val windowSizeClass = calculateWindowSizeClass(activity)
942
- when (windowSizeClass.widthSizeClass) {
943
- WindowWidthSizeClass.Compact -> Unit
944
- WindowWidthSizeClass.Medium -> Unit
945
- WindowWidthSizeClass.Expanded -> Unit
946
- }
947
- }
948
- `;
949
-
950
- const match = findAndroidAdaptiveLayoutsMatch(source);
951
-
952
- assert.ok(match);
953
- assert.deepEqual(match.primary_node, {
954
- kind: 'call',
955
- name: 'calculateWindowSizeClass',
956
- lines: [3],
957
- });
958
- assert.deepEqual(match.related_nodes, [
959
- { kind: 'member', name: 'WindowWidthSizeClass', lines: [5, 6, 7] },
960
- ]);
961
- assert.match(match.why, /WindowSizeClass|adaptive|responsive/i);
962
- assert.match(match.impact, /layout|pantallas|responsive/i);
963
- assert.match(match.expected_fix, /calculateWindowSizeClass|WindowWidthSizeClass/i);
964
- });
965
-
966
- test('hasAndroidAdaptiveLayoutsUsage ignora comentarios y strings', () => {
967
- assert.equal(
968
- hasAndroidAdaptiveLayoutsUsage(`\n// calculateWindowSizeClass(activity)\nval sample = "WindowWidthSizeClass.Compact"\nfun render() {\n Text("ok")\n}\n`),
969
- false
970
- );
971
- });
972
-
973
- test('findAndroidExistingStructureMatch devuelve payload semantico para interfaces y modules Android', () => {
974
- const source = `
975
- interface SessionContract {
976
- fun load(): String
977
- }
978
-
979
- @Module
980
- @InstallIn(SingletonComponent::class)
981
- object SessionModule {
982
- @Provides
983
- fun provideSessionContract(): SessionContract = RealSessionContract()
984
- }
985
- `;
986
-
987
- const match = findAndroidExistingStructureMatch(source);
988
-
989
- assert.ok(match);
990
- assert.deepEqual(match.primary_node, {
991
- kind: 'member',
992
- name: 'interface declaration: SessionContract',
993
- lines: [2],
994
- });
995
- assert.deepEqual(match.related_nodes, [
996
- { kind: 'member', name: 'module annotation', lines: [6] },
997
- { kind: 'member', name: 'module annotation', lines: [7] },
998
- ]);
999
- assert.match(match.why, /estructura existente|interfaces|módulos/i);
1000
- assert.match(match.impact, /dependenc|gradle|di/i);
1001
- assert.match(match.expected_fix, /módulos|interfaces|Gradle/i);
1002
- });
1003
-
1004
- test('findAndroidExistingStructureMatch devuelve payload semantico para dependencies Gradle', () => {
1005
- const source = `
1006
- plugins {
1007
- id("com.android.application")
1008
- kotlin("android")
1009
- }
1010
-
1011
- dependencies {
1012
- implementation(libs.androidx.core.ktx)
1013
- api(libs.core.domain)
1014
- }
1015
- `;
1016
-
1017
- const match = findAndroidExistingStructureMatch(source);
1018
-
1019
- assert.ok(match);
1020
- assert.deepEqual(match.primary_node, {
1021
- kind: 'member',
1022
- name: 'dependencies block',
1023
- lines: [7],
1024
- });
1025
- assert.deepEqual(match.related_nodes, [
1026
- { kind: 'member', name: 'dependency declaration', lines: [8] },
1027
- { kind: 'member', name: 'dependency declaration', lines: [9] },
1028
- ]);
1029
- assert.match(match.why, /Gradle|dependenc/i);
1030
- assert.match(match.impact, /módulos|dependencias|Gradle/i);
1031
- assert.match(match.expected_fix, /dependencies|catálogo|Gradle/i);
1032
- });
1033
-
1034
- test('hasAndroidExistingStructureUsage ignora comentarios y strings', () => {
1035
- assert.equal(
1036
- hasAndroidExistingStructureUsage(`\n// interface SessionRepository\nval sample = "dependencies { implementation(\\"foo\\") }"\n`),
1037
- false
1038
- );
1039
- });
1040
-
1041
- test('findAndroidThemeMatch devuelve payload semantico para theme Material 3 en Compose', () => {
1042
- const source = `
1043
- @Composable
1044
- fun AppTheme(content: @Composable () -> Unit) {
1045
- MaterialTheme(
1046
- colorScheme = darkColorScheme(),
1047
- typography = AppTypography,
1048
- shapes = AppShapes,
1049
- content = content
1050
- )
1051
- }
1052
- `;
1053
-
1054
- const match = findAndroidThemeMatch(source);
1055
-
1056
- assert.ok(match);
1057
- assert.deepEqual(match.primary_node, {
1058
- kind: 'member',
1059
- name: '@Composable fun AppTheme',
1060
- lines: [3],
1061
- });
1062
- assert.deepEqual(match.related_nodes, [
1063
- { kind: 'call', name: 'MaterialTheme', lines: [4] },
1064
- { kind: 'property', name: 'colorScheme', lines: [5] },
1065
- { kind: 'property', name: 'typography', lines: [6] },
1066
- { kind: 'property', name: 'shapes', lines: [7] },
1067
- ]);
1068
- assert.match(match.why, /tema|MaterialTheme|colorScheme/i);
1069
- assert.match(match.impact, /coherencia|escalabilidad|UI/i);
1070
- assert.match(match.expected_fix, /tema|colorScheme|typography|shapes/i);
1071
- });
1072
-
1073
- test('hasAndroidThemeUsage ignora comentarios y strings', () => {
1074
- assert.equal(
1075
- hasAndroidThemeUsage(`\n// MaterialTheme(colorScheme = darkColorScheme())\nval sample = "MaterialTheme(colorScheme = darkColorScheme())"\n@Composable fun Sample() {\n Text("ok")\n}\n`),
1076
- false
1077
- );
1078
- });
1079
-
1080
- test('findAndroidDarkThemeMatch devuelve payload semantico para soporte de tema oscuro', () => {
1081
- const source = `
1082
- @Composable
1083
- fun AppTheme(content: @Composable () -> Unit) {
1084
- val darkTheme = isSystemInDarkTheme()
1085
- val colorScheme = if (darkTheme) darkColorScheme() else lightColorScheme()
1086
- MaterialTheme(
1087
- colorScheme = colorScheme,
1088
- typography = AppTypography,
1089
- shapes = AppShapes,
1090
- content = content
1091
- )
1092
- }
1093
- `;
1094
-
1095
- const match = findAndroidDarkThemeMatch(source);
1096
-
1097
- assert.ok(match);
1098
- assert.deepEqual(match.primary_node, {
1099
- kind: 'member',
1100
- name: '@Composable fun AppTheme',
1101
- lines: [3],
1102
- });
1103
- assert.deepEqual(match.related_nodes, [
1104
- { kind: 'call', name: 'isSystemInDarkTheme', lines: [4] },
1105
- { kind: 'call', name: 'darkColorScheme', lines: [5] },
1106
- { kind: 'call', name: 'lightColorScheme', lines: [5] },
1107
- { kind: 'call', name: 'MaterialTheme', lines: [6] },
1108
- ]);
1109
- assert.match(match.why, /tema oscuro|isSystemInDarkTheme/i);
1110
- assert.match(match.impact, /tema oscuro|UI|sistema/i);
1111
- assert.match(match.expected_fix, /isSystemInDarkTheme|darkColorScheme|lightColorScheme/i);
1112
- });
1113
-
1114
- test('hasAndroidDarkThemeUsage ignora comentarios y strings', () => {
1115
- assert.equal(
1116
- hasAndroidDarkThemeUsage(`\n// isSystemInDarkTheme()\nval sample = "darkColorScheme()"\n@Composable fun Sample() {\n Text("ok")\n}\n`),
1117
- false
1118
- );
1119
- });
1120
-
1121
- test('findAndroidAccessibilityMatch devuelve payload semantico para accesibilidad Compose', () => {
1122
- const source = `
1123
- @Composable fun AccessibleIconScreen() {
1124
- Icon(
1125
- imageVector = Icons.Default.Settings,
1126
- contentDescription = "Ajustes"
1127
- )
1128
- Box(modifier = Modifier.semantics { contentDescription = "Pantalla de ajustes" })
1129
- }
1130
- `;
1131
-
1132
- const match = findAndroidAccessibilityMatch(source);
1133
-
1134
- assert.ok(match);
1135
- assert.deepEqual(match.primary_node, {
1136
- kind: 'member',
1137
- name: '@Composable fun AccessibleIconScreen',
1138
- lines: [2],
1139
- });
1140
- assert.deepEqual(match.related_nodes, [
1141
- { kind: 'property', name: 'contentDescription', lines: [5] },
1142
- { kind: 'call', name: 'semantics', lines: [7] },
1143
- ]);
1144
- assert.match(match.why, /contentDescription|semantics|accesibilidad/i);
1145
- assert.match(match.impact, /accesible|lectores de pantalla/i);
1146
- assert.match(match.expected_fix, /contentDescription|semantics/i);
1147
- });
1148
-
1149
- test('hasAndroidAccessibilityUsage ignora comentarios, strings y accesibilidad implícita', () => {
1150
- const source = `
1151
- // contentDescription = "debug"
1152
- val sample = "Modifier.semantics { }"
1153
- @Composable fun Sample() {
1154
- Text("ok")
1155
- }
1156
- `;
1157
-
1158
- assert.equal(hasAndroidAccessibilityUsage(source), false);
1159
- });
1160
-
1161
- test('findAndroidTalkBackMatch devuelve payload semantico para TalkBack en Compose', () => {
1162
- const source = `
1163
- @Composable fun AccessibleIconScreen() {
1164
- Box(modifier = Modifier.semantics { })
1165
- }
1166
- `;
1167
-
1168
- const match = findAndroidTalkBackMatch(source);
1169
-
1170
- assert.ok(match);
1171
- assert.deepEqual(match.primary_node, {
1172
- kind: 'member',
1173
- name: '@Composable fun AccessibleIconScreen',
1174
- lines: [2],
1175
- });
1176
- assert.deepEqual(match.related_nodes, [
1177
- { kind: 'call', name: 'semantics', lines: [3] },
1178
- ]);
1179
- assert.match(match.why, /TalkBack|accesibilidad|screen reader/i);
1180
- assert.match(match.impact, /lectores de pantalla|tecnolog/i);
1181
- assert.match(match.expected_fix, /TalkBack|contentDescription|semantics/i);
1182
- });
1183
-
1184
- test('hasAndroidTalkBackUsage ignora comentarios y strings', () => {
1185
- assert.equal(
1186
- hasAndroidTalkBackUsage(`\n// TalkBack\nval sample = "contentDescription = ignored"\n@Composable fun Sample() {\n Text("ok")\n}\n`),
1187
- false
1188
- );
1189
- });
1190
-
1191
- test('findAndroidTextScalingMatch devuelve payload semantico para text scaling en Compose', () => {
1192
- const source = `
1193
- @Composable fun ScaledTextScreen() {
1194
- val fontScale = LocalDensity.current.fontScale
1195
- Text(
1196
- text = "Hola",
1197
- fontSize = 16.sp
1198
- )
1199
- }
1200
- `;
1201
-
1202
- const match = findAndroidTextScalingMatch(source);
1203
-
1204
- assert.ok(match);
1205
- assert.deepEqual(match.primary_node, {
1206
- kind: 'member',
1207
- name: '@Composable fun ScaledTextScreen',
1208
- lines: [2],
1209
- });
1210
- assert.deepEqual(match.related_nodes, [
1211
- { kind: 'property', name: 'fontScale', lines: [3] },
1212
- { kind: 'property', name: 'fontSize', lines: [6] },
1213
- ]);
1214
- assert.match(match.why, /font scaling|fontScale|sp/i);
1215
- assert.match(match.impact, /legibilidad|texto|acces/i);
1216
- assert.match(match.expected_fix, /fontScale|sp|TextUnit\.Sp/i);
1217
- });
1218
-
1219
- test('hasAndroidTextScalingUsage ignora comentarios y strings', () => {
1220
- assert.equal(
1221
- hasAndroidTextScalingUsage(`\n// fontScale\nval sample = "fontSize = 16.sp"\n@Composable fun Sample() {\n Text("ok")\n}\n`),
1222
- false
1223
- );
1224
- });
1225
-
1226
- test('findAndroidTouchTargetsMatch devuelve payload semantico para touch targets en Compose', () => {
1227
- const source = `
1228
- @Composable fun TouchTargetButton() {
1229
- IconButton(
1230
- onClick = {},
1231
- modifier = Modifier.sizeIn(minWidth = 48.dp, minHeight = 48.dp)
1232
- ) {
1233
- Icon(imageVector = Icons.Default.Settings, contentDescription = "Ajustes")
1234
- }
1235
- }
1236
- `;
1237
-
1238
- const match = findAndroidTouchTargetsMatch(source);
1239
-
1240
- assert.ok(match);
1241
- assert.deepEqual(match.primary_node, {
1242
- kind: 'member',
1243
- name: '@Composable fun TouchTargetButton',
1244
- lines: [2],
1245
- });
1246
- assert.deepEqual(match.related_nodes, [
1247
- { kind: 'call', name: 'sizeIn48dp', lines: [5] },
1248
- ]);
1249
- assert.match(match.why, /touch targets|48dp|interact/i);
1250
- assert.match(match.impact, /toque|interactiv|precisi/i);
1251
- assert.match(match.expected_fix, /minimumInteractiveComponentSize|sizeIn|minWidth|minHeight/i);
1252
- });
1253
-
1254
- test('hasAndroidTouchTargetsUsage ignora comentarios, strings y tamanos inferiores a 48dp', () => {
1255
- const source = `
1256
- // sizeIn(minWidth = 48.dp, minHeight = 48.dp)
1257
- val sample = "minimumInteractiveComponentSize()"
1258
- @Composable fun Sample() {
1259
- IconButton(
1260
- onClick = {},
1261
- modifier = Modifier.sizeIn(minWidth = 24.dp, minHeight = 24.dp)
1262
- ) {
1263
- Icon(imageVector = Icons.Default.Settings, contentDescription = "Ajustes")
1264
- }
1265
- }
1266
- `;
1267
-
1268
- assert.equal(hasAndroidTouchTargetsUsage(source), false);
1269
- });
1270
-
1271
- test('findAndroidContentDescriptionMatch devuelve payload semantico para contentDescription en Compose', () => {
1272
- const source = `
1273
- @Composable fun SettingsButton() {
1274
- Icon(
1275
- imageVector = Icons.Default.Settings,
1276
- contentDescription = "Ajustes"
1277
- )
1278
- }
1279
- `;
1280
-
1281
- const match = findAndroidContentDescriptionMatch(source);
1282
-
1283
- assert.ok(match);
1284
- assert.deepEqual(match.primary_node, {
1285
- kind: 'member',
1286
- name: '@Composable fun SettingsButton',
1287
- lines: [2],
1288
- });
1289
- assert.deepEqual(match.related_nodes, [
1290
- { kind: 'property', name: 'contentDescription', lines: [5] },
1291
- ]);
1292
- assert.match(match.why, /contentDescription|imágenes|botones/i);
1293
- assert.match(match.impact, /lectores de pantalla|tecnolog/i);
1294
- assert.match(match.expected_fix, /contentDescription|accesible/i);
1295
- });
1296
-
1297
- test('hasAndroidContentDescriptionUsage ignora comentarios y strings', () => {
1298
- assert.equal(
1299
- hasAndroidContentDescriptionUsage(`\n// contentDescription = "debug"\nval sample = "contentDescription = ignored"\n@Composable fun Sample() {\n Text("ok")\n}\n`),
1300
- false
1301
- );
1302
- });
1303
-
1304
- test('findAndroidRecompositionMatch devuelve payload semantico para recomposition idempotente', () => {
1305
- const source = `
1306
- @Composable fun CounterScreen(viewModel: CounterViewModel) {
1307
- println("recompose")
1308
- viewModel.counter.value = viewModel.counter.value + 1
1309
- }
1310
- `;
1311
-
1312
- const match = findAndroidRecompositionMatch(source);
1313
-
1314
- assert.ok(match);
1315
- assert.deepEqual(match.primary_node, {
1316
- kind: 'member',
1317
- name: '@Composable fun CounterScreen',
1318
- lines: [2],
1319
- });
1320
- assert.deepEqual(match.related_nodes, [
1321
- { kind: 'call', name: 'println', lines: [3] },
1322
- { kind: 'call', name: 'state mutation', lines: [4] },
1323
- ]);
1324
- assert.match(match.why, /idempotencia|recompos/i);
1325
- assert.match(match.impact, /repetir|mutaciones|side effects/i);
1326
- assert.match(match.expected_fix, /idempotencia|side effects|ViewModel/i);
1327
- });
1328
-
1329
- test('hasAndroidRecompositionUsage ignora comentarios y strings', () => {
1330
- assert.equal(
1331
- hasAndroidRecompositionUsage(`\n// println("recompose")\nval sample = "Log.d(\\\"x\\\")"\n@Composable fun Sample() {\n Text("ok")\n}\n`),
1332
- false
1333
- );
1334
- });
1335
-
1336
- test('hasAndroidStateHoistingUsage detecta composables con estado local y descarta files sin estado', () => {
1337
- const source = `
1338
- @Composable fun CounterScreen() {
1339
- var count by rememberSaveable { mutableStateOf(0) }
1340
- }
1341
-
1342
- @Composable fun StaticScreen() {
1343
- Text("hello")
1344
- }
1345
- `;
1346
-
1347
- assert.equal(hasAndroidStateHoistingUsage(source), true);
1348
- assert.equal(
1349
- hasAndroidStateHoistingUsage(`
1350
- @Composable fun StaticScreen() {
1351
- Text("hello")
1352
- }
1353
- `),
1354
- false
1355
- );
1356
- });
1357
-
1358
- test('hasAndroidViewModelUsage detecta ViewModel real y descarta clases no relacionadas', () => {
1359
- const source = `
1360
- class CatalogViewModel : ViewModel()
1361
- class CatalogPresenter
1362
- `;
1363
-
1364
- assert.equal(hasAndroidViewModelUsage(source), true);
1365
- });
1366
-
1367
- test('findAndroidSuspendFunctionsApiServiceMatch devuelve payload semantico para API service con suspend fun', () => {
1368
- const source = `
1369
- interface CatalogApiService {
1370
- suspend fun fetchCatalog(): List<String>
1371
-
1372
- suspend fun fetchFeaturedCatalog(): List<String>
1373
- }
1374
- `;
1375
-
1376
- const match = findAndroidSuspendFunctionsApiServiceMatch(source);
1377
-
1378
- assert.ok(match);
1379
- assert.deepEqual(match.primary_node, {
1380
- kind: 'class',
1381
- name: 'CatalogApiService',
1382
- lines: [2],
1383
- });
1384
- assert.deepEqual(match.related_nodes, [
1385
- { kind: 'member', name: 'API service: CatalogApiService', lines: [2] },
1386
- { kind: 'member', name: 'suspend fun fetchCatalog', lines: [3] },
1387
- { kind: 'member', name: 'suspend fun fetchFeaturedCatalog', lines: [5] },
1388
- ]);
1389
- assert.match(match.why, /API/i);
1390
- assert.match(match.impact, /API lineal|callbacks/i);
1391
- assert.match(match.expected_fix, /suspend functions|coroutine/i);
1392
- });
1393
-
1394
- test('hasAndroidSuspendFunctionsApiServiceUsage ignora comentarios y servicios no API', () => {
1395
- const source = `
1396
- // suspend fun fake()
1397
- val sample = "CatalogApiService"
1398
-
1399
- interface CatalogRepository {
1400
- fun fetchCatalog()
1401
- }
1402
- `;
1403
-
1404
- assert.equal(hasAndroidSuspendFunctionsApiServiceUsage(source), false);
1405
- });
1406
-
1407
- test('findAndroidSuspendFunctionsAsyncMatch devuelve payload semantico para suspend fun async en clases generales', () => {
1408
- const source = `
1409
- class DashboardRepository {
1410
- suspend fun loadDashboard(): String = "ok"
1411
-
1412
- suspend fun refreshDashboard(): String = "ok"
1413
- }
1414
- `;
1415
-
1416
- const match = findAndroidSuspendFunctionsAsyncMatch(source);
1417
-
1418
- assert.ok(match);
1419
- assert.deepEqual(match.primary_node, {
1420
- kind: 'class',
1421
- name: 'DashboardRepository',
1422
- lines: [2],
1423
- });
1424
- assert.deepEqual(match.related_nodes, [
1425
- { kind: 'member', name: 'DashboardRepository', lines: [2] },
1426
- { kind: 'member', name: 'suspend fun loadDashboard', lines: [3] },
1427
- { kind: 'member', name: 'suspend fun refreshDashboard', lines: [5] },
1428
- ]);
1429
- assert.match(match.why, /suspend functions/i);
1430
- assert.match(match.impact, /lineal|componer/i);
1431
- assert.match(match.expected_fix, /async|suspend functions/i);
1432
- });
1433
-
1434
- test('hasAndroidSuspendFunctionsAsyncUsage ignora comentarios y excluye services y daos', () => {
1435
- const source = `
1436
- // suspend fun fake()
1437
- val sample = "suspend fun load"
1438
-
1439
- interface CatalogApiService {
1440
- suspend fun fetchCatalog(): List<String>
1441
- }
1442
-
1443
- @Dao
1444
- interface CatalogDao {
1445
- @Query("SELECT * FROM catalog")
1446
- suspend fun loadCatalog(): List<String>
1447
- }
1448
-
1449
- class DashboardRepository {
1450
- suspend fun loadDashboard(): String = "ok"
1451
- }
1452
- `;
1453
-
1454
- assert.equal(hasAndroidSuspendFunctionsAsyncUsage(source), true);
1455
- assert.equal(
1456
- hasAndroidSuspendFunctionsAsyncUsage(`
1457
- // suspend fun fake()
1458
- val sample = "suspend fun load"
1459
-
1460
- interface CatalogApiService {
1461
- suspend fun fetchCatalog(): List<String>
1462
- }
1463
-
1464
- @Dao
1465
- interface CatalogDao {
1466
- @Query("SELECT * FROM catalog")
1467
- suspend fun loadCatalog(): List<String>
1468
- }
1469
- `),
1470
- false
1471
- );
1472
- });
1473
-
1474
- test('findAndroidAsyncAwaitParallelismMatch devuelve payload semantico para async/await en paralelismo', () => {
1475
- const source = `
1476
- class ReportCoordinator {
1477
- suspend fun buildReport(): String = coroutineScope {
1478
- val summary = async { "summary" }
1479
- val details = async(Dispatchers.IO) { "details" }
1480
- summary.await()
1481
- details.await()
1482
- "report"
1483
- }
1484
- }
1485
- `;
1486
-
1487
- const match = findAndroidAsyncAwaitParallelismMatch(source);
1488
-
1489
- assert.ok(match);
1490
- assert.deepEqual(match.primary_node, {
1491
- kind: 'class',
1492
- name: 'ReportCoordinator',
1493
- lines: [2],
1494
- });
1495
- assert.deepEqual(match.related_nodes, [
1496
- { kind: 'member', name: 'ReportCoordinator', lines: [2] },
1497
- { kind: 'call', name: 'async', lines: [4, 5] },
1498
- { kind: 'call', name: 'await', lines: [6, 7] },
1499
- ]);
1500
- assert.match(match.why, /async\/await/i);
1501
- assert.match(match.impact, /paralelo|parallel|await/i);
1502
- assert.match(match.expected_fix, /coroutineScope|parallel/i);
1503
- });
1504
-
1505
- test('hasAndroidAsyncAwaitParallelismUsage ignora comentarios y excluye services y daos', () => {
1506
- const source = `
1507
- // async { fake() }
1508
- val sample = "await()"
1509
-
1510
- interface CatalogApiService {
1511
- suspend fun fetchCatalog(): List<String>
1512
- }
1513
-
1514
- @Dao
1515
- interface CatalogDao {
1516
- @Query("SELECT * FROM catalog")
1517
- suspend fun loadCatalog(): List<String>
1518
- }
1519
-
1520
- class ReportCoordinator {
1521
- suspend fun buildReport(): String = coroutineScope {
1522
- val summary = async { "summary" }
1523
- summary.await()
1524
- "report"
1525
- }
1526
- }
1527
- `;
1528
-
1529
- assert.equal(hasAndroidAsyncAwaitParallelismUsage(source), true);
1530
- assert.equal(
1531
- hasAndroidAsyncAwaitParallelismUsage(`
1532
- // async { fake() }
1533
- val sample = "await()"
1534
-
1535
- interface CatalogApiService {
1536
- suspend fun fetchCatalog(): List<String>
1537
- }
1538
-
1539
- @Dao
1540
- interface CatalogDao {
1541
- @Query("SELECT * FROM catalog")
1542
- suspend fun loadCatalog(): List<String>
1543
- }
1544
- `),
1545
- false
1546
- );
1547
- });
1548
-
1549
- test('findAndroidDaoSuspendFunctionsMatch devuelve payload semantico para DAO con suspend fun', () => {
1550
- const source = `
1551
- @Dao
1552
- interface CatalogDao {
1553
- @Query("SELECT * FROM catalog")
1554
- suspend fun loadCatalog(): List<String>
1555
-
1556
- @Insert
1557
- suspend fun saveCatalog(items: List<String>)
1558
- }
1559
- `;
1560
-
1561
- const match = findAndroidDaoSuspendFunctionsMatch(source);
1562
-
1563
- assert.ok(match);
1564
- assert.deepEqual(match.primary_node, {
1565
- kind: 'class',
1566
- name: 'CatalogDao',
1567
- lines: [3],
1568
- });
1569
- assert.deepEqual(match.related_nodes, [
1570
- { kind: 'member', name: '@Dao', lines: [2] },
1571
- { kind: 'member', name: 'suspend fun loadCatalog', lines: [5] },
1572
- { kind: 'member', name: 'suspend fun saveCatalog', lines: [8] },
1573
- ]);
1574
- assert.match(match.why, /DAO/i);
1575
- assert.match(match.impact, /persistencia|coroutines|Room/i);
1576
- assert.match(match.expected_fix, /DAO|suspend functions|repositor/i);
1577
- });
1578
-
1579
- test('findAndroidTransactionMatch devuelve payload semantico para DAO con @Transaction', () => {
1580
- const source = `
1581
- @Dao
1582
- interface OrderDao {
1583
- @Transaction
1584
- fun loadOrderGraph(): Order
1585
-
1586
- @Transaction
1587
- fun saveOrder()
1588
- }
1589
- `;
1590
-
1591
- const match = findAndroidTransactionMatch(source);
1592
-
1593
- assert.ok(match);
1594
- assert.deepEqual(match?.primary_node, {
1595
- kind: 'class',
1596
- name: 'OrderDao',
1597
- lines: [3],
1598
- });
1599
- assert.deepEqual(match?.related_nodes, [
1600
- { kind: 'member', name: '@Dao', lines: [2] },
1601
- { kind: 'member', name: '@Transaction fun loadOrderGraph', lines: [4, 5] },
1602
- { kind: 'member', name: '@Transaction fun saveOrder', lines: [7, 8] },
1603
- ]);
1604
- assert.deepEqual(match?.lines, [2, 3, 4, 5, 7, 8]);
1605
- });
1606
-
1607
- test('hasAndroidDaoSuspendFunctionsUsage ignora comentarios y DAOs sin suspend fun', () => {
1608
- const source = `
1609
- // @Dao
1610
- interface CatalogDao {
1611
- fun loadCatalog()
1612
- }
1613
- `;
1614
-
1615
- assert.equal(hasAndroidDaoSuspendFunctionsUsage(source), false);
1616
- });
1617
-
1618
- test('hasAndroidTransactionUsage detecta transacciones en DAO y descarta comentarios', () => {
1619
- const source = `
1620
- // @Transaction
1621
- @Dao
1622
- interface OrderDao {
1623
- @Transaction
1624
- fun loadOrderGraph(): Order
1625
- }
1626
- `;
1627
-
1628
- assert.equal(hasAndroidTransactionUsage(source), true);
1629
- assert.equal(
1630
- hasAndroidTransactionUsage(`
1631
- // @Transaction
1632
- // fun shouldNotTrigger()
1633
- `),
1634
- false
1635
- );
1636
- });
1637
-
1638
- test('hasKotlinForceUnwrapUsage detecta operador !! en codigo Kotlin real', () => {
1639
- const source = `
1640
- fun renderName(user: User?) {
1641
- val name = user!!.name
1642
- println(name)
1643
- }
1644
- `;
1645
- assert.equal(hasKotlinForceUnwrapUsage(source), true);
1646
- });
1647
-
1648
- test('hasKotlinForceUnwrapUsage ignora comentarios, strings y operador !=', () => {
1649
- const source = `
1650
- // val name = user!!.name
1651
- val debug = "user!!.name"
1652
- fun isDifferent(left: String?, right: String?) = left != right
1653
- `;
1654
- assert.equal(hasKotlinForceUnwrapUsage(source), false);
1655
- });
1656
-
1657
- test('hasAndroidJavaSourceCode detecta codigo Java real', () => {
1658
- const source = `
1659
- package com.acme.orders;
1660
-
1661
- public class OrdersActivity {
1662
- }
1663
- `;
1664
- assert.equal(hasAndroidJavaSourceCode(source), true);
1665
- });
1666
-
1667
- test('hasAndroidJavaSourceCode ignora menciones Java en comentarios y strings', () => {
1668
- const source = `
1669
- // public class OrdersActivity {}
1670
- val sample = "class OrdersActivity"
1671
- `;
1672
- assert.equal(hasAndroidJavaSourceCode(source), false);
1673
- });
1674
-
1675
- test('hasAndroidHiltDependencyUsage detecta dependencia Hilt real en Gradle', () => {
1676
- const source = `
1677
- dependencies {
1678
- implementation("com.google.dagger:hilt-android:2.51.1")
1679
- kapt("com.google.dagger:hilt-compiler:2.51.1")
1680
- }
1681
- `;
1682
- assert.equal(hasAndroidHiltDependencyUsage(source), true);
1683
- });
1684
-
1685
- test('hasAndroidHiltDependencyUsage ignora comentarios y dependencias no Hilt', () => {
1686
- const source = `
1687
- // implementation("com.google.dagger:hilt-android:2.51.1")
1688
- implementation("com.squareup.retrofit2:retrofit:2.11.0")
1689
- `;
1690
- assert.equal(hasAndroidHiltDependencyUsage(source), false);
1691
- });
1692
-
1693
- test('findAndroidWorkManagerDependencyMatch devuelve payload semantico para WorkManager en Gradle', () => {
1694
- const source = `
1695
- dependencies {
1696
- implementation("androidx.work:work-runtime-ktx:2.9.1")
1697
- }
1698
- `;
1699
-
1700
- const match = findAndroidWorkManagerDependencyMatch(source);
1701
-
1702
- assert.ok(match);
1703
- assert.deepEqual(match.primary_node, {
1704
- kind: 'member',
1705
- name: 'WorkManager dependency',
1706
- lines: [3],
1707
- });
1708
- assert.deepEqual(match.related_nodes, [
1709
- { kind: 'member', name: 'androidx.work:work-runtime-ktx', lines: [3] },
1710
- ]);
1711
- assert.match(match.why, /WorkManager/i);
1712
- assert.match(match.impact, /background|background tasks|background/i);
1713
- assert.match(match.expected_fix, /work-runtime-ktx|WorkManager/i);
1714
- });
1715
-
1716
- test('hasAndroidWorkManagerDependencyUsage detecta dependencia WorkManager real en Gradle y descarta dependencias no relacionadas', () => {
1717
- assert.equal(
1718
- hasAndroidWorkManagerDependencyUsage(`
1719
- dependencies {
1720
- implementation("androidx.work:work-runtime-ktx:2.9.1")
1721
- }
1722
- `),
1723
- true
1724
- );
1725
- assert.equal(
1726
- hasAndroidWorkManagerDependencyUsage(`
1727
- dependencies {
1728
- implementation("androidx.room:room-ktx:2.6.1")
1729
- }
1730
- `),
1731
- false
1732
- );
1733
- });
1734
-
1735
- test('findAndroidVersionCatalogMatch devuelve payload semantico para libs.versions.toml', () => {
1736
- const source = `
1737
- [versions]
1738
- kotlin = "1.9.24"
1739
- androidx-core = "1.13.1"
1740
-
1741
- [libraries]
1742
- androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
1743
- androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidx-core" }
1744
- `;
1745
-
1746
- const match = findAndroidVersionCatalogMatch(source);
1747
-
1748
- assert.ok(match);
1749
- assert.deepEqual(match.primary_node, {
1750
- kind: 'member',
1751
- name: 'libs.versions.toml',
1752
- lines: [2],
1753
- });
1754
- assert.deepEqual(match.related_nodes, [
1755
- { kind: 'member', name: 'versions section', lines: [2] },
1756
- { kind: 'member', name: 'version alias: kotlin', lines: [3] },
1757
- { kind: 'member', name: 'version alias: androidx-core', lines: [4] },
1758
- { kind: 'member', name: 'libraries section', lines: [6] },
1759
- { kind: 'member', name: 'library alias: androidx-core-ktx', lines: [7] },
1760
- {
1761
- kind: 'member',
1762
- name: 'library alias: androidx-lifecycle-runtime-ktx',
1763
- lines: [8],
1764
- },
1765
- ]);
1766
- assert.match(match.why, /version catalog|libs\.versions\.toml/i);
1767
- assert.match(match.impact, /centralizados|accessors/i);
1768
- assert.match(match.expected_fix, /libs\.versions\.toml|catalogo/i);
1769
- });
1770
-
1771
- test('hasAndroidVersionCatalogUsage detecta version catalog real y descarta toml incompleto', () => {
1772
- const source = `
1773
- [versions]
1774
- kotlin = "1.9.24"
1775
-
1776
- [libraries]
1777
- androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "kotlin" }
1778
- `;
1779
-
1780
- assert.equal(hasAndroidVersionCatalogUsage(source), true);
1781
- assert.equal(
1782
- hasAndroidVersionCatalogUsage(`
1783
- [versions]
1784
- kotlin = "1.9.24"
1785
- `),
1786
- false
1787
- );
1788
- });
1789
-
1790
- test('findAndroidAaaPatternMatch devuelve payload semantico para AAA en tests Android', () => {
1791
- const source = `
1792
- class OrderTest {
1793
- @Test
1794
- fun savesOrder() {
1795
- // Arrange
1796
- val repository = FakeRepository()
1797
- // Act
1798
- val result = repository.save()
1799
- // Assert
1800
- assertTrue(result)
1801
- }
1802
- }
1803
- `;
1804
-
1805
- const match = findAndroidAaaPatternMatch(source);
1806
-
1807
- assert.ok(match);
1808
- assert.deepEqual(match.primary_node, {
1809
- kind: 'member',
1810
- name: '@Test fun savesOrder',
1811
- lines: [4],
1812
- });
1813
- assert.deepEqual(match.related_nodes, [
1814
- { kind: 'member', name: 'AAA marker', lines: [5] },
1815
- { kind: 'member', name: 'AAA marker', lines: [7] },
1816
- { kind: 'member', name: 'AAA marker', lines: [9] },
1817
- ]);
1818
- assert.match(match.why, /AAA|Arrange|Act|Assert/i);
1819
- assert.match(match.impact, /intenci[oó]n|separaci[oó]n/i);
1820
- assert.match(match.expected_fix, /Arrange|Act|Assert/i);
1821
- });
1822
-
1823
- test('hasAndroidAaaPatternUsage detecta AAA real y descarta tests sin estructura', () => {
1824
- assert.equal(
1825
- hasAndroidAaaPatternUsage(`
1826
- class OrderTest {
1827
- @Test
1828
- fun savesOrder() {
1829
- // Arrange
1830
- val repository = FakeRepository()
1831
- // Act
1832
- val result = repository.save()
1833
- // Assert
1834
- assertTrue(result)
1835
- }
1836
- }
1837
- `),
1838
- true
1839
- );
1840
- assert.equal(
1841
- hasAndroidAaaPatternUsage(`
1842
- class OrderTest {
1843
- @Test
1844
- fun savesOrder() {
1845
- val repository = FakeRepository()
1846
- val result = repository.save()
1847
- assertTrue(result)
1848
- }
1849
- }
1850
- `),
1851
- false
1852
- );
1853
- });
1854
-
1855
- test('findAndroidGivenWhenThenMatch devuelve payload semantico para BDD en tests Android', () => {
1856
- const source = `
1857
- class OrderTest {
1858
- @Test
1859
- fun savesOrder() {
1860
- // Given
1861
- val repository = FakeRepository()
1862
- // When
1863
- val result = repository.save()
1864
- // Then
1865
- assertTrue(result)
1866
- }
1867
- }
1868
- `;
1869
-
1870
- const match = findAndroidGivenWhenThenMatch(source);
1871
-
1872
- assert.ok(match);
1873
- assert.deepEqual(match.primary_node, {
1874
- kind: 'member',
1875
- name: '@Test fun savesOrder',
1876
- lines: [4],
1877
- });
1878
- assert.deepEqual(match.related_nodes, [
1879
- { kind: 'member', name: 'BDD marker', lines: [5] },
1880
- { kind: 'member', name: 'BDD marker', lines: [7] },
1881
- { kind: 'member', name: 'BDD marker', lines: [9] },
1882
- ]);
1883
- assert.match(match.why, /Given-When-Then|BDD/i);
1884
- assert.match(match.impact, /comportamiento|lenguaje de producto/i);
1885
- assert.match(match.expected_fix, /Given|When|Then/i);
1886
- });
1887
-
1888
- test('hasAndroidGivenWhenThenUsage detecta BDD real y descarta tests sin estructura', () => {
1889
- assert.equal(
1890
- hasAndroidGivenWhenThenUsage(`
1891
- class OrderTest {
1892
- @Test
1893
- fun savesOrder() {
1894
- // Given
1895
- val repository = FakeRepository()
1896
- // When
1897
- val result = repository.save()
1898
- // Then
1899
- assertTrue(result)
1900
- }
1901
- }
1902
- `),
1903
- true
1904
- );
1905
- assert.equal(
1906
- hasAndroidGivenWhenThenUsage(`
1907
- class OrderTest {
1908
- @Test
1909
- fun savesOrder() {
1910
- val repository = FakeRepository()
1911
- val result = repository.save()
1912
- assertTrue(result)
1913
- }
1914
- }
1915
- `),
1916
- false
1917
- );
1918
- });
1919
-
1920
- test('findAndroidJvmUnitTestMatch devuelve payload semantico para unit tests JVM en test/', () => {
1921
- const source = `
1922
- class CatalogRepositoryTest {
1923
- @Test
1924
- fun loadsCatalog() {
1925
- assertTrue(true)
1926
- }
1927
- }
1928
- `;
1929
-
1930
- const match = findAndroidJvmUnitTestMatch(source);
1931
-
1932
- assert.ok(match);
1933
- assert.deepEqual(match.primary_node, {
1934
- kind: 'member',
1935
- name: '@Test fun loadsCatalog',
1936
- lines: [4],
1937
- });
1938
- assert.deepEqual(match.related_nodes, []);
1939
- assert.match(match.why, /test\/|JVM/i);
1940
- assert.match(match.impact, /rápidas|androidTest/i);
1941
- assert.match(match.expected_fix, /src\/test|androidTest/i);
1942
- });
1943
-
1944
- test('hasAndroidJvmUnitTestUsage detecta unit tests JVM reales y descarta fuentes no test', () => {
1945
- assert.equal(
1946
- hasAndroidJvmUnitTestUsage(`
1947
- class CatalogRepositoryTest {
1948
- @Test
1949
- fun loadsCatalog() {
1950
- assertTrue(true)
1951
- }
1952
- }
1953
- `),
1954
- true
1955
- );
1956
- assert.equal(
1957
- hasAndroidJvmUnitTestUsage(`
1958
- class CatalogRepository {
1959
- fun loadsCatalog() {
1960
- assertTrue(true)
1961
- }
1962
- }
1963
- `),
1964
- false
1965
- );
1966
- });
1967
-
1968
- test('findAndroidWorkManagerBackgroundTaskMatch devuelve payload semantico para Worker de WorkManager', () => {
1969
- const source = `
1970
- class SyncWorker(
1971
- appContext: Context,
1972
- workerParams: WorkerParameters,
1973
- ) : CoroutineWorker(appContext, workerParams) {
1974
- override suspend fun doWork(): Result {
1975
- return Result.success()
1976
- }
1977
- }
1978
- `;
1979
-
1980
- const match = findAndroidWorkManagerBackgroundTaskMatch(source);
1981
-
1982
- assert.ok(match);
1983
- assert.deepEqual(match.primary_node, {
1984
- kind: 'class',
1985
- name: 'SyncWorker',
1986
- lines: [2],
1987
- });
1988
- assert.deepEqual(match.related_nodes, [
1989
- { kind: 'member', name: 'CoroutineWorker', lines: [2] },
1990
- { kind: 'call', name: 'doWork', lines: [6] },
1991
- ]);
1992
- assert.match(match.why, /WorkManager|WorkManager/i);
1993
- assert.match(match.impact, /background|Worker/i);
1994
- assert.match(match.expected_fix, /Worker|CoroutineWorker|ListenableWorker/i);
1995
- });
1996
-
1997
- test('hasAndroidWorkManagerBackgroundTaskUsage detecta Worker real y descarta clases no relacionadas', () => {
1998
- assert.equal(
1999
- hasAndroidWorkManagerBackgroundTaskUsage(`
2000
- class SyncWorker(
2001
- appContext: Context,
2002
- workerParams: WorkerParameters,
2003
- ) : CoroutineWorker(appContext, workerParams) {
2004
- override suspend fun doWork(): Result {
2005
- return Result.success()
2006
- }
2007
- }
2008
- `),
2009
- true
2010
- );
2011
- assert.equal(
2012
- hasAndroidWorkManagerBackgroundTaskUsage(`
2013
- class SyncManager(
2014
- appContext: Context,
2015
- workerParams: WorkerParameters,
2016
- ) {
2017
- fun doWork(): Result = Result.success()
2018
- }
2019
- `),
2020
- false
2021
- );
2022
- });
2023
-
2024
- test('findAndroidInstrumentedTestMatch devuelve payload semantico para androidTest instrumentado', () => {
2025
- const source = `
2026
- @RunWith(AndroidJUnit4::class)
2027
- class CatalogInstrumentedTest {
2028
- @Test fun launchesActivity() {
2029
- ActivityScenario.launch(MainActivity::class.java)
2030
- InstrumentationRegistry.getInstrumentation()
2031
- onView(withId(R.id.title)).check(matches(isDisplayed()))
2032
- }
2033
- }
2034
- `;
2035
-
2036
- const match = findAndroidInstrumentedTestMatch(source);
2037
-
2038
- assert.ok(match);
2039
- assert.deepEqual(match.primary_node, {
2040
- kind: 'member',
2041
- name: 'androidTest/',
2042
- lines: [2],
2043
- });
2044
- assert.deepEqual(match.related_nodes, [
2045
- { kind: 'member', name: 'androidTest marker', lines: [2] },
2046
- { kind: 'member', name: 'androidTest marker', lines: [5] },
2047
- { kind: 'member', name: 'androidTest marker', lines: [6] },
2048
- { kind: 'member', name: 'androidTest marker', lines: [7] },
2049
- ]);
2050
- assert.match(match.why, /androidTest|instrumentad/i);
2051
- assert.match(match.impact, /dispositivo|emulador|UI/i);
2052
- assert.match(match.expected_fix, /AndroidJUnit4|ActivityScenario|Espresso/i);
2053
- });
2054
-
2055
- test('hasAndroidInstrumentedTestUsage detecta androidTest real y descarta fuentes no instrumentadas', () => {
2056
- assert.equal(
2057
- hasAndroidInstrumentedTestUsage(`
2058
- @RunWith(AndroidJUnit4::class)
2059
- class CatalogInstrumentedTest {
2060
- @Test fun launchesActivity() {
2061
- ActivityScenario.launch(MainActivity::class.java)
2062
- }
2063
- }
2064
- `),
2065
- true
2066
- );
2067
- assert.equal(
2068
- hasAndroidInstrumentedTestUsage(`
2069
- class CatalogUnitTest {
2070
- @Test fun launchesActivity() {
2071
- assertTrue(true)
2072
- }
2073
- }
2074
- `),
2075
- false
2076
- );
2077
- });
2078
-
2079
- test('hasAndroidHiltFrameworkUsage detecta anotaciones y referencias Hilt reales', () => {
2080
- const source = `
2081
- import dagger.hilt.android.HiltAndroidApp
2082
- import javax.inject.Inject
2083
-
2084
- @HiltAndroidApp
2085
- class App : Application()
2086
- `;
2087
- assert.equal(hasAndroidHiltFrameworkUsage(source), true);
2088
- });
2089
-
2090
- test('hasAndroidHiltFrameworkUsage ignora comentarios y strings', () => {
2091
- const source = `
2092
- // @HiltAndroidApp
2093
- val sample = "dagger.hilt.android"
2094
- `;
2095
- assert.equal(hasAndroidHiltFrameworkUsage(source), false);
2096
- });
2097
-
2098
- test('hasAndroidHiltAndroidAppUsage detecta Application Hilt real', () => {
2099
- const source = `
2100
- @HiltAndroidApp
2101
- class App : Application()
2102
- `;
2103
- assert.equal(hasAndroidHiltAndroidAppUsage(source), true);
2104
- });
2105
-
2106
- test('hasAndroidAndroidEntryPointUsage detecta Activity o Fragment EntryPoint real', () => {
2107
- const source = `
2108
- @AndroidEntryPoint
2109
- class HomeActivity : AppCompatActivity()
2110
- `;
2111
- assert.equal(hasAndroidAndroidEntryPointUsage(source), true);
2112
- });
2113
-
2114
- test('hasAndroidInjectConstructorUsage detecta constructor injection real', () => {
2115
- const source = `
2116
- class HomeViewModel @Inject constructor(
2117
- private val repository: HomeRepository
2118
- )
2119
- `;
2120
- assert.equal(hasAndroidInjectConstructorUsage(source), true);
2121
- });
2122
-
2123
- test('hasAndroidModuleInstallInUsage detecta Module + InstallIn reales', () => {
2124
- const source = `
2125
- @Module
2126
- @InstallIn(SingletonComponent::class)
2127
- object NetworkModule
2128
- `;
2129
- assert.equal(hasAndroidModuleInstallInUsage(source), true);
2130
- });
2131
-
2132
- test('hasAndroidBindsUsage detecta Binds real con Module + InstallIn', () => {
2133
- const source = `
2134
- @Module
2135
- @InstallIn(SingletonComponent::class)
2136
- abstract class NetworkModule {
2137
- @Binds
2138
- abstract fun bindRepository(impl: RepositoryImpl): Repository
2139
- }
2140
- `;
2141
- assert.equal(hasAndroidBindsUsage(source), true);
2142
- });
2143
-
2144
- test('hasAndroidBindsUsage ignora comentarios y fuentes sin Module + InstallIn', () => {
2145
- const source = `
2146
- // @Binds abstract fun bindRepository(impl: RepositoryImpl): Repository
2147
- @Provides
2148
- fun provideRepository(): Repository = RepositoryImpl()
2149
- `;
2150
- assert.equal(hasAndroidBindsUsage(source), false);
2151
- });
2152
-
2153
- test('hasAndroidProvidesUsage detecta Provides real con Module + InstallIn', () => {
2154
- const source = `
2155
- @Module
2156
- @InstallIn(SingletonComponent::class)
2157
- abstract class NetworkModule {
2158
- @Provides
2159
- fun provideRepository(): Repository = RepositoryImpl()
2160
- }
2161
- `;
2162
- assert.equal(hasAndroidProvidesUsage(source), true);
2163
- });
2164
-
2165
- test('hasAndroidProvidesUsage ignora comentarios y fuentes sin Module + InstallIn', () => {
2166
- const source = `
2167
- // @Provides fun provideRepository(): Repository = RepositoryImpl()
2168
- fun provideRepository(): Repository = RepositoryImpl()
2169
- `;
2170
- assert.equal(hasAndroidProvidesUsage(source), false);
2171
- });
2172
-
2173
- test('hasAndroidViewModelScopedUsage detecta ViewModelScoped real', () => {
2174
- const source = `
2175
- @ViewModelScoped
2176
- class HomeRepository @Inject constructor()
2177
- `;
2178
- assert.equal(hasAndroidViewModelScopedUsage(source), true);
2179
- });
2180
-
2181
- test('findAndroidViewModelScopeMatch detecta viewModelScope real en codigo Android', () => {
2182
- const source = `
2183
- class HomeViewModel : ViewModel() {
2184
- fun load() {
2185
- viewModelScope.launch {
2186
- refresh()
2187
- }
2188
- }
2189
- }
2190
- `;
2191
- const match = findAndroidViewModelScopeMatch(source);
2192
- assert.ok(match);
2193
- assert.equal(match?.primary_node.name, 'viewModelScope');
2194
- assert.equal(hasAndroidViewModelScopeUsage(source), true);
2195
- });
2196
-
2197
- test('findAndroidViewModelScopeMatch ignora comentarios, strings y nombres parciales', () => {
2198
- const source = `
2199
- // viewModelScope.launch { }
2200
- val sample = "viewModelScope"
2201
- val viewModelScoped = false
2202
- `;
2203
- assert.equal(findAndroidViewModelScopeMatch(source), undefined);
2204
- assert.equal(hasAndroidViewModelScopeUsage(source), false);
2205
- });
2206
-
2207
- test('findAndroidAppStartupMatch detecta Initializer real en codigo Android', () => {
2208
- const source = `
2209
- class FeatureInitializer : Initializer<Unit> {
2210
- override fun create(context: Context) {
2211
- return Unit
2212
- }
2213
-
2214
- override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
2215
- }
2216
- `;
2217
- const match = findAndroidAppStartupMatch(source);
2218
- assert.ok(match);
2219
- assert.equal(match?.primary_node.name, 'FeatureInitializer');
2220
- assert.equal(hasAndroidAppStartupUsage(source), true);
2221
- });
2222
-
2223
- test('findAndroidAppStartupMatch ignora comentarios, strings y clases sin Initializer', () => {
2224
- const source = `
2225
- // class FeatureInitializer : Initializer<Unit> {}
2226
- val sample = "Initializer<Unit>"
2227
- class FeatureInitializerHelper
2228
- `;
2229
- assert.equal(findAndroidAppStartupMatch(source), undefined);
2230
- assert.equal(hasAndroidAppStartupUsage(source), false);
2231
- });
2232
-
2233
- test('findAndroidAnalyticsMatch detecta tracking real de analytics en codigo Android', () => {
2234
- const source = `
2235
- import com.google.firebase.analytics.FirebaseAnalytics
2236
-
2237
- class PurchaseAnalytics(private val firebaseAnalytics: FirebaseAnalytics) {
2238
- fun trackPurchase(eventName: String) {
2239
- firebaseAnalytics.trackEvent(eventName)
2240
- }
2241
- }
2242
- `;
2243
- const match = findAndroidAnalyticsMatch(source);
2244
- assert.ok(match);
2245
- assert.equal(match?.primary_node.name, 'PurchaseAnalytics');
2246
- assert.equal(hasAndroidAnalyticsUsage(source), true);
2247
- });
2248
-
2249
- test('findAndroidAnalyticsMatch ignora comentarios, strings y clases sin tracking', () => {
2250
- const source = `
2251
- // firebaseAnalytics.logEvent("purchase")
2252
- val sample = "trackEvent(\"purchase\")"
2253
- class PurchaseLogger {
2254
- fun log() = Unit
2255
- }
2256
- `;
2257
- assert.equal(findAndroidAnalyticsMatch(source), undefined);
2258
- assert.equal(hasAndroidAnalyticsUsage(source), false);
2259
- });
2260
-
2261
- test('findAndroidProfilerMatch detecta profiling real de Android en codigo de produccion', () => {
2262
- const source = `
2263
- import android.os.Debug
2264
-
2265
- class CheckoutProfiler {
2266
- fun traceStartup() {
2267
- Debug.startMethodTracing()
2268
- Debug.stopMethodTracing()
2269
- }
2270
- }
2271
- `;
2272
- const match = findAndroidProfilerMatch(source);
2273
- assert.ok(match);
2274
- assert.equal(match?.primary_node.name, 'CheckoutProfiler');
2275
- assert.equal(hasAndroidProfilerUsage(source), true);
2276
- });
2277
-
2278
- test('findAndroidProfilerMatch ignora comentarios, strings y clases sin profiling', () => {
2279
- const source = `
2280
- // Trace.beginSection("startup")
2281
- val sample = "startMethodTracing"
2282
- class CheckoutLogger {
2283
- fun log() = Unit
2284
- }
2285
- `;
2286
- assert.equal(findAndroidProfilerMatch(source), undefined);
2287
- assert.equal(hasAndroidProfilerUsage(source), false);
2288
- });
2289
-
2290
- test('findAndroidBaselineProfilesMatch detecta BaselineProfileRule real en androidTest', () => {
2291
- const source = `
2292
- import androidx.benchmark.macro.junit4.BaselineProfileRule
2293
-
2294
- class StartupBaselineProfileTest {
2295
- @get:Rule
2296
- val baselineProfileRule = BaselineProfileRule()
2297
-
2298
- @Test
2299
- fun generateBaselineProfile() {
2300
- baselineProfileRule.collect(
2301
- packageName = "com.acme.app"
2302
- ) {
2303
- startActivityAndWait()
2304
- }
2305
- }
2306
- }
2307
- `;
2308
- const match = findAndroidBaselineProfilesMatch(source);
2309
- assert.ok(match);
2310
- assert.equal(match?.primary_node.name, 'BaselineProfileRule');
2311
- assert.equal(hasAndroidBaselineProfilesUsage(source), true);
2312
- });
2313
-
2314
- test('findAndroidBaselineProfilesMatch ignora comentarios, strings y referencias parciales', () => {
2315
- const source = `
2316
- // BaselineProfileRule()
2317
- val sample = "BaselineProfileRule.collect(packageName = \\"com.acme.app\\")"
2318
- fun render() {
2319
- val collect = "collect()"
2320
- }
2321
- `;
2322
- assert.equal(findAndroidBaselineProfilesMatch(source), undefined);
2323
- assert.equal(hasAndroidBaselineProfilesUsage(source), false);
2324
- });
2325
-
2326
- test('findAndroidSkipRecompositionMatch detecta composables con parametros estables o inmutables', () => {
2327
- const source = `
2328
- import androidx.compose.runtime.Composable
2329
- import kotlinx.collections.immutable.ImmutableList
2330
-
2331
- @Composable
2332
- fun Feed(items: ImmutableList<FeedItem>, state: FeedUiState) {
2333
- Text(text = state.title)
2334
- }
2335
- `;
2336
- const match = findAndroidSkipRecompositionMatch(source);
2337
- assert.ok(match);
2338
- assert.equal(match?.primary_node.name, '@Composable fun Feed');
2339
- assert.equal(hasAndroidSkipRecompositionUsage(source), true);
2340
- });
2341
-
2342
- test('findAndroidSkipRecompositionMatch ignora composables sin parametros estables o inmutables', () => {
2343
- const source = `
2344
- @Composable
2345
- fun Feed(items: List<FeedItem>, state: FeedUiState) {
2346
- Text(text = state.title)
2347
- }
2348
- `;
2349
- assert.equal(findAndroidSkipRecompositionMatch(source), undefined);
2350
- assert.equal(hasAndroidSkipRecompositionUsage(source), false);
2351
- });
2352
-
2353
- test('findAndroidStabilityMatch detecta composables con tipos @Stable o @Immutable', () => {
2354
- const source = `
2355
- import androidx.compose.runtime.Composable
2356
- import androidx.compose.runtime.Immutable
2357
- import androidx.compose.runtime.Stable
2358
-
2359
- @Stable
2360
- class PlaybackState(val isPlaying: Boolean)
2361
-
2362
- @Immutable
2363
- data class FeedUiState(val title: String)
2364
-
2365
- @Composable
2366
- fun Feed(state: FeedUiState, playback: PlaybackState) {
2367
- Text(text = state.title)
2368
- }
2369
- `;
2370
- const match = findAndroidStabilityMatch(source);
2371
- assert.ok(match);
2372
- assert.equal(match?.primary_node.name, '@Composable fun Feed');
2373
- assert.equal(hasAndroidStabilityUsage(source), true);
2374
- });
2375
-
2376
- test('findAndroidStabilityMatch ignora composables sin tipos estables o inmutables', () => {
2377
- const source = `
2378
- @Composable
2379
- fun Feed(state: FeedUiState, playback: PlaybackState) {
2380
- Text(text = state.title)
2381
- }
2382
-
2383
- data class FeedUiState(val title: String)
2384
- class PlaybackState(val isPlaying: Boolean)
2385
- `;
2386
- assert.equal(findAndroidStabilityMatch(source), undefined);
2387
- assert.equal(hasAndroidStabilityUsage(source), false);
2388
- });
2389
-
2390
- test('findAndroidComposableFunctionMatch detecta composable real en codigo Android', () => {
2391
- const source = `
2392
- @Composable
2393
- fun HomeScreen() {
2394
- Box(modifier = Modifier)
2395
- }
2396
- `;
2397
- const match = findAndroidComposableFunctionMatch(source);
2398
- assert.ok(match);
2399
- assert.equal(match?.primary_node.name, '@Composable fun HomeScreen');
2400
- assert.deepEqual(match?.lines, [3]);
2401
- assert.equal(hasAndroidComposableFunctionUsage(source), true);
2402
- });
2403
-
2404
- test('findAndroidComposableFunctionMatch ignora comentarios y strings', () => {
2405
- const source = `
2406
- // @Composable fun HomeScreen() {}
2407
- val sample = "@Composable fun HomeScreen()"
2408
- `;
2409
- assert.equal(findAndroidComposableFunctionMatch(source), undefined);
2410
- assert.equal(hasAndroidComposableFunctionUsage(source), false);
2411
- });
2412
-
2413
- test('findAndroidArgumentsMatch devuelve payload semantico para argumentos entre pantallas', () => {
2414
- const source = `
2415
- import androidx.lifecycle.SavedStateHandle
2416
-
2417
- class OrderDetailViewModel(
2418
- private val navController: NavController,
2419
- savedStateHandle: SavedStateHandle,
2420
- ) : ViewModel() {
2421
- val orderId = checkNotNull(savedStateHandle["orderId"])
2422
-
2423
- fun openOtherScreen(route: String) {
2424
- navController.navigate(route)
2425
- }
2426
- }
2427
- `;
2428
- const match = findAndroidArgumentsMatch(source);
2429
- assert.ok(match);
2430
- assert.equal(match?.primary_node.name, 'OrderDetailViewModel');
2431
- assert.equal(hasAndroidArgumentsUsage(source), true);
2432
- });
2433
-
2434
- test('findAndroidArgumentsMatch ignora navigate sin argumentos y comentarios', () => {
2435
- const source = `
2436
- // NavHost(navController = navController, startDestination = "orders") {}
2437
- class OrderDetailViewModel : ViewModel() {
2438
- fun openDetails(navController: NavController) {
2439
- navController.navigate("orders")
2440
- }
2441
- }
2442
- `;
2443
- assert.equal(findAndroidArgumentsMatch(source), undefined);
2444
- assert.equal(hasAndroidArgumentsUsage(source), false);
2445
- });
2446
-
2447
- test('findAndroidSingleActivityComposeShellMatch detecta Activity shell con Compose', () => {
2448
- const source = `
2449
- class MainActivity : ComponentActivity() {
2450
- override fun onCreate(savedInstanceState: Bundle?) {
2451
- super.onCreate(savedInstanceState)
2452
- setContent {
2453
- NavHost(navController = rememberNavController(), startDestination = homeRoute()) { }
2454
- }
2455
- }
2456
- }
2457
- `;
2458
- const match = findAndroidSingleActivityComposeShellMatch(source);
2459
- assert.ok(match);
2460
- assert.equal(match?.primary_node.name, 'MainActivity');
2461
- assert.equal(hasAndroidSingleActivityComposeShellUsage(source), true);
2462
- });
2463
-
2464
- test('findAndroidSingleActivityComposeShellMatch ignora Activities sin Compose shell', () => {
2465
- const source = `
2466
- class MainActivity : ComponentActivity() {
2467
- override fun onCreate(savedInstanceState: Bundle?) {
2468
- super.onCreate(savedInstanceState)
2469
- }
2470
- }
2471
- `;
2472
- assert.equal(findAndroidSingleActivityComposeShellMatch(source), undefined);
2473
- assert.equal(hasAndroidSingleActivityComposeShellUsage(source), false);
2474
- });
2475
-
2476
- test('findAndroidGodActivityMatch detecta God Activity que mezcla shell Compose y composables', () => {
2477
- const source = `
2478
- class MainActivity : ComponentActivity() {
2479
- override fun onCreate(savedInstanceState: Bundle?) {
2480
- super.onCreate(savedInstanceState)
2481
- setContent {
2482
- HomeScreen()
2483
- }
2484
- }
2485
- }
2486
-
2487
- @Composable
2488
- fun HomeScreen() {
2489
- Box(modifier = Modifier)
2490
- }
2491
- `;
2492
- const match = findAndroidGodActivityMatch(source);
2493
- assert.ok(match);
2494
- assert.equal(match?.primary_node.name, 'MainActivity');
2495
- assert.equal(hasAndroidGodActivityUsage(source), true);
2496
- });
2497
-
2498
- test('findAndroidGodActivityMatch ignora Activity shell pura', () => {
2499
- const source = `
2500
- class MainActivity : ComponentActivity() {
2501
- override fun onCreate(savedInstanceState: Bundle?) {
2502
- super.onCreate(savedInstanceState)
2503
- setContent {
2504
- NavHost(navController = rememberNavController(), startDestination = homeRoute()) { }
2505
- }
2506
- }
2507
- }
2508
- `;
2509
- assert.equal(findAndroidGodActivityMatch(source), undefined);
2510
- assert.equal(hasAndroidGodActivityUsage(source), false);
2511
- });
2512
-
2513
- test('hasAndroidAsyncTaskUsage detecta AsyncTask real en codigo Android', () => {
2514
- const source = `
2515
- import android.os.AsyncTask
2516
-
2517
- class LoadDataTask : AsyncTask<Unit, Unit, String>() {
2518
- override fun doInBackground(vararg params: Unit): String = "ok"
2519
- }
2520
- `;
2521
- assert.equal(hasAndroidAsyncTaskUsage(source), true);
2522
- });
2523
-
2524
- test('hasAndroidAsyncTaskUsage ignora comentarios, strings y nombres parciales', () => {
2525
- const source = `
2526
- // AsyncTask should be removed
2527
- val sample = "AsyncTask"
2528
- class AsyncTaskRunner
2529
- `;
2530
- assert.equal(hasAndroidAsyncTaskUsage(source), false);
2531
- });
2532
-
2533
- test('hasAndroidFindViewByIdUsage detecta findViewById real en codigo Android', () => {
2534
- const source = `
2535
- class HomeActivity : AppCompatActivity() {
2536
- fun render() {
2537
- val title = findViewById<TextView>(R.id.title)
2538
- }
2539
- }
2540
- `;
2541
- assert.equal(hasAndroidFindViewByIdUsage(source), true);
2542
- });
2543
-
2544
- test('hasAndroidFindViewByIdUsage ignora comentarios, strings y nombres parciales', () => {
2545
- const source = `
2546
- // findViewById<TextView>(R.id.title)
2547
- val sample = "findViewById"
2548
- class FindViewByIdHelper
2549
- `;
2550
- assert.equal(hasAndroidFindViewByIdUsage(source), false);
2551
- });
2552
-
2553
- test('hasAndroidRxJavaUsage detecta RxJava real en codigo Android', () => {
2554
- const source = `
2555
- import io.reactivex.rxjava3.core.Observable
2556
-
2557
- class HomeRepository {
2558
- fun load() = Observable.just("ok")
2559
- }
2560
- `;
2561
- assert.equal(hasAndroidRxJavaUsage(source), true);
2562
- });
2563
-
2564
- test('hasAndroidRxJavaUsage ignora comentarios, strings y nombres parciales', () => {
2565
- const source = `
2566
- // Observable.just("ok")
2567
- val sample = "RxJava"
2568
- class ObservableHelper
2569
- `;
2570
- assert.equal(hasAndroidRxJavaUsage(source), false);
2571
- });
2572
-
2573
- test('hasAndroidDispatcherUsage detecta Dispatchers reales en codigo Android', () => {
2574
- const source = `
2575
- import kotlinx.coroutines.Dispatchers
2576
- import kotlinx.coroutines.withContext
2577
-
2578
- suspend fun load() = withContext(Dispatchers.IO) {
2579
- "ok"
2580
- }
2581
- `;
2582
- assert.equal(hasAndroidDispatcherUsage(source), true);
2583
- });
2584
-
2585
- test('hasAndroidDispatcherUsage ignora comentarios, strings y nombres parciales', () => {
2586
- const source = `
2587
- // withContext(Dispatchers.IO)
2588
- val sample = "Dispatchers.Main"
2589
- class DispatchersHelper
2590
- `;
2591
- assert.equal(hasAndroidDispatcherUsage(source), false);
2592
- });
2593
-
2594
- test('hasAndroidWithContextUsage detecta withContext real en codigo Android', () => {
2595
- const source = `
2596
- import kotlinx.coroutines.Dispatchers
2597
- import kotlinx.coroutines.withContext
2598
-
2599
- suspend fun load() = withContext(Dispatchers.IO) {
2600
- "ok"
2601
- }
2602
- `;
2603
- assert.equal(hasAndroidWithContextUsage(source), true);
2604
- });
2605
-
2606
- test('hasAndroidWithContextUsage ignora comentarios, strings y nombres parciales', () => {
2607
- const source = `
2608
- // withContext(Dispatchers.IO)
2609
- val sample = "withContext"
2610
- class WithContextHelper
2611
- `;
2612
- assert.equal(hasAndroidWithContextUsage(source), false);
2613
- });
2614
-
2615
- test('hasAndroidCoroutineTryCatchUsage detecta try-catch real en codigo coroutine Android', () => {
2616
- const source = `
2617
- import kotlinx.coroutines.Dispatchers
2618
- import kotlinx.coroutines.withContext
2619
-
2620
- suspend fun load() {
2621
- try {
2622
- withContext(Dispatchers.IO) { }
2623
- } catch (e: Exception) {
2624
- throw e
2625
- }
2626
- }
2627
- `;
2628
- assert.equal(hasAndroidCoroutineTryCatchUsage(source), true);
2629
- });
2630
-
2631
- test('findAndroidSupervisorScopeMatch detecta supervisorScope real en codigo coroutine Android', () => {
2632
- const source = `
2633
- import kotlinx.coroutines.async
2634
- import kotlinx.coroutines.launch
2635
-
2636
- suspend fun load() = supervisorScope {
2637
- val summary = async { loadSummary() }
2638
- launch { refreshCache() }
2639
- summary.await()
2640
- }
2641
- `;
2642
-
2643
- const match = findAndroidSupervisorScopeMatch(source);
2644
-
2645
- assert.ok(match);
2646
- assert.deepEqual(match?.primary_node, {
2647
- kind: 'member',
2648
- name: 'supervisorScope',
2649
- lines: [5],
2650
- });
2651
- assert.equal(hasAndroidSupervisorScopeUsage(source), true);
2652
- });
2653
-
2654
- test('findAndroidSupervisorScopeMatch ignora comentarios, strings y nombres parciales', () => {
2655
- const source = `
2656
- // supervisorScope { launch { } }
2657
- val sample = "supervisorScope"
2658
- class SupervisorScopeHelper
2659
- `;
2660
-
2661
- assert.equal(findAndroidSupervisorScopeMatch(source), undefined);
2662
- assert.equal(hasAndroidSupervisorScopeUsage(source), false);
2663
- });
2664
-
2665
- test('hasAndroidCoroutineTryCatchUsage ignora comentarios, strings y codigo no coroutine', () => {
2666
- const source = `
2667
- // try { withContext(Dispatchers.IO) } catch (e: Exception) {}
2668
- val sample = "try catch"
2669
- fun notCoroutine() {
2670
- try {
2671
- println("ok")
2672
- } catch (e: Exception) {
2673
- println(e)
2674
- }
2675
- }
2676
- `;
2677
- assert.equal(hasAndroidCoroutineTryCatchUsage(source), false);
2678
- });
2679
-
2680
- test('hasAndroidNoConsoleLogUsage detecta logs reales en codigo Android y permite debug guards', () => {
2681
- const source = `
2682
- fun render() {
2683
- Log.d("visible in production")
2684
- if (BuildConfig.DEBUG) Log.d("visible in debug only")
2685
- if (BuildConfig.DEBUG) {
2686
- Log.d("Tag", "debug only")
2687
- }
2688
- }
2689
- `;
2690
- assert.equal(hasAndroidNoConsoleLogUsage(source), true);
2691
- });
2692
-
2693
- test('hasAndroidNoConsoleLogUsage ignora comentarios, strings y logs protegidos por BuildConfig.DEBUG', () => {
2694
- const source = `
2695
- // Log.d("debug")
2696
- val sample = "Log.e(\\"Tag\\", \\"debug\\")"
2697
- fun render() {
2698
- if (BuildConfig.DEBUG) Log.d("debug only")
2699
- if (BuildConfig.DEBUG) {
2700
- Log.e("Tag", "debug only")
2701
- }
2702
- }
2703
- `;
2704
- assert.equal(hasAndroidNoConsoleLogUsage(source), false);
2705
- });
2706
-
2707
- test('hasAndroidTimberUsage detecta Timber real en codigo Android y permite debug guards', () => {
2708
- const source = `
2709
- import timber.log.Timber
2710
-
2711
- fun render() {
2712
- Timber.d("visible in production")
2713
- if (BuildConfig.DEBUG) Timber.d("visible in debug only")
2714
- }
2715
- `;
2716
- assert.equal(hasAndroidTimberUsage(source), true);
2717
- });
2718
-
2719
- test('hasAndroidTimberUsage ignora comentarios, strings y logs protegidos por BuildConfig.DEBUG', () => {
2720
- const source = `
2721
- // Timber.d("debug")
2722
- val sample = "Timber.e(\\"Tag\\", \\"debug\\")"
2723
- fun render() {
2724
- if (BuildConfig.DEBUG) Timber.d("debug only")
2725
- }
2726
- `;
2727
- assert.equal(hasAndroidTimberUsage(source), false);
2728
- });
2729
-
2730
- test('hasAndroidBuildConfigConstantUsage detecta constantes BuildConfig reales en codigo Android', () => {
2731
- const source = `
2732
- fun versionName(): String {
2733
- return BuildConfig.VERSION_NAME
2734
- }
2735
- `;
2736
- assert.equal(hasAndroidBuildConfigConstantUsage(source), true);
2737
- });
2738
-
2739
- test('hasAndroidBuildConfigConstantUsage ignora comentarios, strings y BuildConfig.DEBUG', () => {
2740
- const source = `
2741
- // BuildConfig.VERSION_NAME
2742
- val sample = "BuildConfig.BUILD_TYPE"
2743
- fun render() {
2744
- if (BuildConfig.DEBUG) {
2745
- Timber.d("debug only")
2746
- }
2747
- }
2748
- `;
2749
- assert.equal(hasAndroidBuildConfigConstantUsage(source), false);
2750
- });
2751
-
2752
- test('hasAndroidHardcodedStringUsage detecta strings hardcodeadas en codigo Android', () => {
2753
- const source = `
2754
- fun render() {
2755
- val title = "Hola mundo"
2756
- }
2757
- `;
2758
- assert.equal(hasAndroidHardcodedStringUsage(source), true);
2759
- });
2760
-
2761
- test('hasAndroidHardcodedStringUsage ignora comentarios y referencias a resources', () => {
2762
- const source = `
2763
- // "Hola mundo"
2764
- fun render() {
2765
- val title = R.string.app_name
2766
- }
2767
- `;
2768
- assert.equal(hasAndroidHardcodedStringUsage(source), false);
2769
- });
2770
-
2771
- test('findAndroidStringsXmlMatch devuelve payload semantico para strings.xml localizado', () => {
2772
- const source = `
2773
- <resources>
2774
- <string name="app_name">Mi App</string>
2775
- <string-array name="welcome_steps">
2776
- <item>Bienvenido</item>
2777
- </string-array>
2778
- </resources>
2779
- `;
2780
-
2781
- const match = findAndroidStringsXmlMatch(source);
2782
-
2783
- assert.ok(match);
2784
- assert.deepEqual(match.primary_node, {
2785
- kind: 'member',
2786
- name: 'strings.xml',
2787
- lines: [2],
2788
- });
2789
- assert.deepEqual(match.related_nodes, [
2790
- { kind: 'property', name: 'string', lines: [3] },
2791
- { kind: 'property', name: 'string-array', lines: [4] },
2792
- ]);
2793
- assert.match(match.why, /strings\.xml|localiz/i);
2794
- assert.match(match.impact, /internacionalizaci[oó]n|mantenimiento/i);
2795
- assert.match(match.expected_fix, /values-\\*\/strings\.xml|R\.string/i);
2796
- });
2797
-
2798
- test('hasAndroidStringsXmlUsage ignora comentarios y XML incompleto', () => {
2799
- const source = `
2800
- <!-- <string name="debug">Hola</string> -->
2801
- <resources>
2802
- <!-- strings xml placeholder -->
2803
- </resources>
2804
- `;
2805
-
2806
- assert.equal(hasAndroidStringsXmlUsage(source), false);
2807
- });
2808
-
2809
- test('findAndroidStringFormattingMatch devuelve payload semantico para strings.xml con placeholders posicionales', () => {
2810
- const source = `
2811
- <resources>
2812
- <string name="order_summary">Hola %1$s, total %2$d</string>
2813
- <string name="app_name">Mi App</string>
2814
- </resources>
2815
- `;
2816
-
2817
- const match = findAndroidStringFormattingMatch(source);
2818
-
2819
- assert.ok(match);
2820
- assert.deepEqual(match.primary_node, {
2821
- kind: 'member',
2822
- name: 'strings.xml',
2823
- lines: [2],
2824
- });
2825
- assert.deepEqual(match.related_nodes, [
2826
- { kind: 'property', name: 'formatted string', lines: [3] },
2827
- ]);
2828
- assert.match(match.why, /placeholders|argumentos|idiomas/i);
2829
- assert.match(match.impact, /locale|traducci[oó]n|argumentos/i);
2830
- assert.match(match.expected_fix, /%1\$s|%2\$d|strings\.xml/i);
2831
- });
2832
-
2833
- test('findAndroidStringFormattingMatch ignora strings sin placeholders posicionales', () => {
2834
- const source = `
2835
- <resources>
2836
- <string name="order_summary">Hola mundo</string>
2837
- </resources>
2838
- `;
2839
-
2840
- assert.equal(findAndroidStringFormattingMatch(source), undefined);
2841
- assert.equal(hasAndroidStringFormattingUsage(source), false);
2842
- });
2843
-
2844
- test('findAndroidPluralsXmlMatch devuelve payload semantico para plurals.xml localizado', () => {
2845
- const source = `
2846
- <resources>
2847
- <plurals name="notification_count">
2848
- <item quantity="one">1 notificación</item>
2849
- <item quantity="other">%d notificaciones</item>
2850
- </plurals>
2851
- </resources>
2852
- `;
2853
-
2854
- const match = findAndroidPluralsXmlMatch(source);
2855
-
2856
- assert.ok(match);
2857
- assert.deepEqual(match.primary_node, {
2858
- kind: 'member',
2859
- name: 'plurals.xml',
2860
- lines: [2],
2861
- });
2862
- assert.deepEqual(match.related_nodes, [
2863
- { kind: 'property', name: 'plurals', lines: [3] },
2864
- { kind: 'property', name: 'plural item', lines: [4] },
2865
- ]);
2866
- assert.match(match.why, /plurals\.xml|plural/i);
2867
- assert.match(match.impact, /plural|idioma|cantidad/i);
2868
- assert.match(match.expected_fix, /plurals|quantity|R\.plurals/i);
2869
- });
2870
-
2871
- test('hasAndroidPluralsXmlUsage ignora comentarios y XML incompleto', () => {
2872
- const source = `
2873
- <!-- <plurals name="debug_count"> -->
2874
- <resources>
2875
- <item quantity="one">1 notificación</item>
2876
- </resources>
2877
- `;
2878
-
2879
- assert.equal(hasAndroidPluralsXmlUsage(source), false);
2880
- });
2881
-
2882
- test('hasAndroidSingletonUsage detecta object declarations y companion singleton holders', () => {
2883
- const source = `
2884
- object SessionManager {
2885
- fun refresh() {}
2886
- }
2887
-
2888
- class HomeRepository private constructor() {
2889
- companion object {
2890
- @Volatile private var INSTANCE: HomeRepository? = null
2891
-
2892
- fun getInstance(): HomeRepository {
2893
- return INSTANCE ?: HomeRepository()
2894
- }
2895
- }
2896
- }
2897
- `;
2898
- assert.equal(hasAndroidSingletonUsage(source), true);
2899
- });
2900
-
2901
- test('hasAndroidSingletonUsage ignora anonymous objects y companion objects inocuos', () => {
2902
- const source = `
2903
- val listener = object : Runnable {
2904
- override fun run() {}
2905
- }
2906
-
2907
- @Module
2908
- object NetworkModule {
2909
- fun provideClient(): String = "ok"
2910
- }
2911
-
2912
- class Themes {
2913
- companion object {
2914
- const val DEFAULT = "dark"
2915
- }
2916
- }
2917
- `;
2918
- assert.equal(hasAndroidSingletonUsage(source), false);
2919
- });
2920
-
2921
94
  test('findKotlinPresentationSrpMatch devuelve payload semantico para SRP-Android en presentation', () => {
2922
95
  const source = `
2923
96
  import android.content.SharedPreferences