pumuki 6.3.222 → 6.3.224

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.
@@ -35,6 +35,7 @@ import {
35
35
  hasSwiftMixedTestingFrameworksUsage,
36
36
  hasSwiftLegacyXCTestImportUsage,
37
37
  hasSwiftModernizableXCTestSuiteUsage,
38
+ hasSwiftNonLazyScrollForEachUsage,
38
39
  hasSwiftAssumeIsolatedUsage,
39
40
  hasSwiftCoreDataLayerLeakUsage,
40
41
  hasSwiftSwiftDataLayerLeakUsage,
@@ -63,6 +64,7 @@ import {
63
64
  hasSwiftJSONSerializationUsage,
64
65
  hasSwiftExplicitColorStaticMemberUsage,
65
66
  hasSwiftClosureBasedViewBuilderContentUsage,
67
+ hasSwiftRedundantReactiveStateAssignmentUsage,
66
68
  hasSwiftInlineForEachTransformUsage,
67
69
  hasSwiftStringFormatUsage,
68
70
  hasSwiftStrongDelegateReferenceUsage,
@@ -122,6 +124,44 @@ test('hasSwiftAnyViewUsage ignora comentarios, strings y coincidencias parciales
122
124
  assert.equal(hasSwiftAnyViewUsage(source), false);
123
125
  });
124
126
 
127
+ test('hasSwiftNonLazyScrollForEachUsage detecta ScrollView con stack no lazy y preserva LazyVStack', () => {
128
+ const source = `
129
+ struct FeedView: View {
130
+ let items: [Item]
131
+
132
+ var body: some View {
133
+ ScrollView {
134
+ VStack(spacing: 12) {
135
+ ForEach(items) { item in
136
+ FeedRow(item: item)
137
+ }
138
+ }
139
+ }
140
+ }
141
+ }
142
+ `;
143
+ const safe = `
144
+ struct FeedView: View {
145
+ let items: [Item]
146
+
147
+ var body: some View {
148
+ ScrollView {
149
+ LazyVStack(spacing: 12) {
150
+ ForEach(items) { item in
151
+ FeedRow(item: item)
152
+ }
153
+ }
154
+ }
155
+ let sample = "ScrollView { VStack { ForEach(items) } }"
156
+ // ScrollView { VStack { ForEach(items) } }
157
+ }
158
+ }
159
+ `;
160
+
161
+ assert.equal(hasSwiftNonLazyScrollForEachUsage(source), true);
162
+ assert.equal(hasSwiftNonLazyScrollForEachUsage(safe), false);
163
+ });
164
+
125
165
  test('hasSwiftForceTryUsage detecta try! y descarta try?', () => {
126
166
  const positive = `
127
167
  func load() {
@@ -737,6 +777,9 @@ GeometryReader { proxy in
737
777
  Text("Headline").fontWeight(.bold)
738
778
  Text("State").foregroundStyle(Color.green)
739
779
  let content: () -> Content
780
+ .onChange(of: query) { newValue in
781
+ query = newValue
782
+ }
740
783
  let filtered = items.filter { $0.title.contains(searchText) }
741
784
  ForEach(items.indices, id: \\.self) { index in
742
785
  Text(items[index].title)
@@ -785,6 +828,7 @@ MainActor.assumeIsolated { reload() }
785
828
  assert.equal(hasSwiftFontWeightBoldUsage(source), true);
786
829
  assert.equal(hasSwiftExplicitColorStaticMemberUsage(source), true);
787
830
  assert.equal(hasSwiftClosureBasedViewBuilderContentUsage(source), true);
831
+ assert.equal(hasSwiftRedundantReactiveStateAssignmentUsage(source), true);
788
832
  assert.equal(hasSwiftObservableObjectUsage(source), true);
789
833
  assert.equal(hasSwiftLegacySwiftUiObservableWrapperUsage(source), true);
790
834
  assert.equal(hasSwiftNavigationViewUsage(source), true);
@@ -826,6 +870,7 @@ let t = "MainActor.assumeIsolated { reload() }"
826
870
  let u = "ForEach(items.filter { $0.isVisible }) { item in }"
827
871
  let v = "Color.green"
828
872
  let w = "let content: () -> Content"
873
+ let x = ".onChange(of: query) { newValue in query = newValue }"
829
874
  `;
830
875
  assert.equal(hasSwiftPreconcurrencyUsage(source), false);
831
876
  assert.equal(hasSwiftNonisolatedUnsafeUsage(source), false);
@@ -837,6 +882,7 @@ let w = "let content: () -> Content"
837
882
  assert.equal(hasSwiftFontWeightBoldUsage(source), false);
838
883
  assert.equal(hasSwiftExplicitColorStaticMemberUsage(source), false);
839
884
  assert.equal(hasSwiftClosureBasedViewBuilderContentUsage(source), false);
885
+ assert.equal(hasSwiftRedundantReactiveStateAssignmentUsage(source), false);
840
886
  assert.equal(hasSwiftTaskDetachedUsage(source), false);
841
887
  assert.equal(hasSwiftNavigationViewUsage(source), false);
842
888
  assert.equal(hasSwiftForegroundColorUsage(source), false);
@@ -891,6 +937,7 @@ ScrollView {
891
937
  assert.equal(hasSwiftFontWeightBoldUsage(source), false);
892
938
  assert.equal(hasSwiftExplicitColorStaticMemberUsage(source), false);
893
939
  assert.equal(hasSwiftClosureBasedViewBuilderContentUsage(source), false);
940
+ assert.equal(hasSwiftRedundantReactiveStateAssignmentUsage(source), false);
894
941
  assert.equal(hasSwiftForegroundColorUsage(source), false);
895
942
  assert.equal(hasSwiftCornerRadiusUsage(source), false);
896
943
  assert.equal(hasSwiftTabItemUsage(source), false);
@@ -946,6 +993,47 @@ let ignored = "let content: () -> Content"
946
993
  assert.equal(hasSwiftClosureBasedViewBuilderContentUsage(safe), false);
947
994
  });
948
995
 
996
+ test('hasSwiftRedundantReactiveStateAssignmentUsage detecta asignaciones reactivas redundantes y preserva guard de cambio', () => {
997
+ const source = `
998
+ struct SearchView: View {
999
+ @State private var query = ""
1000
+
1001
+ var body: some View {
1002
+ TextField("Search", text: $query)
1003
+ .onChange(of: query) { newValue in
1004
+ query = newValue
1005
+ }
1006
+ .onReceive(model.$value) { value in
1007
+ self.query = value
1008
+ }
1009
+ }
1010
+ }
1011
+ `;
1012
+ const safe = `
1013
+ struct SearchView: View {
1014
+ @State private var query = ""
1015
+
1016
+ var body: some View {
1017
+ TextField("Search", text: $query)
1018
+ .onChange(of: query) { newValue in
1019
+ if query != newValue {
1020
+ query = newValue
1021
+ }
1022
+ }
1023
+ .onReceive(model.$value) { value in
1024
+ guard self.query != value else { return }
1025
+ self.query = value
1026
+ }
1027
+ }
1028
+ }
1029
+ let ignored = ".onChange(of: query) { newValue in query = newValue }"
1030
+ // .onReceive(model.$value) { value in query = value }
1031
+ `;
1032
+
1033
+ assert.equal(hasSwiftRedundantReactiveStateAssignmentUsage(source), true);
1034
+ assert.equal(hasSwiftRedundantReactiveStateAssignmentUsage(safe), false);
1035
+ });
1036
+
949
1037
  test('hasSwiftLegacyXCTestImportUsage detecta XCTest unitario y excluye UI/performance', () => {
950
1038
  const unitTest = `
951
1039
  import XCTest
@@ -380,6 +380,14 @@ export const hasSwiftAnyViewUsage = (source: string): boolean => {
380
380
  });
381
381
  };
382
382
 
383
+ export const hasSwiftNonLazyScrollForEachUsage = (source: string): boolean => {
384
+ const swiftSource = sanitizeSwiftSourceForMultilineRegex(source);
385
+ const nonLazyScrollableCollectionPattern =
386
+ /\bScrollView\s*(?:\([^)]*\))?\s*\{[\s\S]{0,2000}\b(?:VStack|HStack)\s*(?:\([^)]*\))?\s*\{[\s\S]{0,1200}\bForEach\s*\(/;
387
+
388
+ return nonLazyScrollableCollectionPattern.test(swiftSource);
389
+ };
390
+
383
391
  export const hasSwiftDispatchQueueUsage = (source: string): boolean => {
384
392
  return scanCodeLikeSource(source, ({ source: swiftSource, index, current }) => {
385
393
  if (current !== 'D' || !hasIdentifierAt(swiftSource, index, 'DispatchQueue')) {
@@ -877,6 +885,28 @@ export const hasSwiftClosureBasedViewBuilderContentUsage = (source: string): boo
877
885
  );
878
886
  };
879
887
 
888
+ export const hasSwiftRedundantReactiveStateAssignmentUsage = (source: string): boolean => {
889
+ const sanitized = sanitizeSwiftSourceForMultilineRegex(source);
890
+ const reactiveAssignmentPattern =
891
+ /\.(?:onChange|onReceive)\s*\([^)]*\)\s*\{[\s\S]{0,500}?\b(?:self\s*\.\s*)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:newValue|value|output|receivedValue)\b/g;
892
+
893
+ for (const match of sanitized.matchAll(reactiveAssignmentPattern)) {
894
+ const target = match[1];
895
+ const segment = match[0] ?? '';
896
+ if (!target) {
897
+ continue;
898
+ }
899
+ const guardedAssignmentPattern = new RegExp(
900
+ `\\b(?:if|guard)\\s+(?:self\\s*\\.\\s*)?${target}\\s*!=\\s*(?:newValue|value|output|receivedValue)\\b`
901
+ );
902
+ if (!guardedAssignmentPattern.test(segment)) {
903
+ return true;
904
+ }
905
+ }
906
+
907
+ return false;
908
+ };
909
+
880
910
  export const hasSwiftLegacySwiftUiObservableWrapperUsage = (source: string): boolean => {
881
911
  return hasSwiftSanitizedRegexMatch(source, /@\s*(?:StateObject|ObservedObject)\b/);
882
912
  };
@@ -684,6 +684,8 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
684
684
  { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftFontWeightBoldUsage, ruleId: 'heuristics.ios.font-weight-bold.ast', code: 'HEURISTICS_IOS_FONT_WEIGHT_BOLD_AST', message: 'AST heuristic detected fontWeight(.bold) usage where bold() may be preferred.' },
685
685
  { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftExplicitColorStaticMemberUsage, ruleId: 'heuristics.ios.swiftui.explicit-color-static-member.ast', code: 'HEURISTICS_IOS_SWIFTUI_EXPLICIT_COLOR_STATIC_MEMBER_AST', message: 'AST heuristic detected Color.* static member usage where SwiftUI static member lookup may be preferred.' },
686
686
  { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftClosureBasedViewBuilderContentUsage, ruleId: 'heuristics.ios.swiftui.closure-based-viewbuilder-content.ast', code: 'HEURISTICS_IOS_SWIFTUI_CLOSURE_BASED_VIEWBUILDER_CONTENT_AST', message: 'AST heuristic detected closure-based content storage; @ViewBuilder let content: Content remains the preferred SwiftUI container baseline.' },
687
+ { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftRedundantReactiveStateAssignmentUsage, ruleId: 'heuristics.ios.swiftui.redundant-reactive-state-assignment.ast', code: 'HEURISTICS_IOS_SWIFTUI_REDUNDANT_REACTIVE_STATE_ASSIGNMENT_AST', message: 'AST heuristic detected reactive state assignment without a value-change guard; check for value changes before assigning state in hot paths.' },
688
+ { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftNonLazyScrollForEachUsage, ruleId: 'heuristics.ios.swiftui.non-lazy-scroll-foreach.ast', code: 'HEURISTICS_IOS_SWIFTUI_NON_LAZY_SCROLL_FOREACH_AST', message: 'AST heuristic detected ScrollView with a non-lazy stack feeding ForEach; LazyVStack/LazyHStack remain the preferred baseline for large scrollable collections.' },
687
689
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftNavigationViewUsage, ruleId: 'heuristics.ios.navigation-view.ast', code: 'HEURISTICS_IOS_NAVIGATION_VIEW_AST', message: 'AST heuristic detected NavigationView usage.' },
688
690
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftForegroundColorUsage, ruleId: 'heuristics.ios.foreground-color.ast', code: 'HEURISTICS_IOS_FOREGROUND_COLOR_AST', message: 'AST heuristic detected foregroundColor usage.' },
689
691
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftCornerRadiusUsage, ruleId: 'heuristics.ios.corner-radius.ast', code: 'HEURISTICS_IOS_CORNER_RADIUS_AST', message: 'AST heuristic detected cornerRadius usage.' },
@@ -893,6 +893,43 @@ export const iosRules: RuleSet = [
893
893
  code: 'HEURISTICS_IOS_SWIFTUI_CLOSURE_BASED_VIEWBUILDER_CONTENT_AST',
894
894
  },
895
895
  },
896
+ {
897
+ id: 'heuristics.ios.swiftui.redundant-reactive-state-assignment.ast',
898
+ description: 'Detects onChange/onReceive state assignments without a value-change guard.',
899
+ severity: 'WARN',
900
+ platform: 'ios',
901
+ locked: true,
902
+ when: {
903
+ kind: 'Heuristic',
904
+ where: {
905
+ ruleId: 'heuristics.ios.swiftui.redundant-reactive-state-assignment.ast',
906
+ },
907
+ },
908
+ then: {
909
+ kind: 'Finding',
910
+ message: 'AST heuristic detected reactive state assignment without a value-change guard; check for value changes before assigning state in hot paths.',
911
+ code: 'HEURISTICS_IOS_SWIFTUI_REDUNDANT_REACTIVE_STATE_ASSIGNMENT_AST',
912
+ },
913
+ },
914
+ {
915
+ id: 'heuristics.ios.swiftui.non-lazy-scroll-foreach.ast',
916
+ description: 'Detects ScrollView content backed by non-lazy stacks and ForEach.',
917
+ severity: 'WARN',
918
+ platform: 'ios',
919
+ locked: true,
920
+ when: {
921
+ kind: 'Heuristic',
922
+ where: {
923
+ ruleId: 'heuristics.ios.swiftui.non-lazy-scroll-foreach.ast',
924
+ },
925
+ },
926
+ then: {
927
+ kind: 'Finding',
928
+ message:
929
+ 'AST heuristic detected ScrollView with a non-lazy stack feeding ForEach; LazyVStack/LazyHStack remain the preferred baseline for large scrollable collections.',
930
+ code: 'HEURISTICS_IOS_SWIFTUI_NON_LAZY_SCROLL_FOREACH_AST',
931
+ },
932
+ },
896
933
  {
897
934
  id: 'heuristics.ios.navigation-view.ast',
898
935
  description: 'Detects NavigationView usage in iOS production code.',
@@ -250,6 +250,14 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
250
250
  heuristicDetector('ios.swiftui.closure-based-viewbuilder-content', [
251
251
  'heuristics.ios.swiftui.closure-based-viewbuilder-content.ast',
252
252
  ]),
253
+ 'skills.ios.guideline.ios-swiftui-expert.avoid-redundant-state-updates-in-onreceive-onchange-scroll-handlers':
254
+ heuristicDetector('ios.swiftui.redundant-reactive-state-assignment', [
255
+ 'heuristics.ios.swiftui.redundant-reactive-state-assignment.ast',
256
+ ]),
257
+ 'skills.ios.guideline.ios-swiftui-expert.use-lazyvstack-lazyhstack-for-large-lists':
258
+ heuristicDetector('ios.swiftui.non-lazy-scroll-foreach', [
259
+ 'heuristics.ios.swiftui.non-lazy-scroll-foreach.ast',
260
+ ]),
253
261
  'skills.ios.no-scrollview-shows-indicators': heuristicDetector(
254
262
  'ios.scrollview-shows-indicators',
255
263
  ['heuristics.ios.scrollview-shows-indicators.ast']
@@ -353,6 +353,20 @@ const normalizeKnownRuleTarget = (
353
353
  ) {
354
354
  return 'skills.ios.guideline.ios-swiftui-expert.prefer-viewbuilder-let-content-content-over-closure-based-content-prop';
355
355
  }
356
+ if (
357
+ includes('redundant state updates') ||
358
+ (includes('onreceive') && includes('onchange') && includes('state updates')) ||
359
+ (includes('check for value changes') && includes('assigning state'))
360
+ ) {
361
+ return 'skills.ios.guideline.ios-swiftui-expert.avoid-redundant-state-updates-in-onreceive-onchange-scroll-handlers';
362
+ }
363
+ if (
364
+ (includes('lazyvstack') && includes('lazyhstack') && includes('large lists')) ||
365
+ (includes('lazyvstack') && includes('foreach') && includes('scrollview')) ||
366
+ (includes('lazyhstack') && includes('foreach') && includes('scrollview'))
367
+ ) {
368
+ return 'skills.ios.guideline.ios-swiftui-expert.use-lazyvstack-lazyhstack-for-large-lists';
369
+ }
356
370
  if (
357
371
  includes('scrollindicators hidden') ||
358
372
  includes('scroll indicators hidden') ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pumuki",
3
- "version": "6.3.222",
3
+ "version": "6.3.224",
4
4
  "description": "Enterprise-grade AST Intelligence System with multi-platform support (iOS, Android, Backend, Frontend) and Feature-First + DDD + Clean Architecture enforcement. Includes dynamic violations API for intelligent querying.",
5
5
  "main": "index.js",
6
6
  "bin": {
package/skills.lock.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": "1.0",
3
3
  "compilerVersion": "1.0.0",
4
- "generatedAt": "2026-05-13T16:04:37.359Z",
4
+ "generatedAt": "2026-05-13T16:15:52.668Z",
5
5
  "bundles": [
6
6
  {
7
7
  "name": "android-guidelines",
@@ -8632,7 +8632,7 @@
8632
8632
  "name": "ios-swiftui-expert-guidelines",
8633
8633
  "version": "1.0.0",
8634
8634
  "source": "file:vendor/skills/swiftui-expert-skill/SKILL.md",
8635
- "hash": "938525eec7fea70ecba6c27711800036b964e4b668f80b177cc918cb268e08c5",
8635
+ "hash": "2acbd66e86e9e809f0c4bdb5d215618096f5773e359085e3a4dc707b83c1ebb1",
8636
8636
  "rules": [
8637
8637
  {
8638
8638
  "id": "skills.ios.guideline.ios-swiftui-expert.always-mark-state-and-stateobject-as-private-makes-dependencies-clear",
@@ -8667,7 +8667,7 @@
8667
8667
  "sourcePath": "vendor/skills/swiftui-expert-skill/SKILL.md",
8668
8668
  "confidence": "MEDIUM",
8669
8669
  "locked": true,
8670
- "evaluationMode": "DECLARATIVE",
8670
+ "evaluationMode": "AUTO",
8671
8671
  "origin": "core"
8672
8672
  },
8673
8673
  {
@@ -8787,7 +8787,7 @@
8787
8787
  "sourcePath": "vendor/skills/swiftui-expert-skill/SKILL.md",
8788
8788
  "confidence": "MEDIUM",
8789
8789
  "locked": true,
8790
- "evaluationMode": "DECLARATIVE",
8790
+ "evaluationMode": "AUTO",
8791
8791
  "origin": "core"
8792
8792
  },
8793
8793
  {