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.
- package/core/facts/detectors/text/ios.test.ts +49 -0
- package/core/facts/detectors/text/ios.ts +22 -0
- package/core/facts/extractHeuristicFacts.ts +1 -0
- package/core/rules/presets/heuristics/ios.ts +18 -0
- package/integrations/config/skillsDetectorRegistry.ts +4 -0
- package/integrations/config/skillsMarkdownRules.ts +7 -0
- package/package.json +1 -1
- package/skills.lock.json +3 -3
|
@@ -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.
|
|
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:
|
|
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": "
|
|
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": "
|
|
8670
|
+
"evaluationMode": "AUTO",
|
|
8671
8671
|
"origin": "core"
|
|
8672
8672
|
},
|
|
8673
8673
|
{
|