pumuki 6.3.222 → 6.3.223

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.
@@ -63,6 +63,7 @@ import {
63
63
  hasSwiftJSONSerializationUsage,
64
64
  hasSwiftExplicitColorStaticMemberUsage,
65
65
  hasSwiftClosureBasedViewBuilderContentUsage,
66
+ hasSwiftRedundantReactiveStateAssignmentUsage,
66
67
  hasSwiftInlineForEachTransformUsage,
67
68
  hasSwiftStringFormatUsage,
68
69
  hasSwiftStrongDelegateReferenceUsage,
@@ -737,6 +738,9 @@ GeometryReader { proxy in
737
738
  Text("Headline").fontWeight(.bold)
738
739
  Text("State").foregroundStyle(Color.green)
739
740
  let content: () -> Content
741
+ .onChange(of: query) { newValue in
742
+ query = newValue
743
+ }
740
744
  let filtered = items.filter { $0.title.contains(searchText) }
741
745
  ForEach(items.indices, id: \\.self) { index in
742
746
  Text(items[index].title)
@@ -785,6 +789,7 @@ MainActor.assumeIsolated { reload() }
785
789
  assert.equal(hasSwiftFontWeightBoldUsage(source), true);
786
790
  assert.equal(hasSwiftExplicitColorStaticMemberUsage(source), true);
787
791
  assert.equal(hasSwiftClosureBasedViewBuilderContentUsage(source), true);
792
+ assert.equal(hasSwiftRedundantReactiveStateAssignmentUsage(source), true);
788
793
  assert.equal(hasSwiftObservableObjectUsage(source), true);
789
794
  assert.equal(hasSwiftLegacySwiftUiObservableWrapperUsage(source), true);
790
795
  assert.equal(hasSwiftNavigationViewUsage(source), true);
@@ -826,6 +831,7 @@ let t = "MainActor.assumeIsolated { reload() }"
826
831
  let u = "ForEach(items.filter { $0.isVisible }) { item in }"
827
832
  let v = "Color.green"
828
833
  let w = "let content: () -> Content"
834
+ let x = ".onChange(of: query) { newValue in query = newValue }"
829
835
  `;
830
836
  assert.equal(hasSwiftPreconcurrencyUsage(source), false);
831
837
  assert.equal(hasSwiftNonisolatedUnsafeUsage(source), false);
@@ -837,6 +843,7 @@ let w = "let content: () -> Content"
837
843
  assert.equal(hasSwiftFontWeightBoldUsage(source), false);
838
844
  assert.equal(hasSwiftExplicitColorStaticMemberUsage(source), false);
839
845
  assert.equal(hasSwiftClosureBasedViewBuilderContentUsage(source), false);
846
+ assert.equal(hasSwiftRedundantReactiveStateAssignmentUsage(source), false);
840
847
  assert.equal(hasSwiftTaskDetachedUsage(source), false);
841
848
  assert.equal(hasSwiftNavigationViewUsage(source), false);
842
849
  assert.equal(hasSwiftForegroundColorUsage(source), false);
@@ -891,6 +898,7 @@ ScrollView {
891
898
  assert.equal(hasSwiftFontWeightBoldUsage(source), false);
892
899
  assert.equal(hasSwiftExplicitColorStaticMemberUsage(source), false);
893
900
  assert.equal(hasSwiftClosureBasedViewBuilderContentUsage(source), false);
901
+ assert.equal(hasSwiftRedundantReactiveStateAssignmentUsage(source), false);
894
902
  assert.equal(hasSwiftForegroundColorUsage(source), false);
895
903
  assert.equal(hasSwiftCornerRadiusUsage(source), false);
896
904
  assert.equal(hasSwiftTabItemUsage(source), false);
@@ -946,6 +954,47 @@ let ignored = "let content: () -> Content"
946
954
  assert.equal(hasSwiftClosureBasedViewBuilderContentUsage(safe), false);
947
955
  });
948
956
 
957
+ test('hasSwiftRedundantReactiveStateAssignmentUsage detecta asignaciones reactivas redundantes y preserva guard de cambio', () => {
958
+ const source = `
959
+ struct SearchView: View {
960
+ @State private var query = ""
961
+
962
+ var body: some View {
963
+ TextField("Search", text: $query)
964
+ .onChange(of: query) { newValue in
965
+ query = newValue
966
+ }
967
+ .onReceive(model.$value) { value in
968
+ self.query = value
969
+ }
970
+ }
971
+ }
972
+ `;
973
+ const safe = `
974
+ struct SearchView: View {
975
+ @State private var query = ""
976
+
977
+ var body: some View {
978
+ TextField("Search", text: $query)
979
+ .onChange(of: query) { newValue in
980
+ if query != newValue {
981
+ query = newValue
982
+ }
983
+ }
984
+ .onReceive(model.$value) { value in
985
+ guard self.query != value else { return }
986
+ self.query = value
987
+ }
988
+ }
989
+ }
990
+ let ignored = ".onChange(of: query) { newValue in query = newValue }"
991
+ // .onReceive(model.$value) { value in query = value }
992
+ `;
993
+
994
+ assert.equal(hasSwiftRedundantReactiveStateAssignmentUsage(source), true);
995
+ assert.equal(hasSwiftRedundantReactiveStateAssignmentUsage(safe), false);
996
+ });
997
+
949
998
  test('hasSwiftLegacyXCTestImportUsage detecta XCTest unitario y excluye UI/performance', () => {
950
999
  const unitTest = `
951
1000
  import XCTest
@@ -877,6 +877,28 @@ export const hasSwiftClosureBasedViewBuilderContentUsage = (source: string): boo
877
877
  );
878
878
  };
879
879
 
880
+ export const hasSwiftRedundantReactiveStateAssignmentUsage = (source: string): boolean => {
881
+ const sanitized = sanitizeSwiftSourceForMultilineRegex(source);
882
+ const reactiveAssignmentPattern =
883
+ /\.(?: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;
884
+
885
+ for (const match of sanitized.matchAll(reactiveAssignmentPattern)) {
886
+ const target = match[1];
887
+ const segment = match[0] ?? '';
888
+ if (!target) {
889
+ continue;
890
+ }
891
+ const guardedAssignmentPattern = new RegExp(
892
+ `\\b(?:if|guard)\\s+(?:self\\s*\\.\\s*)?${target}\\s*!=\\s*(?:newValue|value|output|receivedValue)\\b`
893
+ );
894
+ if (!guardedAssignmentPattern.test(segment)) {
895
+ return true;
896
+ }
897
+ }
898
+
899
+ return false;
900
+ };
901
+
880
902
  export const hasSwiftLegacySwiftUiObservableWrapperUsage = (source: string): boolean => {
881
903
  return hasSwiftSanitizedRegexMatch(source, /@\s*(?:StateObject|ObservedObject)\b/);
882
904
  };
@@ -684,6 +684,7 @@ 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.' },
687
688
  { 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
689
  { 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
690
  { 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,24 @@ 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
+ },
896
914
  {
897
915
  id: 'heuristics.ios.navigation-view.ast',
898
916
  description: 'Detects NavigationView usage in iOS production code.',
@@ -250,6 +250,10 @@ 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
+ ]),
253
257
  'skills.ios.no-scrollview-shows-indicators': heuristicDetector(
254
258
  'ios.scrollview-shows-indicators',
255
259
  ['heuristics.ios.scrollview-shows-indicators.ast']
@@ -353,6 +353,13 @@ 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
+ }
356
363
  if (
357
364
  includes('scrollindicators hidden') ||
358
365
  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.223",
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:08:30.839Z",
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": "6570119e6399677bda8b010b801f6397859f7a066eeecf1a33fcfe9ca97348fe",
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
  {