pumuki 6.3.219 → 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 +46 -0
- package/core/facts/detectors/text/ios.ts +7 -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
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
hasSwiftSensitiveUserDefaultsStorageUsage,
|
|
62
62
|
hasSwiftInsecureTransportUsage,
|
|
63
63
|
hasSwiftJSONSerializationUsage,
|
|
64
|
+
hasSwiftInlineForEachTransformUsage,
|
|
64
65
|
hasSwiftStringFormatUsage,
|
|
65
66
|
hasSwiftStrongDelegateReferenceUsage,
|
|
66
67
|
hasSwiftStrongSelfEscapingClosureUsage,
|
|
@@ -736,6 +737,9 @@ let filtered = items.filter { $0.title.contains(searchText) }
|
|
|
736
737
|
ForEach(items.indices, id: \\.self) { index in
|
|
737
738
|
Text(items[index].title)
|
|
738
739
|
}
|
|
740
|
+
ForEach(items.filter { $0.isVisible }) { item in
|
|
741
|
+
Text(item.title)
|
|
742
|
+
}
|
|
739
743
|
Text("Primary").foregroundColor(.blue)
|
|
740
744
|
Image("hero").cornerRadius(12)
|
|
741
745
|
TabView {
|
|
@@ -771,6 +775,7 @@ MainActor.assumeIsolated { reload() }
|
|
|
771
775
|
assert.equal(hasSwiftNonisolatedUnsafeUsage(source), true);
|
|
772
776
|
assert.equal(hasSwiftAssumeIsolatedUsage(source), true);
|
|
773
777
|
assert.equal(hasSwiftForEachIndicesUsage(source), true);
|
|
778
|
+
assert.equal(hasSwiftInlineForEachTransformUsage(source), true);
|
|
774
779
|
assert.equal(hasSwiftContainsUserFilterUsage(source), true);
|
|
775
780
|
assert.equal(hasSwiftGeometryReaderUsage(source), true);
|
|
776
781
|
assert.equal(hasSwiftFontWeightBoldUsage(source), true);
|
|
@@ -812,11 +817,13 @@ let q = ".fontWeight(.bold)"
|
|
|
812
817
|
let r = "@preconcurrency import LegacyFramework"
|
|
813
818
|
let s = "nonisolated(unsafe) static var sharedBridge: Model?"
|
|
814
819
|
let t = "MainActor.assumeIsolated { reload() }"
|
|
820
|
+
let u = "ForEach(items.filter { $0.isVisible }) { item in }"
|
|
815
821
|
`;
|
|
816
822
|
assert.equal(hasSwiftPreconcurrencyUsage(source), false);
|
|
817
823
|
assert.equal(hasSwiftNonisolatedUnsafeUsage(source), false);
|
|
818
824
|
assert.equal(hasSwiftAssumeIsolatedUsage(source), false);
|
|
819
825
|
assert.equal(hasSwiftForEachIndicesUsage(source), false);
|
|
826
|
+
assert.equal(hasSwiftInlineForEachTransformUsage(source), false);
|
|
820
827
|
assert.equal(hasSwiftContainsUserFilterUsage(source), false);
|
|
821
828
|
assert.equal(hasSwiftGeometryReaderUsage(source), false);
|
|
822
829
|
assert.equal(hasSwiftFontWeightBoldUsage(source), false);
|
|
@@ -949,6 +956,45 @@ struct DashboardView: View {
|
|
|
949
956
|
assert.equal(hasSwiftNonPrivateStateOwnershipUsage(safe), false);
|
|
950
957
|
});
|
|
951
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
|
+
|
|
952
998
|
test('hasSwiftPassedValueStateWrapperUsage detecta valores inyectados guardados como @State o @StateObject', () => {
|
|
953
999
|
const invalidOwnership = `
|
|
954
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
|
|
@@ -678,6 +678,7 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
|
|
|
678
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.' },
|
|
679
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.' },
|
|
680
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.' },
|
|
681
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.' },
|
|
682
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.' },
|
|
683
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.' },
|
|
@@ -785,6 +785,24 @@ export const iosRules: RuleSet = [
|
|
|
785
785
|
code: 'HEURISTICS_IOS_FOREACH_INDICES_AST',
|
|
786
786
|
},
|
|
787
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
|
+
},
|
|
788
806
|
{
|
|
789
807
|
id: 'heuristics.ios.contains-user-filter.ast',
|
|
790
808
|
description: 'Detects contains() usage in user-facing filter flows where localizedStandardContains() may be preferred.',
|
|
@@ -229,6 +229,10 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
|
|
|
229
229
|
'skills.ios.no-foreach-indices': heuristicDetector('ios.foreach-indices', [
|
|
230
230
|
'heuristics.ios.foreach-indices.ast',
|
|
231
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
|
+
]),
|
|
232
236
|
'skills.ios.no-contains-user-filter': heuristicDetector('ios.contains-user-filter', [
|
|
233
237
|
'heuristics.ios.contains-user-filter.ast',
|
|
234
238
|
]),
|
|
@@ -309,6 +309,13 @@ const normalizeKnownRuleTarget = (
|
|
|
309
309
|
) {
|
|
310
310
|
return 'skills.ios.no-foreach-indices';
|
|
311
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
|
+
}
|
|
312
319
|
if (
|
|
313
320
|
includes('localizedstandardcontains') ||
|
|
314
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-13T15:
|
|
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",
|
|
@@ -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
|
{
|