pumuki 6.3.327 → 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.
|
@@ -151,6 +151,7 @@ import {
|
|
|
151
151
|
collectSwiftOnChangeTaskLines,
|
|
152
152
|
collectSwiftOnChangeReadonlyVarLines,
|
|
153
153
|
collectSwiftStrongDelegateReferenceLines,
|
|
154
|
+
collectSwiftStrongSelfEscapingClosureLines,
|
|
154
155
|
hasSwiftOnChangeReadonlyVarUsage,
|
|
155
156
|
hasSwiftOnAppearTaskUsage,
|
|
156
157
|
collectSwiftOnAppearTaskLines,
|
|
@@ -1681,6 +1682,7 @@ final class CartViewModel {
|
|
|
1681
1682
|
`;
|
|
1682
1683
|
|
|
1683
1684
|
assert.equal(hasSwiftStrongSelfEscapingClosureUsage(source), true);
|
|
1685
|
+
assert.deepEqual(collectSwiftStrongSelfEscapingClosureLines(source), [7, 10, 13, 16, 19]);
|
|
1684
1686
|
});
|
|
1685
1687
|
|
|
1686
1688
|
test('hasSwiftStrongSelfEscapingClosureUsage preserva capture lists weak/unowned e ignora comentarios y strings', () => {
|
|
@@ -1703,6 +1705,7 @@ final class CartViewModel {
|
|
|
1703
1705
|
`;
|
|
1704
1706
|
|
|
1705
1707
|
assert.equal(hasSwiftStrongSelfEscapingClosureUsage(source), false);
|
|
1708
|
+
assert.deepEqual(collectSwiftStrongSelfEscapingClosureLines(source), []);
|
|
1706
1709
|
});
|
|
1707
1710
|
|
|
1708
1711
|
test('hasSwiftCustomSingletonUsage detecta singletons propios y excluye usos de singletons del sistema', () => {
|
|
@@ -1498,6 +1498,7 @@ const swiftStrongSelfEscapingClosurePatterns = [
|
|
|
1498
1498
|
/\bDispatchQueue\s*\.\s*[A-Za-z0-9_.]+\s*\.\s*async(?:After)?\s*(?:\([^)]*\))?\s*\{/g,
|
|
1499
1499
|
/\bTimer\s*\.\s*scheduledTimer\s*\([\s\S]{0,320}?\)\s*\{/g,
|
|
1500
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,
|
|
1501
1502
|
/\.\s*sink\s*\([\s\S]{0,420}?\b(?:receiveValue|receiveCompletion)\s*:\s*\{/g,
|
|
1502
1503
|
/\.\s*sink\s*\{/g,
|
|
1503
1504
|
/\.\s*handleEvents\s*\([\s\S]{0,420}?\b(?:receiveOutput|receiveCompletion|receiveCancel)\s*:\s*\{/g,
|
|
@@ -1562,6 +1563,44 @@ export const hasSwiftStrongSelfEscapingClosureUsage = (source: string): boolean
|
|
|
1562
1563
|
return false;
|
|
1563
1564
|
};
|
|
1564
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
|
+
|
|
1565
1604
|
export const hasSwiftCustomSingletonUsage = (source: string): boolean => {
|
|
1566
1605
|
const singletonDeclarationPattern =
|
|
1567
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*=/;
|
|
@@ -789,7 +789,7 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
|
|
|
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
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, 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.' },
|
|
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().' },
|
package/core/facts/index.test.ts
CHANGED
|
@@ -100,3 +100,37 @@ final class SystemPermissionsService {
|
|
|
100
100
|
assert.deepEqual(match.primary_node?.lines, [3]);
|
|
101
101
|
assert.match(match.expected_fix ?? '', /weak var delegate/i);
|
|
102
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.
|
|
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": {
|