pumuki 6.3.326 → 6.3.328

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.
@@ -150,6 +150,8 @@ import {
150
150
  hasSwiftOnChangeTaskUsage,
151
151
  collectSwiftOnChangeTaskLines,
152
152
  collectSwiftOnChangeReadonlyVarLines,
153
+ collectSwiftStrongDelegateReferenceLines,
154
+ collectSwiftStrongSelfEscapingClosureLines,
153
155
  hasSwiftOnChangeReadonlyVarUsage,
154
156
  hasSwiftOnAppearTaskUsage,
155
157
  collectSwiftOnAppearTaskLines,
@@ -1638,7 +1640,9 @@ final class CheckoutCoordinator {
1638
1640
  `;
1639
1641
 
1640
1642
  assert.equal(hasSwiftStrongDelegateReferenceUsage(positive), true);
1643
+ assert.deepEqual(collectSwiftStrongDelegateReferenceLines(positive), [3, 4]);
1641
1644
  assert.equal(hasSwiftStrongDelegateReferenceUsage(negative), false);
1645
+ assert.deepEqual(collectSwiftStrongDelegateReferenceLines(negative), []);
1642
1646
  });
1643
1647
 
1644
1648
  test('hasSwiftStrongDelegateReferenceUsage no marca propiedades no delegate', () => {
@@ -1678,6 +1682,7 @@ final class CartViewModel {
1678
1682
  `;
1679
1683
 
1680
1684
  assert.equal(hasSwiftStrongSelfEscapingClosureUsage(source), true);
1685
+ assert.deepEqual(collectSwiftStrongSelfEscapingClosureLines(source), [7, 10, 13, 16, 19]);
1681
1686
  });
1682
1687
 
1683
1688
  test('hasSwiftStrongSelfEscapingClosureUsage preserva capture lists weak/unowned e ignora comentarios y strings', () => {
@@ -1700,6 +1705,7 @@ final class CartViewModel {
1700
1705
  `;
1701
1706
 
1702
1707
  assert.equal(hasSwiftStrongSelfEscapingClosureUsage(source), false);
1708
+ assert.deepEqual(collectSwiftStrongSelfEscapingClosureLines(source), []);
1703
1709
  });
1704
1710
 
1705
1711
  test('hasSwiftCustomSingletonUsage detecta singletons propios y excluye usos de singletons del sistema', () => {
@@ -1474,11 +1474,31 @@ export const hasSwiftStrongDelegateReferenceUsage = (source: string): boolean =>
1474
1474
  });
1475
1475
  };
1476
1476
 
1477
+ export const collectSwiftStrongDelegateReferenceLines = (source: string): readonly number[] => {
1478
+ const delegatePropertyPattern =
1479
+ /\b(?:var|let)\s+(?:[A-Za-z_][A-Za-z0-9_]*(?:Delegate|DataSource)|delegate|dataSource)\s*:\s*(?:any\s+)?[A-Za-z_][A-Za-z0-9_]*(?:Delegate|DataSource)\b/;
1480
+ const lines: number[] = [];
1481
+
1482
+ source.split(/\r?\n/).forEach((line, index) => {
1483
+ const sanitizedLine = stripSwiftLineForSemanticScan(line);
1484
+ if (!delegatePropertyPattern.test(sanitizedLine)) {
1485
+ return;
1486
+ }
1487
+ if (/\bweak\s+var\b/.test(sanitizedLine)) {
1488
+ return;
1489
+ }
1490
+ lines.push(index + 1);
1491
+ });
1492
+
1493
+ return sortedUniqueLines(lines);
1494
+ };
1495
+
1477
1496
  const swiftStrongSelfEscapingClosurePatterns = [
1478
1497
  /\bTask\s*(?:\([^)]*\))?\s*\{/g,
1479
1498
  /\bDispatchQueue\s*\.\s*[A-Za-z0-9_.]+\s*\.\s*async(?:After)?\s*(?:\([^)]*\))?\s*\{/g,
1480
1499
  /\bTimer\s*\.\s*scheduledTimer\s*\([\s\S]{0,320}?\)\s*\{/g,
1481
1500
  /\bNotificationCenter\s*\.\s*default\s*\.\s*addObserver\s*\([\s\S]{0,420}?\busing\s*:\s*\{/g,
1501
+ /\bNotificationCenter\s*\.\s*default\s*\.\s*addObserver\s*\([\s\S]{0,420}?\)\s*\{/g,
1482
1502
  /\.\s*sink\s*\([\s\S]{0,420}?\b(?:receiveValue|receiveCompletion)\s*:\s*\{/g,
1483
1503
  /\.\s*sink\s*\{/g,
1484
1504
  /\.\s*handleEvents\s*\([\s\S]{0,420}?\b(?:receiveOutput|receiveCompletion|receiveCancel)\s*:\s*\{/g,
@@ -1543,6 +1563,44 @@ export const hasSwiftStrongSelfEscapingClosureUsage = (source: string): boolean
1543
1563
  return false;
1544
1564
  };
1545
1565
 
1566
+ const toSwiftLineNumberAtOffset = (source: string, offset: number): number =>
1567
+ source.slice(0, Math.max(0, offset)).split(/\r?\n/).length;
1568
+
1569
+ export const collectSwiftStrongSelfEscapingClosureLines = (source: string): readonly number[] => {
1570
+ const sanitized = sanitizeSwiftSourceForMultilineRegex(source);
1571
+ const lines: number[] = [];
1572
+
1573
+ for (const pattern of swiftStrongSelfEscapingClosurePatterns) {
1574
+ pattern.lastIndex = 0;
1575
+ for (const match of sanitized.matchAll(pattern)) {
1576
+ const matchedSource = match[0] ?? '';
1577
+ const openingBraceOffset = matchedSource.lastIndexOf('{');
1578
+ if (openingBraceOffset < 0 || match.index === undefined) {
1579
+ continue;
1580
+ }
1581
+ const openingBraceIndex = match.index + openingBraceOffset;
1582
+ const closingBraceIndex = findMatchingSwiftBraceIndex(sanitized, openingBraceIndex);
1583
+ if (closingBraceIndex < 0) {
1584
+ continue;
1585
+ }
1586
+ const closureBodyStartIndex = openingBraceIndex + 1;
1587
+ const closureBody = sanitized.slice(closureBodyStartIndex, closingBraceIndex);
1588
+ if (hasWeakOrUnownedSelfCaptureList(closureBody)) {
1589
+ continue;
1590
+ }
1591
+ const strongSelfMatches = closureBody.matchAll(/\bself\s*\./g);
1592
+ for (const strongSelfMatch of strongSelfMatches) {
1593
+ if (strongSelfMatch.index === undefined) {
1594
+ continue;
1595
+ }
1596
+ lines.push(toSwiftLineNumberAtOffset(sanitized, closureBodyStartIndex + strongSelfMatch.index));
1597
+ }
1598
+ }
1599
+ }
1600
+
1601
+ return sortedUniqueLines(lines);
1602
+ };
1603
+
1546
1604
  export const hasSwiftCustomSingletonUsage = (source: string): boolean => {
1547
1605
  const singletonDeclarationPattern =
1548
1606
  /^\s*(?:(?:private|fileprivate|internal|public|open)\s+)?static\s+(?:let|var)\s+shared\b(?:\s*:\s*[A-Za-z_][A-Za-z0-9_.<>]*)?\s*=/;
@@ -788,8 +788,8 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
788
788
  { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftOnAppearTaskUsage, locateLines: TextIOS.collectSwiftOnAppearTaskLines, primaryNode: (lines) => ({ kind: 'call', name: 'Task launched inside SwiftUI onAppear', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: .task { ... } on the view', lines }], why: 'A Task launched from onAppear is not owned by the SwiftUI view lifecycle in the same way as .task.', impact: 'Async work can outlive view disappearance or require manual cancellation, and the gate must point to the exact Task line instead of blocking the whole file.', expected_fix: 'Move the async work from .onAppear { Task { ... } } into .task { ... } so SwiftUI owns automatic cancellation. Keep onAppear only for synchronous side effects such as analytics.', ruleId: 'heuristics.ios.swiftui.onappear-task.ast', code: 'HEURISTICS_IOS_SWIFTUI_ONAPPEAR_TASK_AST', message: 'AST heuristic detected Task launched from SwiftUI onAppear; .task/.task(id:) provides lifecycle-aware cancellation.' },
789
789
  { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftOnChangeTaskUsage, locateLines: TextIOS.collectSwiftOnChangeTaskLines, primaryNode: (lines) => ({ kind: 'call', name: 'Task launched inside SwiftUI onChange', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: .task(id:) for value-dependent async work', lines }], why: 'A Task launched from onChange makes value-dependent async work manual instead of tying cancellation to the changing value.', impact: 'Search, load or refresh work can race after state changes because cancellation is not expressed through SwiftUI .task(id:).', expected_fix: 'Move value-dependent async work from .onChange { Task { ... } } into .task(id: value) { ... }. Keep onChange only for synchronous derivations or analytics.', ruleId: 'heuristics.ios.swiftui.onchange-task.ast', code: 'HEURISTICS_IOS_SWIFTUI_ONCHANGE_TASK_AST', message: 'AST heuristic detected Task launched from SwiftUI onChange; .task(id:) provides lifecycle-aware cancellation for value-dependent async work.' },
790
790
  { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftOnChangeReadonlyVarUsage, locateLines: TextIOS.collectSwiftOnChangeReadonlyVarLines, primaryNode: (lines) => ({ kind: 'property', name: 'var declared inside SwiftUI onChange closure', lines }), relatedNodes: (lines) => [{ kind: 'property', name: 'replacement: let for read-only derived value inside onChange', lines }], why: 'A local var inside onChange hides whether the closure is deriving a read-only value or mutating state as part of a reactive update.', impact: 'Reactive closures become harder to audit because unnecessary mutability can mask accidental state changes and value-dependent side effects.', expected_fix: 'Use let for read-only derived values inside onChange. Keep var only when the local value is intentionally mutated and extract complex mutation out of the view closure.', ruleId: 'heuristics.ios.swiftui.onchange-readonly-var.ast', code: 'HEURISTICS_IOS_SWIFTUI_ONCHANGE_READONLY_VAR_AST', message: 'AST heuristic detected local var inside SwiftUI onChange; prefer let for read-only derived values.' },
791
- { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftStrongDelegateReferenceUsage, ruleId: 'heuristics.ios.memory.strong-delegate.ast', code: 'HEURISTICS_IOS_MEMORY_STRONG_DELEGATE_AST', message: 'AST heuristic detected a strong delegate/dataSource reference; weak delegates remain the preferred baseline to avoid retain cycles.' },
792
- { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftStrongSelfEscapingClosureUsage, ruleId: 'heuristics.ios.memory.strong-self-escaping-closure.ast', code: 'HEURISTICS_IOS_MEMORY_STRONG_SELF_ESCAPING_CLOSURE_AST', message: 'AST heuristic detected strong self capture in an escaping iOS closure; weak or unowned captures remain the preferred baseline when ownership is not explicit.' },
791
+ { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftStrongDelegateReferenceUsage, locateLines: TextIOS.collectSwiftStrongDelegateReferenceLines, primaryNode: (lines) => ({ kind: 'property', name: 'strong delegate or dataSource property', lines }), relatedNodes: (lines) => [{ kind: 'property', name: 'replacement: weak var delegate/dataSource', lines }], why: 'Delegate and dataSource references usually point back to an owner or coordinator and should not be retained strongly by the callee.', impact: 'A strong delegate/dataSource property can create retain cycles between services, coordinators, view controllers or adapters, causing leaks that tests may not catch.', expected_fix: 'Declare delegate/dataSource references as weak var delegate or weak var dataSource, and keep the protocol class-bound with AnyObject when Swift requires weak storage.', ruleId: 'heuristics.ios.memory.strong-delegate.ast', code: 'HEURISTICS_IOS_MEMORY_STRONG_DELEGATE_AST', message: 'AST heuristic detected a strong delegate/dataSource reference; weak delegates remain the preferred baseline to avoid retain cycles.' },
792
+ { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftStrongSelfEscapingClosureUsage, locateLines: TextIOS.collectSwiftStrongSelfEscapingClosureLines, primaryNode: (lines) => ({ kind: 'call', name: 'escaping closure captures self strongly', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: capture [weak self] or explicit lifetime owner', lines }], why: 'Escaping closures can outlive the object that created them, so capturing self strongly keeps the owner alive unless lifetime ownership is explicit.', impact: 'Tasks, timers, NotificationCenter observers and Combine sinks can retain view models, coordinators or services and produce leaks that only appear after navigation or cancellation paths.', expected_fix: 'Add an explicit capture list such as [weak self] and unwrap self safely inside the closure, or document and encode a deliberate owner lifetime when a strong capture is required.', ruleId: 'heuristics.ios.memory.strong-self-escaping-closure.ast', code: 'HEURISTICS_IOS_MEMORY_STRONG_SELF_ESCAPING_CLOSURE_AST', message: 'AST heuristic detected strong self capture in an escaping iOS closure; weak or unowned captures remain the preferred baseline when ownership is not explicit.' },
793
793
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftUnownedSelfCaptureUsage, ruleId: 'heuristics.ios.memory.unowned-self-capture.ast', code: 'HEURISTICS_IOS_MEMORY_UNOWNED_SELF_CAPTURE_AST', message: 'AST heuristic detected unowned capture in an iOS closure; use weak capture unless lifetime is explicitly guaranteed.' },
794
794
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftManualMemoryManagementUsage, locateLines: TextIOS.collectSwiftManualMemoryManagementLines, primaryNode: (lines) => ({ kind: 'call', name: 'manual ARC bypass / Core Foundation retain-release', lines }), relatedNodes: (lines) => [{ kind: 'class', name: 'replacement: ARC-owned Swift object lifetime', lines }], why: 'Manual Unmanaged or Core Foundation retain/release bypasses normal Swift ARC ownership and needs an explicit bridge boundary.', impact: 'Manual memory ownership can leak or over-release objects, and without line evidence the gate cannot identify the unsafe bridge call.', expected_fix: 'Prefer ARC-owned Swift references. If a Core Foundation bridge is unavoidable, isolate it in infrastructure with documented ownership invariants and typed wrappers.', ruleId: 'heuristics.ios.memory.manual-management.ast', code: 'HEURISTICS_IOS_MEMORY_MANUAL_MANAGEMENT_AST', message: 'AST heuristic detected manual memory management that bypasses Swift ARC.' },
795
795
  { platform: 'ios', pathCheck: isSwiftTestPath, excludePaths: [], detect: TextIOS.hasSwiftDirectSUTInstantiationWithoutMakeSUTUsage, locateLines: TextIOS.collectSwiftDirectSUTInstantiationWithoutMakeSUTLines, primaryNode: (lines) => ({ kind: 'call', name: 'direct let sut = Type(...) instantiation', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: makeSUT() factory', lines }], why: 'iOS tests that instantiate the SUT inline duplicate setup and bypass the repository makeSUT factory contract.', impact: 'Test setup drifts across methods, memory tracking and dependency wiring become inconsistent, and later brownfield quality checks cannot rely on a single factory boundary.', expected_fix: 'Move direct SUT construction into a private makeSUT() helper and let test methods call let sut = makeSUT(). Add trackForMemoryLeaks(sut) inside the factory when the repository memory contract requires it.', ruleId: 'heuristics.ios.testing.direct-sut-instantiation-without-makesut.ast', code: 'HEURISTICS_IOS_TESTING_DIRECT_SUT_INSTANTIATION_WITHOUT_MAKESUT_AST', message: 'AST heuristic detected direct SUT instantiation in an iOS test without makeSUT().' },
@@ -70,3 +70,67 @@ test('facts barrel expone extractHeuristicFacts y permite retorno vacio sin plat
70
70
 
71
71
  assert.deepEqual(extractedFacts, []);
72
72
  });
73
+
74
+ test('extractHeuristicFacts emite strong delegate iOS con linea y remediation accionable', () => {
75
+ const params: HeuristicExtractionParams = {
76
+ facts: [
77
+ {
78
+ kind: 'FileContent',
79
+ source: 'repo',
80
+ path: 'apps/ios/Infrastructure/Permissions/SystemPermissionsService.swift',
81
+ content: `
82
+ final class SystemPermissionsService {
83
+ var delegate: PermissionsServiceDelegate?
84
+ }
85
+ `,
86
+ },
87
+ ],
88
+ detectedPlatforms: {
89
+ ios: { detected: true, confidence: 'HIGH' },
90
+ },
91
+ };
92
+
93
+ const extractedFacts = extractHeuristicFacts(params);
94
+ const match = extractedFacts.find(
95
+ (fact) => fact.ruleId === 'heuristics.ios.memory.strong-delegate.ast'
96
+ );
97
+
98
+ assert.ok(match);
99
+ assert.deepEqual(match.lines, [3]);
100
+ assert.deepEqual(match.primary_node?.lines, [3]);
101
+ assert.match(match.expected_fix ?? '', /weak var delegate/i);
102
+ });
103
+
104
+ test('extractHeuristicFacts emite strong self iOS con linea y remediation accionable', () => {
105
+ const params: HeuristicExtractionParams = {
106
+ facts: [
107
+ {
108
+ kind: 'FileContent',
109
+ source: 'repo',
110
+ path: 'apps/ios/Presentation/Features/Cart/CartViewModel.swift',
111
+ content: `
112
+ final class CartViewModel {
113
+ func bind() {
114
+ Task {
115
+ await self.reload()
116
+ }
117
+ }
118
+ }
119
+ `,
120
+ },
121
+ ],
122
+ detectedPlatforms: {
123
+ ios: { detected: true, confidence: 'HIGH' },
124
+ },
125
+ };
126
+
127
+ const extractedFacts = extractHeuristicFacts(params);
128
+ const match = extractedFacts.find(
129
+ (fact) => fact.ruleId === 'heuristics.ios.memory.strong-self-escaping-closure.ast'
130
+ );
131
+
132
+ assert.ok(match);
133
+ assert.deepEqual(match.lines, [5]);
134
+ assert.deepEqual(match.primary_node?.lines, [5]);
135
+ assert.match(match.expected_fix ?? '', /\[weak self\]/i);
136
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pumuki",
3
- "version": "6.3.326",
3
+ "version": "6.3.328",
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": {