pumuki 6.3.329 → 6.3.330
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.
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
collectSwiftAnyViewLines,
|
|
20
20
|
collectSwiftAnyTypeErasureLines,
|
|
21
21
|
collectSwiftCallbackStyleSignatureLines,
|
|
22
|
+
collectSwiftCombineSinkWithoutStoreLines,
|
|
22
23
|
collectSwiftDispatchGroupLines,
|
|
23
24
|
collectSwiftDispatchSemaphoreLines,
|
|
24
25
|
collectSwiftDummyAwaitLines,
|
|
@@ -528,7 +529,9 @@ let ignoredAssign = ".assign(to: \\\\Model.title, on: model)"
|
|
|
528
529
|
`;
|
|
529
530
|
|
|
530
531
|
assert.equal(hasSwiftCombineSinkWithoutStoreUsage(source), true);
|
|
532
|
+
assert.deepEqual(collectSwiftCombineSinkWithoutStoreLines(source), [3, 8]);
|
|
531
533
|
assert.equal(hasSwiftCombineSinkWithoutStoreUsage(safe), false);
|
|
534
|
+
assert.deepEqual(collectSwiftCombineSinkWithoutStoreLines(safe), []);
|
|
532
535
|
});
|
|
533
536
|
|
|
534
537
|
test('hasSwiftProductionTestDoubleUsage detecta mocks/spies productivos y preserva strings', () => {
|
|
@@ -129,8 +129,9 @@ export const hasSwiftWarningSuppressionUsage = (source: string): boolean => {
|
|
|
129
129
|
return collectSwiftWarningSuppressionLines(source).length > 0;
|
|
130
130
|
};
|
|
131
131
|
|
|
132
|
-
export const
|
|
132
|
+
export const collectSwiftCombineSinkWithoutStoreLines = (source: string): readonly number[] => {
|
|
133
133
|
const lines = source.split(/\r?\n/);
|
|
134
|
+
const matches: number[] = [];
|
|
134
135
|
|
|
135
136
|
for (let index = 0; index < lines.length; index += 1) {
|
|
136
137
|
const line = stripSwiftLineForSemanticScan(lines[index] ?? '');
|
|
@@ -144,11 +145,15 @@ export const hasSwiftCombineSinkWithoutStoreUsage = (source: string): boolean =>
|
|
|
144
145
|
.join('\n');
|
|
145
146
|
|
|
146
147
|
if (!/\.store\s*\(\s*in\s*:\s*&[A-Za-z_][A-Za-z0-9_]*\s*\)/.test(chain)) {
|
|
147
|
-
|
|
148
|
+
matches.push(index + 1);
|
|
148
149
|
}
|
|
149
150
|
}
|
|
150
151
|
|
|
151
|
-
return
|
|
152
|
+
return sortedUniqueLines(matches);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export const hasSwiftCombineSinkWithoutStoreUsage = (source: string): boolean => {
|
|
156
|
+
return collectSwiftCombineSinkWithoutStoreLines(source).length > 0;
|
|
152
157
|
};
|
|
153
158
|
|
|
154
159
|
export const hasSwiftProductionTestDoubleUsage = (source: string): boolean => {
|
|
@@ -774,7 +774,7 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
|
|
|
774
774
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftForceTryUsage, locateLines: TextIOS.collectSwiftForceTryLines, primaryNode: (lines) => ({ kind: 'call', name: 'force try expression try!', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: do/catch or throwing boundary', lines }], why: 'Force try converts a throwable operation into an unconditional runtime crash instead of preserving the typed error boundary.', impact: 'A recoverable domain, network, persistence or decoding error can terminate the app and bypass user-facing recovery, telemetry and tests.', expected_fix: 'Replace try! with do/catch, try await propagation, throws on the current boundary, or a guarded fallback that handles the error explicitly.', ruleId: 'heuristics.ios.force-try.ast', code: 'HEURISTICS_IOS_FORCE_TRY_AST', message: 'AST heuristic detected force try usage.' },
|
|
775
775
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftForceCastUsage, locateLines: TextIOS.collectSwiftForceCastLines, primaryNode: (lines) => ({ kind: 'call', name: 'force cast expression as!', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: conditional cast or typed boundary', lines }], why: 'Force cast converts a type mismatch into an unconditional runtime crash instead of preserving a checked domain or presentation boundary.', impact: 'Unexpected payloads, dependency substitutions or navigation models can terminate the app instead of producing a recoverable validation path.', expected_fix: 'Replace as! with as?, guard let, pattern matching, generic constraints, protocol boundaries, or a typed mapper that validates the runtime value explicitly.', ruleId: 'heuristics.ios.force-cast.ast', code: 'HEURISTICS_IOS_FORCE_CAST_AST', message: 'AST heuristic detected force cast usage.' },
|
|
776
776
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath, isApprovedIOSBridgePath], detect: TextIOS.hasSwiftCallbackStyleSignature, locateLines: TextIOS.collectSwiftCallbackStyleSignatureLines, primaryNode: (lines) => ({ kind: 'call', name: 'escaping callback-style API signature', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: async/await API or explicit bridge adapter', lines }], why: 'Callback-style completion APIs outside bridge layers bypass Swift structured concurrency and make cancellation, isolation and error flow implicit.', impact: 'Consumers must reason about escaping lifetime, actor hops and callback ordering manually, which increases race, leak and flaky-test risk in production iOS flows.', expected_fix: 'Expose async/await or AsyncSequence APIs in production boundaries. Keep callbacks only inside approved bridge/adapters that wrap legacy SDKs and document the conversion point explicitly.', ruleId: 'heuristics.ios.callback-style.ast', code: 'HEURISTICS_IOS_CALLBACK_STYLE_AST', message: 'AST heuristic detected callback-style API signature outside bridge layers.' },
|
|
777
|
-
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftCombineSinkWithoutStoreUsage, ruleId: 'heuristics.ios.combine.sink-without-store.ast', code: 'HEURISTICS_IOS_COMBINE_SINK_WITHOUT_STORE_AST', message: 'AST heuristic detected Combine sink without store(in:); keep cancellables retained explicitly.' },
|
|
777
|
+
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftCombineSinkWithoutStoreUsage, locateLines: TextIOS.collectSwiftCombineSinkWithoutStoreLines, primaryNode: (lines) => ({ kind: 'call', name: 'Combine sink/assign subscription without store(in:)', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: .store(in: &cancellables)', lines }], why: 'Combine subscriptions returned by sink/assign are cancelled immediately unless the AnyCancellable is retained explicitly.', impact: 'Reactive flows can silently stop receiving values, leak intent across view model lifetimes, or behave differently after navigation if subscription ownership is not clear.', expected_fix: 'Store the returned AnyCancellable with .store(in: &cancellables) or assign it to an explicit cancellable property owned by the view model/service lifetime.', ruleId: 'heuristics.ios.combine.sink-without-store.ast', code: 'HEURISTICS_IOS_COMBINE_SINK_WITHOUT_STORE_AST', message: 'AST heuristic detected Combine sink without store(in:); keep cancellables retained explicitly.' },
|
|
778
778
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftProductionTestDoubleUsage, ruleId: 'heuristics.ios.testing.production-test-double.ast', code: 'HEURISTICS_IOS_TESTING_PRODUCTION_TEST_DOUBLE_AST', message: 'AST heuristic detected Mock/Fake/Spy/Stub usage in iOS production code.' },
|
|
779
779
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftDispatchQueueUsage, locateLines: TextIOS.collectSwiftDispatchQueueLines, primaryNode: (lines) => ({ kind: 'call', name: 'GCD DispatchQueue call', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: structured concurrency Task/actor/MainActor boundary', lines }], why: 'DispatchQueue introduces unstructured GCD scheduling in production Swift code instead of preserving Swift concurrency cancellation, priority and actor isolation semantics.', impact: 'Manual queue hops make ordering, cancellation and main-actor safety harder to reason about, increasing race and flaky UI update risk.', expected_fix: 'Use async/await, Task, TaskGroup, actors, MainActor.run or isolated async APIs. Keep GCD only inside explicitly approved legacy bridge layers with documented ownership.', ruleId: 'heuristics.ios.dispatchqueue.ast', code: 'HEURISTICS_IOS_DISPATCHQUEUE_AST', message: 'AST heuristic detected DispatchQueue usage.' },
|
|
780
780
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftDispatchGroupUsage, locateLines: TextIOS.collectSwiftDispatchGroupLines, primaryNode: (lines) => ({ kind: 'call', name: 'GCD DispatchGroup call', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: structured concurrency TaskGroup or async aggregation boundary', lines }], why: 'DispatchGroup is an unstructured coordination primitive that makes asynchronous control flow and cancellation implicit instead of modeled by Swift concurrency.', impact: 'Group coordination is harder to reason about and can hide waiting or deadlock risks in production code paths that should be expressed through TaskGroup or async aggregation.', expected_fix: 'Use TaskGroup, async let, await aggregation, actors or explicit async APIs. Keep DispatchGroup only inside approved legacy bridge layers with documented ownership and migration scope.', ruleId: 'heuristics.ios.dispatchgroup.ast', code: 'HEURISTICS_IOS_DISPATCHGROUP_AST', message: 'AST heuristic detected DispatchGroup usage.' },
|
package/core/facts/index.test.ts
CHANGED
|
@@ -177,3 +177,38 @@ final class BuyerAPIClient {
|
|
|
177
177
|
assert.deepEqual(jsonSerialization.primary_node?.lines, [7]);
|
|
178
178
|
assert.match(jsonSerialization.expected_fix ?? '', /Codable/i);
|
|
179
179
|
});
|
|
180
|
+
|
|
181
|
+
test('extractHeuristicFacts emite Combine sink iOS con linea y remediation accionable', () => {
|
|
182
|
+
const params: HeuristicExtractionParams = {
|
|
183
|
+
facts: [
|
|
184
|
+
{
|
|
185
|
+
kind: 'FileContent',
|
|
186
|
+
source: 'repo',
|
|
187
|
+
path: 'apps/ios/Presentation/Features/Orders/OrdersViewModel.swift',
|
|
188
|
+
content: `
|
|
189
|
+
final class OrdersViewModel {
|
|
190
|
+
func bind() {
|
|
191
|
+
publisher
|
|
192
|
+
.sink { value in
|
|
193
|
+
self.render(value)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
`,
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
detectedPlatforms: {
|
|
201
|
+
ios: { detected: true, confidence: 'HIGH' },
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const extractedFacts = extractHeuristicFacts(params);
|
|
206
|
+
const match = extractedFacts.find(
|
|
207
|
+
(fact) => fact.ruleId === 'heuristics.ios.combine.sink-without-store.ast'
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
assert.ok(match);
|
|
211
|
+
assert.deepEqual(match.lines, [5]);
|
|
212
|
+
assert.deepEqual(match.primary_node?.lines, [5]);
|
|
213
|
+
assert.match(match.expected_fix ?? '', /\.store\(in:/i);
|
|
214
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.330",
|
|
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": {
|