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.
- package/AGENTS.md +1 -16
- package/CHANGELOG.md +0 -101
- package/README.md +10 -14
- package/VERSION +1 -1
- package/core/facts/detectors/text/android.test.ts +0 -2827
- package/core/facts/detectors/text/android.ts +182 -5121
- package/core/facts/detectors/text/ios.test.ts +12 -290
- package/core/facts/detectors/text/ios.ts +28 -301
- package/core/facts/detectors/typescript/index.test.ts +139 -3733
- package/core/facts/detectors/typescript/index.ts +264 -4959
- package/core/facts/extractHeuristicFacts.ts +11 -328
- package/core/gate/evaluateRules.test.ts +0 -7
- package/core/gate/evaluateRules.ts +2 -1
- package/core/rules/presets/heuristics/android.test.ts +1 -399
- package/core/rules/presets/heuristics/android.ts +1 -1481
- package/core/rules/presets/heuristics/ios.test.ts +1 -11
- package/core/rules/presets/heuristics/ios.ts +0 -36
- package/core/rules/presets/heuristics/typescript.test.ts +2 -158
- package/core/rules/presets/heuristics/typescript.ts +0 -508
- package/core/rules/presets/iosEnterpriseRuleSet.test.ts +0 -5
- package/core/rules/presets/iosEnterpriseRuleSet.ts +5 -5
- package/docs/README.md +3 -3
- package/docs/operations/RELEASE_NOTES.md +1 -94
- package/docs/operations/framework-menu-consumer-walkthrough.md +15 -18
- package/docs/product/API_REFERENCE.md +1 -1
- package/docs/product/CONFIGURATION.md +0 -7
- package/docs/product/USAGE.md +1 -1
- package/docs/validation/README.md +1 -3
- package/docs/validation/ios-avdlee-parity-matrix.md +1 -1
- package/integrations/config/skillsCompilerTemplates.test.ts +0 -145
- package/integrations/config/skillsCompilerTemplates.ts +2 -1013
- package/integrations/config/skillsDetectorRegistry.ts +8 -523
- package/integrations/config/skillsMarkdownRules.ts +8 -1088
- package/integrations/config/skillsRuleSet.ts +3 -44
- package/integrations/evidence/buildEvidence.ts +5 -34
- package/integrations/evidence/platformSummary.test.ts +9 -73
- package/integrations/evidence/platformSummary.ts +7 -165
- package/integrations/evidence/repoState.ts +0 -3
- package/integrations/evidence/rulesCoverage.ts +0 -83
- package/integrations/evidence/schema.ts +0 -29
- package/integrations/evidence/writeEvidence.test.ts +0 -4
- package/integrations/evidence/writeEvidence.ts +2 -41
- package/integrations/gate/evaluateAiGate.ts +8 -312
- package/integrations/gate/remediationCatalog.ts +2 -20
- package/integrations/gate/stagePolicies.ts +18 -24
- package/integrations/git/astIntelligenceDualValidation.ts +2 -2
- package/integrations/git/gitAtomicity.ts +39 -284
- package/integrations/git/resolveGitRefs.ts +6 -35
- package/integrations/git/runPlatformGate.ts +143 -512
- package/integrations/git/runPlatformGateOutput.ts +8 -13
- package/integrations/git/stageRunners.ts +41 -26
- package/integrations/lifecycle/adapter.ts +0 -24
- package/integrations/lifecycle/audit.ts +49 -14
- package/integrations/lifecycle/cli.ts +20 -37
- package/integrations/lifecycle/cliSdd.ts +3 -4
- package/integrations/lifecycle/doctor.ts +1 -1
- package/integrations/lifecycle/packageInfo.ts +1 -118
- package/integrations/lifecycle/policyReconcile.ts +4 -27
- package/integrations/lifecycle/preWriteAutomation.ts +5 -5
- package/integrations/lifecycle/state.ts +1 -8
- package/integrations/lifecycle/watch.ts +8 -28
- package/integrations/mcp/aiGateCheck.ts +10 -194
- package/integrations/mcp/autoExecuteAiStart.ts +4 -7
- package/integrations/mcp/enterpriseServer.ts +3 -19
- package/integrations/mcp/preFlightCheck.ts +10 -89
- package/integrations/policy/gitAtomicityEnforcement.ts +2 -2
- package/integrations/policy/heuristicsEnforcement.ts +2 -2
- package/integrations/policy/policyProfiles.ts +18 -24
- package/integrations/policy/preWriteEnforcement.ts +1 -1
- package/integrations/policy/sddCompletenessEnforcement.ts +2 -2
- package/integrations/policy/skillsEnforcement.ts +47 -1
- package/integrations/policy/tddBddEnforcement.ts +2 -2
- package/integrations/sdd/evidenceScaffold.ts +8 -124
- package/integrations/tdd/contract.ts +0 -1
- package/integrations/tdd/enforcement.ts +0 -103
- package/integrations/tdd/types.ts +0 -6
- package/package.json +1 -1
- package/scripts/check-tracking-single-active.sh +1 -1
- package/scripts/framework-menu-advanced-view-lib.ts +0 -49
- package/scripts/framework-menu-consumer-actions-lib.ts +32 -32
- package/scripts/framework-menu-consumer-preflight-render.ts +0 -10
- package/scripts/framework-menu-consumer-preflight-run.ts +5 -31
- package/scripts/framework-menu-consumer-preflight-types.ts +0 -12
- package/scripts/framework-menu-consumer-runtime-actions.ts +5 -11
- package/scripts/framework-menu-consumer-runtime-audit.ts +28 -0
- package/scripts/framework-menu-consumer-runtime-evidence-classic.ts +42 -118
- package/scripts/framework-menu-consumer-runtime-lib.ts +0 -38
- package/scripts/framework-menu-consumer-runtime-menu.ts +15 -55
- package/scripts/framework-menu-consumer-runtime-types.ts +0 -4
- package/scripts/framework-menu-evidence-summary-read.ts +1 -17
- package/scripts/framework-menu-evidence-summary-types.ts +0 -3
- package/scripts/framework-menu-layout-data.ts +23 -3
- package/scripts/framework-menu-system-notifications-cause.ts +1 -24
- package/scripts/framework-menu-system-notifications-env.ts +0 -8
- package/scripts/framework-menu-system-notifications-gate.ts +2 -9
- package/scripts/framework-menu-system-notifications-macos-applescript-dialog.ts +1 -1
- package/scripts/framework-menu-system-notifications-macos-dialog-payload.ts +2 -14
- package/scripts/framework-menu-system-notifications-macos-swift-source.ts +1 -1
- package/scripts/framework-menu-system-notifications-payloads-blocked.ts +4 -128
- package/scripts/framework-menu-system-notifications-payloads.ts +1 -8
- package/scripts/framework-menu-system-notifications-remediation.ts +1 -15
- package/scripts/framework-menu-system-notifications-text.ts +1 -7
- package/scripts/framework-menu.ts +2 -37
- package/scripts/package-install-smoke-consumer-git-repo-lib.ts +1 -10
- package/scripts/package-install-smoke-consumer-npm-lib.ts +9 -46
- package/skills.lock.json +1244 -807
- package/integrations/evidence/trackingContract.ts +0 -17
- package/integrations/gate/blockingCause.ts +0 -40
- package/integrations/gate/governanceActionCatalog.ts +0 -296
- package/integrations/gate/runPlatformGateConfig.ts +0 -55
- package/integrations/gate/runPlatformGateDefaults.ts +0 -19
- package/integrations/lifecycle/bootstrapManifest.ts +0 -248
- package/integrations/lifecycle/cliGovernanceConsole.ts +0 -69
- package/integrations/lifecycle/governanceNextAction.ts +0 -181
- package/integrations/lifecycle/governanceObservationSnapshot.ts +0 -376
- package/integrations/lifecycle/trackingState.ts +0 -403
- package/integrations/mcp/alignedPlatformGate.ts +0 -248
- package/integrations/mcp/readMcpPrePushStdin.ts +0 -7
- package/scripts/build-ruralgo-s1-evidence-pack.ts +0 -85
- 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
|