pumuki 6.3.218 → 6.3.220
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 +67 -0
- package/core/facts/detectors/text/ios.ts +18 -0
- package/core/facts/extractHeuristicFacts.ts +2 -0
- package/core/rules/presets/heuristics/ios.ts +37 -0
- package/integrations/config/skillsDetectorRegistry.ts +8 -0
- package/integrations/config/skillsMarkdownRules.ts +14 -0
- package/package.json +1 -1
- package/skills.lock.json +4 -4
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
hasSwiftNSManagedObjectBoundaryUsage,
|
|
44
44
|
hasSwiftNSManagedObjectStateLeakUsage,
|
|
45
45
|
hasSwiftNavigationViewUsage,
|
|
46
|
+
hasSwiftNonPrivateStateOwnershipUsage,
|
|
46
47
|
hasSwiftNonIBOutletImplicitlyUnwrappedOptionalUsage,
|
|
47
48
|
hasSwiftObservableObjectUsage,
|
|
48
49
|
hasSwiftOnAppearTaskUsage,
|
|
@@ -60,6 +61,7 @@ import {
|
|
|
60
61
|
hasSwiftSensitiveUserDefaultsStorageUsage,
|
|
61
62
|
hasSwiftInsecureTransportUsage,
|
|
62
63
|
hasSwiftJSONSerializationUsage,
|
|
64
|
+
hasSwiftInlineForEachTransformUsage,
|
|
63
65
|
hasSwiftStringFormatUsage,
|
|
64
66
|
hasSwiftStrongDelegateReferenceUsage,
|
|
65
67
|
hasSwiftStrongSelfEscapingClosureUsage,
|
|
@@ -735,6 +737,9 @@ let filtered = items.filter { $0.title.contains(searchText) }
|
|
|
735
737
|
ForEach(items.indices, id: \\.self) { index in
|
|
736
738
|
Text(items[index].title)
|
|
737
739
|
}
|
|
740
|
+
ForEach(items.filter { $0.isVisible }) { item in
|
|
741
|
+
Text(item.title)
|
|
742
|
+
}
|
|
738
743
|
Text("Primary").foregroundColor(.blue)
|
|
739
744
|
Image("hero").cornerRadius(12)
|
|
740
745
|
TabView {
|
|
@@ -770,6 +775,7 @@ MainActor.assumeIsolated { reload() }
|
|
|
770
775
|
assert.equal(hasSwiftNonisolatedUnsafeUsage(source), true);
|
|
771
776
|
assert.equal(hasSwiftAssumeIsolatedUsage(source), true);
|
|
772
777
|
assert.equal(hasSwiftForEachIndicesUsage(source), true);
|
|
778
|
+
assert.equal(hasSwiftInlineForEachTransformUsage(source), true);
|
|
773
779
|
assert.equal(hasSwiftContainsUserFilterUsage(source), true);
|
|
774
780
|
assert.equal(hasSwiftGeometryReaderUsage(source), true);
|
|
775
781
|
assert.equal(hasSwiftFontWeightBoldUsage(source), true);
|
|
@@ -811,11 +817,13 @@ let q = ".fontWeight(.bold)"
|
|
|
811
817
|
let r = "@preconcurrency import LegacyFramework"
|
|
812
818
|
let s = "nonisolated(unsafe) static var sharedBridge: Model?"
|
|
813
819
|
let t = "MainActor.assumeIsolated { reload() }"
|
|
820
|
+
let u = "ForEach(items.filter { $0.isVisible }) { item in }"
|
|
814
821
|
`;
|
|
815
822
|
assert.equal(hasSwiftPreconcurrencyUsage(source), false);
|
|
816
823
|
assert.equal(hasSwiftNonisolatedUnsafeUsage(source), false);
|
|
817
824
|
assert.equal(hasSwiftAssumeIsolatedUsage(source), false);
|
|
818
825
|
assert.equal(hasSwiftForEachIndicesUsage(source), false);
|
|
826
|
+
assert.equal(hasSwiftInlineForEachTransformUsage(source), false);
|
|
819
827
|
assert.equal(hasSwiftContainsUserFilterUsage(source), false);
|
|
820
828
|
assert.equal(hasSwiftGeometryReaderUsage(source), false);
|
|
821
829
|
assert.equal(hasSwiftFontWeightBoldUsage(source), false);
|
|
@@ -928,6 +936,65 @@ struct ContentView: View {
|
|
|
928
936
|
assert.equal(hasSwiftLegacySwiftUiObservableWrapperUsage(modernWrapper), false);
|
|
929
937
|
});
|
|
930
938
|
|
|
939
|
+
test('hasSwiftNonPrivateStateOwnershipUsage detecta @State y @StateObject no privados', () => {
|
|
940
|
+
const source = `
|
|
941
|
+
struct DashboardView: View {
|
|
942
|
+
@State var query = ""
|
|
943
|
+
@StateObject var viewModel = DashboardViewModel()
|
|
944
|
+
}
|
|
945
|
+
`;
|
|
946
|
+
const safe = `
|
|
947
|
+
struct DashboardView: View {
|
|
948
|
+
@State private var query = ""
|
|
949
|
+
@StateObject private var viewModel = DashboardViewModel()
|
|
950
|
+
let text = "@State var query = \\"\\""
|
|
951
|
+
// @State var query = ""
|
|
952
|
+
}
|
|
953
|
+
`;
|
|
954
|
+
|
|
955
|
+
assert.equal(hasSwiftNonPrivateStateOwnershipUsage(source), true);
|
|
956
|
+
assert.equal(hasSwiftNonPrivateStateOwnershipUsage(safe), false);
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
test('hasSwiftInlineForEachTransformUsage detecta transformaciones inline y preserva colecciones precomputadas', () => {
|
|
960
|
+
const source = `
|
|
961
|
+
struct FeedView: View {
|
|
962
|
+
var body: some View {
|
|
963
|
+
List {
|
|
964
|
+
ForEach(items.filter { $0.isVisible }) { item in
|
|
965
|
+
Text(item.title)
|
|
966
|
+
}
|
|
967
|
+
ForEach(Array(sections.sorted(by: { $0.title < $1.title }))) { section in
|
|
968
|
+
Text(section.title)
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
`;
|
|
974
|
+
const safe = `
|
|
975
|
+
struct FeedView: View {
|
|
976
|
+
let filteredItems: [Item]
|
|
977
|
+
let sortedSections: [Section]
|
|
978
|
+
|
|
979
|
+
var body: some View {
|
|
980
|
+
List {
|
|
981
|
+
ForEach(filteredItems) { item in
|
|
982
|
+
Text(item.title)
|
|
983
|
+
}
|
|
984
|
+
ForEach(sortedSections) { section in
|
|
985
|
+
Text(section.title)
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
let ignored = "ForEach(items.filter { $0.isVisible }) { item in }"
|
|
991
|
+
// ForEach(items.filter { $0.isVisible }) { item in }
|
|
992
|
+
`;
|
|
993
|
+
|
|
994
|
+
assert.equal(hasSwiftInlineForEachTransformUsage(source), true);
|
|
995
|
+
assert.equal(hasSwiftInlineForEachTransformUsage(safe), false);
|
|
996
|
+
});
|
|
997
|
+
|
|
931
998
|
test('hasSwiftPassedValueStateWrapperUsage detecta valores inyectados guardados como @State o @StateObject', () => {
|
|
932
999
|
const invalidOwnership = `
|
|
933
1000
|
struct DetailView: View {
|
|
@@ -810,6 +810,13 @@ export const hasSwiftForEachIndicesUsage = (source: string): boolean => {
|
|
|
810
810
|
);
|
|
811
811
|
};
|
|
812
812
|
|
|
813
|
+
export const hasSwiftInlineForEachTransformUsage = (source: string): boolean => {
|
|
814
|
+
const sanitized = sanitizeSwiftSourceForMultilineRegex(source);
|
|
815
|
+
return /\bForEach\s*\(\s*(?:Array\s*\(\s*)?[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*\s*\.\s*(?:filter|map|compactMap|sorted)\s*(?:\{|\()/g.test(
|
|
816
|
+
sanitized
|
|
817
|
+
);
|
|
818
|
+
};
|
|
819
|
+
|
|
813
820
|
const isUserSearchIdentifier = (value: string): boolean => {
|
|
814
821
|
return /^(?:query|search(?:Text|Term|Query|Value)?|filter(?:Text|Value)?|text|term|input)$/i.test(
|
|
815
822
|
value
|
|
@@ -860,6 +867,17 @@ export const hasSwiftLegacySwiftUiObservableWrapperUsage = (source: string): boo
|
|
|
860
867
|
return hasSwiftSanitizedRegexMatch(source, /@\s*(?:StateObject|ObservedObject)\b/);
|
|
861
868
|
};
|
|
862
869
|
|
|
870
|
+
export const hasSwiftNonPrivateStateOwnershipUsage = (source: string): boolean => {
|
|
871
|
+
return source.split(/\r?\n/).some((line) => {
|
|
872
|
+
const sanitizedLine = stripSwiftLineForSemanticScan(line);
|
|
873
|
+
return (
|
|
874
|
+
/@\s*(?:State|StateObject)\b/.test(sanitizedLine) &&
|
|
875
|
+
/\bvar\b/.test(sanitizedLine) &&
|
|
876
|
+
!/\bprivate\b/.test(sanitizedLine)
|
|
877
|
+
);
|
|
878
|
+
});
|
|
879
|
+
};
|
|
880
|
+
|
|
863
881
|
const hasSwiftPassedValueWrapperInitialization = (
|
|
864
882
|
source: string,
|
|
865
883
|
options: {
|
|
@@ -675,8 +675,10 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
|
|
|
675
675
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftAssumeIsolatedUsage, ruleId: 'heuristics.ios.assume-isolated.ast', code: 'HEURISTICS_IOS_ASSUME_ISOLATED_AST', message: 'AST heuristic detected assumeIsolated usage.' },
|
|
676
676
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftObservableObjectUsage, ruleId: 'heuristics.ios.observable-object.ast', code: 'HEURISTICS_IOS_OBSERVABLE_OBJECT_AST', message: 'AST heuristic detected ObservableObject usage.' },
|
|
677
677
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftLegacySwiftUiObservableWrapperUsage, ruleId: 'heuristics.ios.legacy-swiftui-observable-wrapper.ast', code: 'HEURISTICS_IOS_LEGACY_SWIFTUI_OBSERVABLE_WRAPPER_AST', message: 'AST heuristic detected @StateObject/@ObservedObject usage in a modern SwiftUI path.' },
|
|
678
|
+
{ platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftNonPrivateStateOwnershipUsage, ruleId: 'heuristics.ios.swiftui.non-private-state-ownership.ast', code: 'HEURISTICS_IOS_SWIFTUI_NON_PRIVATE_STATE_OWNERSHIP_AST', message: 'AST heuristic detected @State/@StateObject without private visibility; SwiftUI owned state should be private.' },
|
|
678
679
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftPassedValueStateWrapperUsage, ruleId: 'heuristics.ios.passed-value-state-wrapper.ast', code: 'HEURISTICS_IOS_PASSED_VALUE_STATE_WRAPPER_AST', message: 'AST heuristic detected a passed value stored as @State/@StateObject via init wrapper ownership.' },
|
|
679
680
|
{ platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftForEachIndicesUsage, ruleId: 'heuristics.ios.foreach-indices.ast', code: 'HEURISTICS_IOS_FOREACH_INDICES_AST', message: 'AST heuristic detected ForEach(...indices...) usage where stable element identity may be preferred.' },
|
|
681
|
+
{ platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftInlineForEachTransformUsage, ruleId: 'heuristics.ios.swiftui.inline-foreach-transform.ast', code: 'HEURISTICS_IOS_SWIFTUI_INLINE_FOREACH_TRANSFORM_AST', message: 'AST heuristic detected inline filter/map/sort work inside ForEach; prefiltered or cached collections remain the preferred baseline.' },
|
|
680
682
|
{ platform: 'ios', pathCheck: isIOSApplicationOrPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftContainsUserFilterUsage, ruleId: 'heuristics.ios.contains-user-filter.ast', code: 'HEURISTICS_IOS_CONTAINS_USER_FILTER_AST', message: 'AST heuristic detected contains() in a user-facing filter where localizedStandardContains() may be preferred.' },
|
|
681
683
|
{ platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftGeometryReaderUsage, ruleId: 'heuristics.ios.geometryreader.ast', code: 'HEURISTICS_IOS_GEOMETRYREADER_AST', message: 'AST heuristic detected GeometryReader usage that may be replaceable with modern layout APIs.' },
|
|
682
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.' },
|
|
@@ -730,6 +730,25 @@ export const iosRules: RuleSet = [
|
|
|
730
730
|
code: 'HEURISTICS_IOS_LEGACY_SWIFTUI_OBSERVABLE_WRAPPER_AST',
|
|
731
731
|
},
|
|
732
732
|
},
|
|
733
|
+
{
|
|
734
|
+
id: 'heuristics.ios.swiftui.non-private-state-ownership.ast',
|
|
735
|
+
description: 'Detects @State/@StateObject declarations without private visibility in SwiftUI presentation code.',
|
|
736
|
+
severity: 'WARN',
|
|
737
|
+
platform: 'ios',
|
|
738
|
+
locked: true,
|
|
739
|
+
when: {
|
|
740
|
+
kind: 'Heuristic',
|
|
741
|
+
where: {
|
|
742
|
+
ruleId: 'heuristics.ios.swiftui.non-private-state-ownership.ast',
|
|
743
|
+
},
|
|
744
|
+
},
|
|
745
|
+
then: {
|
|
746
|
+
kind: 'Finding',
|
|
747
|
+
message:
|
|
748
|
+
'AST heuristic detected @State/@StateObject without private visibility; SwiftUI owned state should be private.',
|
|
749
|
+
code: 'HEURISTICS_IOS_SWIFTUI_NON_PRIVATE_STATE_OWNERSHIP_AST',
|
|
750
|
+
},
|
|
751
|
+
},
|
|
733
752
|
{
|
|
734
753
|
id: 'heuristics.ios.passed-value-state-wrapper.ast',
|
|
735
754
|
description: 'Detects passed values stored as @State or @StateObject through init ownership in SwiftUI production code.',
|
|
@@ -766,6 +785,24 @@ export const iosRules: RuleSet = [
|
|
|
766
785
|
code: 'HEURISTICS_IOS_FOREACH_INDICES_AST',
|
|
767
786
|
},
|
|
768
787
|
},
|
|
788
|
+
{
|
|
789
|
+
id: 'heuristics.ios.swiftui.inline-foreach-transform.ast',
|
|
790
|
+
description: 'Detects inline filter/map/sort transformations inside SwiftUI ForEach calls.',
|
|
791
|
+
severity: 'WARN',
|
|
792
|
+
platform: 'ios',
|
|
793
|
+
locked: true,
|
|
794
|
+
when: {
|
|
795
|
+
kind: 'Heuristic',
|
|
796
|
+
where: {
|
|
797
|
+
ruleId: 'heuristics.ios.swiftui.inline-foreach-transform.ast',
|
|
798
|
+
},
|
|
799
|
+
},
|
|
800
|
+
then: {
|
|
801
|
+
kind: 'Finding',
|
|
802
|
+
message: 'AST heuristic detected inline filter/map/sort work inside ForEach; prefiltered or cached collections remain the preferred baseline.',
|
|
803
|
+
code: 'HEURISTICS_IOS_SWIFTUI_INLINE_FOREACH_TRANSFORM_AST',
|
|
804
|
+
},
|
|
805
|
+
},
|
|
769
806
|
{
|
|
770
807
|
id: 'heuristics.ios.contains-user-filter.ast',
|
|
771
808
|
description: 'Detects contains() usage in user-facing filter flows where localizedStandardContains() may be preferred.',
|
|
@@ -201,6 +201,10 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
|
|
|
201
201
|
'ios.legacy-swiftui-observable-wrapper',
|
|
202
202
|
['heuristics.ios.legacy-swiftui-observable-wrapper.ast']
|
|
203
203
|
),
|
|
204
|
+
'skills.ios.guideline.ios-swiftui-expert.always-mark-state-and-stateobject-as-private-makes-dependencies-clear': heuristicDetector(
|
|
205
|
+
'ios.swiftui.non-private-state-ownership',
|
|
206
|
+
['heuristics.ios.swiftui.non-private-state-ownership.ast']
|
|
207
|
+
),
|
|
204
208
|
'skills.ios.no-passed-value-state-wrapper': heuristicDetector('ios.passed-value-state-wrapper', [
|
|
205
209
|
'heuristics.ios.passed-value-state-wrapper.ast',
|
|
206
210
|
]),
|
|
@@ -225,6 +229,10 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
|
|
|
225
229
|
'skills.ios.no-foreach-indices': heuristicDetector('ios.foreach-indices', [
|
|
226
230
|
'heuristics.ios.foreach-indices.ast',
|
|
227
231
|
]),
|
|
232
|
+
'skills.ios.guideline.ios-swiftui-expert.avoid-inline-filtering-in-foreach-prefilter-and-cache':
|
|
233
|
+
heuristicDetector('ios.swiftui.inline-foreach-transform', [
|
|
234
|
+
'heuristics.ios.swiftui.inline-foreach-transform.ast',
|
|
235
|
+
]),
|
|
228
236
|
'skills.ios.no-contains-user-filter': heuristicDetector('ios.contains-user-filter', [
|
|
229
237
|
'heuristics.ios.contains-user-filter.ast',
|
|
230
238
|
]),
|
|
@@ -260,6 +260,13 @@ const normalizeKnownRuleTarget = (
|
|
|
260
260
|
) {
|
|
261
261
|
return 'skills.ios.no-legacy-swiftui-observable-wrapper';
|
|
262
262
|
}
|
|
263
|
+
if (
|
|
264
|
+
includes('state and stateobject as private') ||
|
|
265
|
+
includes('stateobject as private') ||
|
|
266
|
+
(includes('mark state') && includes('private'))
|
|
267
|
+
) {
|
|
268
|
+
return 'skills.ios.guideline.ios-swiftui-expert.always-mark-state-and-stateobject-as-private-makes-dependencies-clear';
|
|
269
|
+
}
|
|
263
270
|
if (
|
|
264
271
|
includes('passed values as state') ||
|
|
265
272
|
includes('passed values as state or stateobject') ||
|
|
@@ -302,6 +309,13 @@ const normalizeKnownRuleTarget = (
|
|
|
302
309
|
) {
|
|
303
310
|
return 'skills.ios.no-foreach-indices';
|
|
304
311
|
}
|
|
312
|
+
if (
|
|
313
|
+
(includes('inline filtering') && includes('foreach')) ||
|
|
314
|
+
(includes('no inline filtering') && includes('foreach')) ||
|
|
315
|
+
(includes('prefilter') && includes('cache') && includes('foreach'))
|
|
316
|
+
) {
|
|
317
|
+
return 'skills.ios.guideline.ios-swiftui-expert.avoid-inline-filtering-in-foreach-prefilter-and-cache';
|
|
318
|
+
}
|
|
305
319
|
if (
|
|
306
320
|
includes('localizedstandardcontains') ||
|
|
307
321
|
includes('localized standard contains') ||
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.220",
|
|
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-
|
|
4
|
+
"generatedAt": "2026-05-13T15:54:55.035Z",
|
|
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": "ad099eaf37bfe92bde672c1042c7d4c18e3af4bbf91a1b9ee6f6a0dc0e5381b5",
|
|
8636
8636
|
"rules": [
|
|
8637
8637
|
{
|
|
8638
8638
|
"id": "skills.ios.guideline.ios-swiftui-expert.always-mark-state-and-stateobject-as-private-makes-dependencies-clear",
|
|
@@ -8643,7 +8643,7 @@
|
|
|
8643
8643
|
"sourcePath": "vendor/skills/swiftui-expert-skill/SKILL.md",
|
|
8644
8644
|
"confidence": "MEDIUM",
|
|
8645
8645
|
"locked": true,
|
|
8646
|
-
"evaluationMode": "
|
|
8646
|
+
"evaluationMode": "AUTO",
|
|
8647
8647
|
"origin": "core"
|
|
8648
8648
|
},
|
|
8649
8649
|
{
|
|
@@ -8655,7 +8655,7 @@
|
|
|
8655
8655
|
"sourcePath": "vendor/skills/swiftui-expert-skill/SKILL.md",
|
|
8656
8656
|
"confidence": "MEDIUM",
|
|
8657
8657
|
"locked": true,
|
|
8658
|
-
"evaluationMode": "
|
|
8658
|
+
"evaluationMode": "AUTO",
|
|
8659
8659
|
"origin": "core"
|
|
8660
8660
|
},
|
|
8661
8661
|
{
|