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.
Files changed (33) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/VERSION +1 -1
  3. package/core/facts/detectors/text/android.test.ts +538 -0
  4. package/core/facts/detectors/text/android.ts +436 -0
  5. package/core/facts/detectors/text/ios.test.ts +328 -1
  6. package/core/facts/detectors/text/ios.ts +241 -0
  7. package/core/facts/detectors/typescript/index.test.ts +393 -0
  8. package/core/facts/detectors/typescript/index.ts +316 -0
  9. package/core/facts/extractHeuristicFacts.ts +70 -1
  10. package/core/rules/presets/heuristics/android.test.ts +91 -1
  11. package/core/rules/presets/heuristics/android.ts +360 -0
  12. package/core/rules/presets/heuristics/ios.test.ts +54 -1
  13. package/core/rules/presets/heuristics/ios.ts +243 -2
  14. package/core/rules/presets/heuristics/typescript.test.ts +50 -2
  15. package/core/rules/presets/heuristics/typescript.ts +162 -0
  16. package/docs/operations/RELEASE_NOTES.md +8 -0
  17. package/integrations/config/skillsDetectorRegistry.ts +501 -0
  18. package/integrations/config/skillsRuleClassification.ts +127 -3
  19. package/integrations/git/runPlatformGate.ts +4 -1
  20. package/integrations/lifecycle/preWriteAutomation.ts +5 -4
  21. package/integrations/lifecycle/preWriteLease.ts +41 -4
  22. package/package.json +1 -1
  23. package/scripts/classify-skills-rules.ts +2 -2
  24. package/scripts/framework-menu-consumer-actions-lib.ts +9 -9
  25. package/scripts/framework-menu-consumer-runtime-actions.ts +53 -117
  26. package/scripts/framework-menu-consumer-runtime-audit.ts +66 -0
  27. package/scripts/framework-menu-consumer-runtime-menu.ts +4 -4
  28. package/scripts/framework-menu-gate-lib.ts +86 -1
  29. package/scripts/framework-menu-layout-data.ts +3 -3
  30. package/scripts/framework-menu-legacy-audit-render-sections.ts +6 -0
  31. package/scripts/framework-menu.ts +10 -6
  32. package/scripts/package-install-smoke-consumer-npm-lib.ts +10 -4
  33. 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;