pumuki 6.3.270 → 6.3.272
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/VERSION +1 -1
- package/core/facts/detectors/text/android.test.ts +538 -0
- package/core/facts/detectors/text/android.ts +436 -0
- package/core/facts/detectors/text/ios.test.ts +328 -1
- package/core/facts/detectors/text/ios.ts +241 -0
- package/core/facts/detectors/typescript/index.test.ts +393 -0
- package/core/facts/detectors/typescript/index.ts +316 -0
- package/core/facts/extractHeuristicFacts.ts +70 -1
- package/core/rules/presets/heuristics/android.test.ts +91 -1
- package/core/rules/presets/heuristics/android.ts +360 -0
- package/core/rules/presets/heuristics/ios.test.ts +54 -1
- package/core/rules/presets/heuristics/ios.ts +243 -2
- package/core/rules/presets/heuristics/typescript.test.ts +50 -2
- package/core/rules/presets/heuristics/typescript.ts +162 -0
- package/docs/operations/RELEASE_NOTES.md +8 -0
- package/integrations/config/skillsDetectorRegistry.ts +501 -0
- package/integrations/config/skillsRuleClassification.ts +127 -3
- package/integrations/git/runPlatformGate.ts +4 -1
- package/integrations/lifecycle/preWriteAutomation.ts +5 -4
- package/integrations/lifecycle/preWriteLease.ts +41 -4
- package/package.json +1 -1
- package/scripts/classify-skills-rules.ts +2 -2
- package/scripts/framework-menu-consumer-actions-lib.ts +9 -9
- package/scripts/framework-menu-consumer-runtime-actions.ts +53 -117
- package/scripts/framework-menu-consumer-runtime-audit.ts +66 -0
- package/scripts/framework-menu-consumer-runtime-menu.ts +4 -4
- package/scripts/framework-menu-gate-lib.ts +86 -1
- package/scripts/framework-menu-layout-data.ts +3 -3
- package/scripts/framework-menu-legacy-audit-render-sections.ts +6 -0
- package/scripts/framework-menu.ts +10 -6
- package/scripts/package-install-smoke-consumer-npm-lib.ts +10 -4
- package/scripts/package-install-smoke-lifecycle-lib.ts +19 -0
|
@@ -134,6 +134,38 @@ const countTokenOccurrences = (line: string, token: string): number => {
|
|
|
134
134
|
return line.split(token).length - 1;
|
|
135
135
|
};
|
|
136
136
|
|
|
137
|
+
const extractBalancedCallSegment = (source: string, openParenIndex: number): string | undefined => {
|
|
138
|
+
let depth = 0;
|
|
139
|
+
for (let index = openParenIndex; index < source.length; index += 1) {
|
|
140
|
+
const character = source[index];
|
|
141
|
+
if (character === '(') {
|
|
142
|
+
depth += 1;
|
|
143
|
+
} else if (character === ')') {
|
|
144
|
+
depth -= 1;
|
|
145
|
+
if (depth === 0) {
|
|
146
|
+
return source.slice(openParenIndex, index + 1);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return undefined;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const extractBalancedBlockSegment = (source: string, openBraceIndex: number): string | undefined => {
|
|
154
|
+
let depth = 0;
|
|
155
|
+
for (let index = openBraceIndex; index < source.length; index += 1) {
|
|
156
|
+
const character = source[index];
|
|
157
|
+
if (character === '{') {
|
|
158
|
+
depth += 1;
|
|
159
|
+
} else if (character === '}') {
|
|
160
|
+
depth -= 1;
|
|
161
|
+
if (depth === 0) {
|
|
162
|
+
return source.slice(openBraceIndex, index + 1);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return undefined;
|
|
167
|
+
};
|
|
168
|
+
|
|
137
169
|
const normalizeKotlinWhenBranchName = (rawLabel: string): string => {
|
|
138
170
|
const normalized = rawLabel.split(',')[0]?.trim() ?? rawLabel.trim();
|
|
139
171
|
const withoutGuard = normalized.split(/\s+if\s+/)[0]?.trim() ?? normalized;
|
|
@@ -289,6 +321,28 @@ export const hasKotlinProductionMockUsage = (source: string): boolean => {
|
|
|
289
321
|
).length > 0;
|
|
290
322
|
};
|
|
291
323
|
|
|
324
|
+
export const collectKotlinHardcodedUiStringLines = (source: string): readonly number[] => {
|
|
325
|
+
return source
|
|
326
|
+
.split(/\r?\n/)
|
|
327
|
+
.map((line, index) => ({ line, number: index + 1 }))
|
|
328
|
+
.filter(({ line }) => {
|
|
329
|
+
const withoutComment = line.replace(/\/\/.*$/, '');
|
|
330
|
+
if (withoutComment.trimStart().startsWith('import ')) {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
return (
|
|
334
|
+
/\bText\s*\(\s*"[^"$\n]+"/.test(withoutComment) ||
|
|
335
|
+
/\bcontentDescription\s*=\s*"[^"$\n]+"/.test(withoutComment) ||
|
|
336
|
+
/\b(?:Button|OutlinedButton|TextButton|NavigationBarItem)\s*\([^)]*text\s*=\s*"[^"$\n]+"/.test(withoutComment)
|
|
337
|
+
);
|
|
338
|
+
})
|
|
339
|
+
.map(({ number }) => number);
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
export const hasKotlinHardcodedUiStringUsage = (source: string): boolean => {
|
|
343
|
+
return collectKotlinHardcodedUiStringLines(source).length > 0;
|
|
344
|
+
};
|
|
345
|
+
|
|
292
346
|
export const hasKotlinSupervisorScopeUsage = (source: string): boolean => {
|
|
293
347
|
return collectKotlinRegexLines(source, /\bsupervisorScope\s*(?:<[^>\n]+>\s*)?(?:\(|\{)/).length > 0;
|
|
294
348
|
};
|
|
@@ -343,6 +397,302 @@ export const hasKotlinRunBlockingUsage = (source: string): boolean => {
|
|
|
343
397
|
});
|
|
344
398
|
};
|
|
345
399
|
|
|
400
|
+
export const hasAndroidAsyncTaskUsage = (source: string): boolean => {
|
|
401
|
+
return scanCodeLikeSource(source, ({ source: androidSource, index, current }) => {
|
|
402
|
+
if (current !== 'A' && current !== 'a') {
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const isBareAsyncTask = hasIdentifierAt(androidSource, index, 'AsyncTask');
|
|
407
|
+
const isQualifiedAsyncTask =
|
|
408
|
+
hasIdentifierAt(androidSource, index, 'android') &&
|
|
409
|
+
androidSource.startsWith('.os.AsyncTask', index + 'android'.length);
|
|
410
|
+
|
|
411
|
+
if (!isBareAsyncTask && !isQualifiedAsyncTask) {
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const start = isBareAsyncTask
|
|
416
|
+
? index + 'AsyncTask'.length
|
|
417
|
+
: index + 'android.os.AsyncTask'.length;
|
|
418
|
+
const tail = androidSource.slice(start, start + 48);
|
|
419
|
+
return /^\s*(<[^>\n]+>\s*)?(?:\(|\{|:|\.|$)/.test(tail);
|
|
420
|
+
});
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const androidActivityBaseTypes = new Set([
|
|
424
|
+
'Activity',
|
|
425
|
+
'ComponentActivity',
|
|
426
|
+
'AppCompatActivity',
|
|
427
|
+
'FragmentActivity',
|
|
428
|
+
]);
|
|
429
|
+
|
|
430
|
+
export const hasKotlinGodActivityUsage = (source: string): boolean => {
|
|
431
|
+
const lines = source.split(/\r?\n/);
|
|
432
|
+
const declarations = parseKotlinTypeDeclarations(source);
|
|
433
|
+
|
|
434
|
+
return declarations.some((declaration) => {
|
|
435
|
+
const isActivity = declaration.conformances.some((conformance) =>
|
|
436
|
+
androidActivityBaseTypes.has(conformance)
|
|
437
|
+
);
|
|
438
|
+
if (!isActivity) {
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const body = lines
|
|
443
|
+
.slice(declaration.bodyStartLine - 1, declaration.bodyEndLine)
|
|
444
|
+
.map((line) => stripKotlinLineForSemanticScan(line))
|
|
445
|
+
.filter((line) => !line.trimStart().startsWith('import '))
|
|
446
|
+
.join('\n');
|
|
447
|
+
|
|
448
|
+
const hasUiEntrypoint = /\bsetContent\s*\{/.test(body) || /@Composable\b/.test(body);
|
|
449
|
+
const hasNetworkResponsibility =
|
|
450
|
+
/\b(?:OkHttpClient|Retrofit|HttpURLConnection|newCall|enqueue|execute)\b/.test(body);
|
|
451
|
+
const hasPersistenceResponsibility =
|
|
452
|
+
/\b(?:Room\.databaseBuilder|SharedPreferences|getSharedPreferences|DataStore|SQLiteDatabase)\b/.test(body);
|
|
453
|
+
const hasNavigationResponsibility =
|
|
454
|
+
/\b(?:NavController|rememberNavController|navigate\s*\(|NavHost\s*\()\b/.test(body);
|
|
455
|
+
const functionCount = (body.match(/\bfun\s+[A-Za-z_][A-Za-z0-9_]*\s*\(/g) ?? []).length;
|
|
456
|
+
const activityBodyLines = Math.max(0, declaration.bodyEndLine - declaration.bodyStartLine + 1);
|
|
457
|
+
|
|
458
|
+
return (
|
|
459
|
+
(hasUiEntrypoint &&
|
|
460
|
+
[hasNetworkResponsibility, hasPersistenceResponsibility, hasNavigationResponsibility].filter(Boolean)
|
|
461
|
+
.length >= 2) ||
|
|
462
|
+
(hasUiEntrypoint && functionCount >= 8) ||
|
|
463
|
+
activityBodyLines >= 140
|
|
464
|
+
);
|
|
465
|
+
});
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
export const hasKotlinNonLazyScrollableCollectionUsage = (source: string): boolean => {
|
|
469
|
+
const sanitizedSource = source
|
|
470
|
+
.split(/\r?\n/)
|
|
471
|
+
.map((line) => stripKotlinLineForSemanticScan(line))
|
|
472
|
+
.filter((line) => !line.trimStart().startsWith('import '))
|
|
473
|
+
.join('\n');
|
|
474
|
+
|
|
475
|
+
const scrollableColumnOrRowPattern =
|
|
476
|
+
/\b(?:Column|Row)\s*\([^)]*\bModifier\s*\.[^)]*(?:verticalScroll|horizontalScroll)\s*\([^)]*\)[\s\S]*?\{([\s\S]*?)\n\s*\}/g;
|
|
477
|
+
|
|
478
|
+
let match: RegExpExecArray | null;
|
|
479
|
+
while ((match = scrollableColumnOrRowPattern.exec(sanitizedSource)) !== null) {
|
|
480
|
+
const body = match[1] ?? '';
|
|
481
|
+
if (/\b(?:forEach|forEachIndexed|map|repeat)\s*(?:\(|\{)|\bfor\s*\([^)]+\bin\b[^)]*\)/.test(body)) {
|
|
482
|
+
return true;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return false;
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
export const hasKotlinUnstableLaunchedEffectKeyUsage = (source: string): boolean => {
|
|
490
|
+
return collectKotlinRegexLines(
|
|
491
|
+
source,
|
|
492
|
+
/\bLaunchedEffect\s*\(\s*(?:Unit|true|false|null)?\s*\)\s*\{/
|
|
493
|
+
).length > 0;
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
export const hasKotlinLaunchedEffectBusyLoopUsage = (source: string): boolean => {
|
|
497
|
+
const sanitizedSource = source
|
|
498
|
+
.split(/\r?\n/)
|
|
499
|
+
.map((line) => stripKotlinLineForSemanticScan(line))
|
|
500
|
+
.filter((line) => !line.trimStart().startsWith('import '))
|
|
501
|
+
.join('\n');
|
|
502
|
+
|
|
503
|
+
const launchedEffectPattern = /\bLaunchedEffect\s*\([^)]*\)\s*\{/g;
|
|
504
|
+
let match: RegExpExecArray | null;
|
|
505
|
+
while ((match = launchedEffectPattern.exec(sanitizedSource)) !== null) {
|
|
506
|
+
const bodyStartIndex = sanitizedSource.indexOf('{', match.index);
|
|
507
|
+
const segment = extractBalancedBlockSegment(sanitizedSource, bodyStartIndex);
|
|
508
|
+
if (
|
|
509
|
+
segment &&
|
|
510
|
+
/\bwhile\s*\(\s*(?:true|isActive)\s*\)\s*\{/.test(segment) &&
|
|
511
|
+
!/\bdelay\s*\(/.test(segment)
|
|
512
|
+
) {
|
|
513
|
+
return true;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return false;
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
export const hasKotlinProductionLoggingUsage = (source: string): boolean => {
|
|
521
|
+
return source.split(/\r?\n/).some((line) => {
|
|
522
|
+
const sanitized = stripKotlinLineForSemanticScan(line);
|
|
523
|
+
if (sanitized.trimStart().startsWith('import ') || sanitized.includes('BuildConfig.DEBUG')) {
|
|
524
|
+
return false;
|
|
525
|
+
}
|
|
526
|
+
return /\b(?:println\s*\(|System\s*\.\s*(?:out|err)\s*\.\s*println\s*\(|Log\s*\.\s*(?:v|d|i|w|e|wtf)\s*\(|Timber\s*\.\s*(?:v|d|i|w|e|wtf)\s*\()/.test(sanitized);
|
|
527
|
+
});
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
export const hasKotlinModifierBackgroundBeforePaddingUsage = (source: string): boolean => {
|
|
531
|
+
const sanitizedSource = source
|
|
532
|
+
.split(/\r?\n/)
|
|
533
|
+
.map((line) => stripKotlinLineForSemanticScan(line))
|
|
534
|
+
.filter((line) => !line.trimStart().startsWith('import '))
|
|
535
|
+
.join('\n');
|
|
536
|
+
|
|
537
|
+
return /\bModifier\b[\s\S]*?\.background\s*\([^)]*\)[\s\S]*?\.padding\s*\(/.test(sanitizedSource);
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
export const hasKotlinMissingContentDescriptionUsage = (source: string): boolean => {
|
|
541
|
+
const sanitizedSource = source
|
|
542
|
+
.split(/\r?\n/)
|
|
543
|
+
.map((line) => stripKotlinLineForSemanticScan(line))
|
|
544
|
+
.filter((line) => !line.trimStart().startsWith('import '))
|
|
545
|
+
.join('\n');
|
|
546
|
+
|
|
547
|
+
const callPattern = /\b(?:Image|Icon)\s*\(/g;
|
|
548
|
+
let match: RegExpExecArray | null;
|
|
549
|
+
while ((match = callPattern.exec(sanitizedSource)) !== null) {
|
|
550
|
+
const openParenIndex = sanitizedSource.indexOf('(', match.index);
|
|
551
|
+
const segment = extractBalancedCallSegment(sanitizedSource, openParenIndex);
|
|
552
|
+
if (segment && !/\bcontentDescription\s*=/.test(segment)) {
|
|
553
|
+
return true;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return false;
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
export const hasKotlinFontScaleDisabledUsage = (source: string): boolean => {
|
|
561
|
+
return collectKotlinRegexLines(
|
|
562
|
+
source,
|
|
563
|
+
/\bfontScale\s*=\s*(?:1(?:\.0)?f?|1(?:\.0)?)\b/
|
|
564
|
+
).length > 0;
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
export const hasKotlinIncompleteMaterialThemeUsage = (source: string): boolean => {
|
|
568
|
+
const sanitizedSource = source
|
|
569
|
+
.split(/\r?\n/)
|
|
570
|
+
.map((line) => stripKotlinLineForSemanticScan(line))
|
|
571
|
+
.filter((line) => !line.trimStart().startsWith('import '))
|
|
572
|
+
.join('\n');
|
|
573
|
+
|
|
574
|
+
const callPattern = /\bMaterialTheme\s*\(/g;
|
|
575
|
+
let match: RegExpExecArray | null;
|
|
576
|
+
while ((match = callPattern.exec(sanitizedSource)) !== null) {
|
|
577
|
+
const openParenIndex = sanitizedSource.indexOf('(', match.index);
|
|
578
|
+
const segment = extractBalancedCallSegment(sanitizedSource, openParenIndex);
|
|
579
|
+
if (
|
|
580
|
+
segment &&
|
|
581
|
+
(!/\bcolorScheme\s*=/.test(segment) ||
|
|
582
|
+
!/\btypography\s*=/.test(segment) ||
|
|
583
|
+
!/\bshapes\s*=/.test(segment))
|
|
584
|
+
) {
|
|
585
|
+
return true;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return false;
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
export const hasKotlinLegacyBottomNavigationUsage = (source: string): boolean => {
|
|
593
|
+
return collectKotlinRegexLines(
|
|
594
|
+
source,
|
|
595
|
+
/\b(?:BottomNavigation|BottomNavigationItem)\s*(?:\(|\{)/
|
|
596
|
+
).length > 0;
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
export const hasKotlinImperativeNavigationUsage = (source: string): boolean => {
|
|
600
|
+
return collectKotlinRegexLines(
|
|
601
|
+
source,
|
|
602
|
+
/\b(?:startActivity\s*\(\s*Intent\s*\(|supportFragmentManager\s*\.\s*beginTransaction\s*\(|childFragmentManager\s*\.\s*beginTransaction\s*\(|parentFragmentManager\s*\.\s*beginTransaction\s*\(|FragmentTransaction\b|FragmentManager\b)/
|
|
603
|
+
).length > 0;
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
export const hasKotlinComposableObjectCreationWithoutRememberUsage = (source: string): boolean => {
|
|
607
|
+
const lines = source.split(/\r?\n/);
|
|
608
|
+
let pendingComposableAnnotation = false;
|
|
609
|
+
let insideComposable = false;
|
|
610
|
+
let braceDepth = 0;
|
|
611
|
+
|
|
612
|
+
for (const rawLine of lines) {
|
|
613
|
+
const sanitized = stripKotlinLineForSemanticScan(rawLine);
|
|
614
|
+
if (sanitized.trimStart().startsWith('import ')) {
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (/@Composable\b/.test(sanitized)) {
|
|
619
|
+
pendingComposableAnnotation = true;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (pendingComposableAnnotation && /\bfun\s+[A-Za-z_][A-Za-z0-9_]*\s*\(/.test(sanitized)) {
|
|
623
|
+
insideComposable = true;
|
|
624
|
+
pendingComposableAnnotation = false;
|
|
625
|
+
braceDepth = 0;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (insideComposable) {
|
|
629
|
+
const createsObject =
|
|
630
|
+
/\b(?:Regex|SimpleDateFormat|DecimalFormat)\s*\(/.test(sanitized) ||
|
|
631
|
+
/\bDateTimeFormatter\s*\.\s*ofPattern\s*\(/.test(sanitized);
|
|
632
|
+
if (createsObject && !/\bremember\s*\{/.test(sanitized)) {
|
|
633
|
+
return true;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
braceDepth += countTokenOccurrences(sanitized, '{');
|
|
637
|
+
braceDepth -= countTokenOccurrences(sanitized, '}');
|
|
638
|
+
if (braceDepth <= 0 && sanitized.includes('}')) {
|
|
639
|
+
insideComposable = false;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
return false;
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
export const hasKotlinComposableStateCreationWithoutRememberUsage = (source: string): boolean => {
|
|
648
|
+
const lines = source.split(/\r?\n/);
|
|
649
|
+
let pendingComposableAnnotation = false;
|
|
650
|
+
let insideComposable = false;
|
|
651
|
+
let braceDepth = 0;
|
|
652
|
+
|
|
653
|
+
for (const rawLine of lines) {
|
|
654
|
+
const sanitized = stripKotlinLineForSemanticScan(rawLine);
|
|
655
|
+
if (sanitized.trimStart().startsWith('import ')) {
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (/@Composable\b/.test(sanitized)) {
|
|
660
|
+
pendingComposableAnnotation = true;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (pendingComposableAnnotation && /\bfun\s+[A-Za-z_][A-Za-z0-9_]*\s*\(/.test(sanitized)) {
|
|
664
|
+
insideComposable = true;
|
|
665
|
+
pendingComposableAnnotation = false;
|
|
666
|
+
braceDepth = 0;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (insideComposable) {
|
|
670
|
+
const createsState = /\b(?:mutableStateOf|derivedStateOf)\s*\(/.test(sanitized);
|
|
671
|
+
if (createsState && !/\bremember\s*\{/.test(sanitized)) {
|
|
672
|
+
return true;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
braceDepth += countTokenOccurrences(sanitized, '{');
|
|
676
|
+
braceDepth -= countTokenOccurrences(sanitized, '}');
|
|
677
|
+
if (braceDepth <= 0 && sanitized.includes('}')) {
|
|
678
|
+
insideComposable = false;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
return false;
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
export const hasKotlinForceUnwrapUsage = (source: string): boolean => {
|
|
687
|
+
return source.split(/\r?\n/).some((line) => {
|
|
688
|
+
const sanitized = stripKotlinLineForSemanticScan(line);
|
|
689
|
+
if (sanitized.trimStart().startsWith('import ')) {
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
return /!!(?!\s*(?:=|is))/.test(sanitized);
|
|
693
|
+
});
|
|
694
|
+
};
|
|
695
|
+
|
|
346
696
|
export const hasKotlinLiveDataStateExposureUsage = (source: string): boolean => {
|
|
347
697
|
return collectKotlinRegexLines(
|
|
348
698
|
source,
|
|
@@ -350,6 +700,92 @@ export const hasKotlinLiveDataStateExposureUsage = (source: string): boolean =>
|
|
|
350
700
|
).length > 0;
|
|
351
701
|
};
|
|
352
702
|
|
|
703
|
+
export const hasKotlinViewModelFlowWithoutStateInUsage = (source: string): boolean => {
|
|
704
|
+
const lines = source.split(/\r?\n/);
|
|
705
|
+
let insideViewModel = false;
|
|
706
|
+
let braceDepth = 0;
|
|
707
|
+
let exposesFlowState = false;
|
|
708
|
+
let usesStateIn = false;
|
|
709
|
+
|
|
710
|
+
for (const rawLine of lines) {
|
|
711
|
+
const sanitized = stripKotlinLineForSemanticScan(rawLine);
|
|
712
|
+
if (sanitized.trimStart().startsWith('import ')) {
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (!insideViewModel && /\bclass\s+\w*ViewModel\b/.test(sanitized)) {
|
|
717
|
+
insideViewModel = true;
|
|
718
|
+
braceDepth =
|
|
719
|
+
countTokenOccurrences(sanitized, '{') - countTokenOccurrences(sanitized, '}');
|
|
720
|
+
} else if (insideViewModel) {
|
|
721
|
+
braceDepth += countTokenOccurrences(sanitized, '{');
|
|
722
|
+
braceDepth -= countTokenOccurrences(sanitized, '}');
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (insideViewModel) {
|
|
726
|
+
if (/\b(?:val|var)\s+\w+\s*:\s*Flow\s*</.test(sanitized)) {
|
|
727
|
+
exposesFlowState = true;
|
|
728
|
+
}
|
|
729
|
+
if (/\.\s*stateIn\s*\(/.test(sanitized)) {
|
|
730
|
+
usesStateIn = true;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (insideViewModel && braceDepth <= 0 && sanitized.includes('}')) {
|
|
735
|
+
if (exposesFlowState && !usesStateIn) {
|
|
736
|
+
return true;
|
|
737
|
+
}
|
|
738
|
+
insideViewModel = false;
|
|
739
|
+
exposesFlowState = false;
|
|
740
|
+
usesStateIn = false;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
return insideViewModel && exposesFlowState && !usesStateIn;
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
export const hasKotlinSharedFlowUsedAsStateUsage = (source: string): boolean => {
|
|
748
|
+
const lines = source.split(/\r?\n/);
|
|
749
|
+
let insideViewModel = false;
|
|
750
|
+
let braceDepth = 0;
|
|
751
|
+
|
|
752
|
+
for (const rawLine of lines) {
|
|
753
|
+
const sanitized = stripKotlinLineForSemanticScan(rawLine);
|
|
754
|
+
if (sanitized.trimStart().startsWith('import ')) {
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
if (!insideViewModel && /\bclass\s+\w*ViewModel\b/.test(sanitized)) {
|
|
759
|
+
insideViewModel = true;
|
|
760
|
+
braceDepth =
|
|
761
|
+
countTokenOccurrences(sanitized, '{') - countTokenOccurrences(sanitized, '}');
|
|
762
|
+
} else if (insideViewModel) {
|
|
763
|
+
braceDepth += countTokenOccurrences(sanitized, '{');
|
|
764
|
+
braceDepth -= countTokenOccurrences(sanitized, '}');
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (
|
|
768
|
+
insideViewModel &&
|
|
769
|
+
/\b(?:val|var)\s+(?:_)?(?:uiState|state|screenState|viewState)\b[^:\n]*:\s*(?:Mutable)?SharedFlow\s*</.test(sanitized)
|
|
770
|
+
) {
|
|
771
|
+
return true;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
if (
|
|
775
|
+
insideViewModel &&
|
|
776
|
+
/\b(?:val|var)\s+\w+\s*:\s*(?:Mutable)?SharedFlow\s*<\s*[A-Za-z_][A-Za-z0-9_]*State\s*>/.test(sanitized)
|
|
777
|
+
) {
|
|
778
|
+
return true;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (insideViewModel && braceDepth <= 0 && sanitized.includes('}')) {
|
|
782
|
+
insideViewModel = false;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
return false;
|
|
787
|
+
};
|
|
788
|
+
|
|
353
789
|
export const hasKotlinManualCoroutineScopeInViewModelUsage = (source: string): boolean => {
|
|
354
790
|
const lines = source.split(/\r?\n/);
|
|
355
791
|
let insideViewModel = false;
|