pumuki 6.3.297 → 6.3.299

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.
@@ -165,6 +165,110 @@ export const hasSwiftUnownedSelfCaptureUsage = (source: string): boolean => {
165
165
  );
166
166
  };
167
167
 
168
+ export const collectSwiftManualMemoryManagementLines = (source: string): readonly number[] => {
169
+ return sortedUniqueLines([
170
+ ...collectSwiftRegexLines(source, /\bUnmanaged\s*</),
171
+ ...collectSwiftRegexLines(source, /\.(?:takeRetainedValue|takeUnretainedValue)\s*\(/),
172
+ ...collectSwiftRegexLines(source, /\b(?:CFRetain|CFRelease)\s*\(/),
173
+ ]);
174
+ };
175
+
176
+ export const hasSwiftManualMemoryManagementUsage = (source: string): boolean => {
177
+ return collectSwiftManualMemoryManagementLines(source).length > 0;
178
+ };
179
+
180
+ const swiftPascalCaseTypeNamePattern = /^[A-Z][A-Za-z0-9]*$/;
181
+
182
+ export const collectSwiftNonPascalCaseTypeDeclarationLines = (source: string): readonly number[] => {
183
+ const matches: number[] = [];
184
+
185
+ source.split(/\r?\n/).forEach((rawLine, index) => {
186
+ const line = stripSwiftLineForSemanticScan(rawLine);
187
+ const declarationMatches = line.matchAll(
188
+ /\b(?:class|struct|enum|actor|protocol)\s+([A-Za-z_][A-Za-z0-9_]*)\b/g
189
+ );
190
+
191
+ for (const match of declarationMatches) {
192
+ const typeName = match[1];
193
+ if (typeName && !swiftPascalCaseTypeNamePattern.test(typeName)) {
194
+ matches.push(index + 1);
195
+ break;
196
+ }
197
+ }
198
+ });
199
+
200
+ return sortedUniqueLines(matches);
201
+ };
202
+
203
+ export const hasSwiftNonPascalCaseTypeDeclarationUsage = (source: string): boolean => {
204
+ return collectSwiftNonPascalCaseTypeDeclarationLines(source).length > 0;
205
+ };
206
+
207
+ const collectSwiftMethodBodyLines = (
208
+ source: string,
209
+ methodPattern: RegExp,
210
+ bodyMatcher: (line: string) => boolean
211
+ ): readonly number[] => {
212
+ const lines = source.split(/\r?\n/);
213
+ const matches: number[] = [];
214
+
215
+ for (let index = 0; index < lines.length; index += 1) {
216
+ const declarationLine = stripSwiftLineForSemanticScan(lines[index] ?? '');
217
+ methodPattern.lastIndex = 0;
218
+ if (!methodPattern.test(declarationLine)) {
219
+ continue;
220
+ }
221
+
222
+ let braceDepth =
223
+ countTokenOccurrences(declarationLine, '{') - countTokenOccurrences(declarationLine, '}');
224
+ const bodyStart = index + 1;
225
+ const bodyEnd = lines.length;
226
+ let hasReuse = /\.dequeueReusableCell\s*\(/.test(declarationLine);
227
+ const localMatches: number[] = [];
228
+
229
+ for (let cursor = bodyStart; cursor < bodyEnd; cursor += 1) {
230
+ const line = stripSwiftLineForSemanticScan(lines[cursor] ?? '');
231
+ if (/\.dequeueReusableCell\s*\(/.test(line)) {
232
+ hasReuse = true;
233
+ }
234
+ if (bodyMatcher(line)) {
235
+ localMatches.push(cursor + 1);
236
+ }
237
+
238
+ braceDepth += countTokenOccurrences(line, '{');
239
+ braceDepth -= countTokenOccurrences(line, '}');
240
+ if (braceDepth <= 0) {
241
+ break;
242
+ }
243
+ }
244
+
245
+ if (!hasReuse) {
246
+ matches.push(...localMatches);
247
+ }
248
+ }
249
+
250
+ return sortedUniqueLines(matches);
251
+ };
252
+
253
+ export const collectSwiftCellCreationWithoutReuseLines = (source: string): readonly number[] => {
254
+ return sortedUniqueLines([
255
+ ...collectSwiftMethodBodyLines(
256
+ source,
257
+ /\bfunc\s+tableView\s*\([^)]*\bcellForRowAt\b[^)]*\)\s*->\s*UITableViewCell\b/,
258
+ (line) => /\bUITableViewCell\s*\(/.test(line)
259
+ ),
260
+ ...collectSwiftMethodBodyLines(
261
+ source,
262
+ /\bfunc\s+collectionView\s*\([^)]*\bcellForItemAt\b[^)]*\)\s*->\s*UICollectionViewCell\b/,
263
+ (line) => /\bUICollectionViewCell\s*\(/.test(line)
264
+ ),
265
+ ]);
266
+ };
267
+
268
+ export const hasSwiftCellCreationWithoutReuseUsage = (source: string): boolean => {
269
+ return collectSwiftCellCreationWithoutReuseLines(source).length > 0;
270
+ };
271
+
168
272
  export const hasSwiftNestedIfPyramidUsage = (source: string): boolean => {
169
273
  const lines = source.split(/\r?\n/);
170
274
  const ifBraceDepths: number[] = [];
@@ -565,6 +669,92 @@ export const hasSwiftUiImageDataDecodingUsage = (source: string): boolean => {
565
669
  return /\bUIImage\s*\(\s*data\s*:/.test(swiftSource);
566
670
  };
567
671
 
672
+ export const collectSwiftUiManualRenderingWithoutImageRendererLines = (source: string): readonly number[] => {
673
+ const swiftSource = sanitizeSwiftSourceForMultilineRegex(source);
674
+ if (/\bImageRenderer\s*\(/.test(swiftSource)) {
675
+ return [];
676
+ }
677
+
678
+ const hasHostedSwiftUiView = /\bUIHostingController\s*\(\s*rootView\s*:/.test(swiftSource);
679
+ const hasManualRenderer =
680
+ /\bUIGraphicsImageRenderer\s*\(/.test(swiftSource) ||
681
+ /\.drawHierarchy\s*\(/.test(swiftSource) ||
682
+ /\.layer\s*\.\s*render\s*\(/.test(swiftSource);
683
+
684
+ if (!hasHostedSwiftUiView || !hasManualRenderer) {
685
+ return [];
686
+ }
687
+
688
+ return sortedUniqueLines([
689
+ ...collectSwiftRegexLines(source, /\bUIHostingController\s*\(\s*rootView\s*:/),
690
+ ...collectSwiftRegexLines(source, /\bUIGraphicsImageRenderer\s*\(/),
691
+ ...collectSwiftRegexLines(source, /\.drawHierarchy\s*\(/),
692
+ ...collectSwiftRegexLines(source, /\.layer\s*\.\s*render\s*\(/),
693
+ ]);
694
+ };
695
+
696
+ export const hasSwiftUiManualRenderingWithoutImageRendererUsage = (source: string): boolean => {
697
+ return collectSwiftUiManualRenderingWithoutImageRendererLines(source).length > 0;
698
+ };
699
+
700
+ export const collectSwiftLargeViewBuilderFunctionLines = (source: string): readonly number[] => {
701
+ const lines = source.split(/\r?\n/);
702
+ const matches: number[] = [];
703
+
704
+ for (let index = 0; index < lines.length; index += 1) {
705
+ const annotationLine = stripSwiftLineForSemanticScan(lines[index] ?? '');
706
+ if (!/@ViewBuilder\b/.test(annotationLine)) {
707
+ continue;
708
+ }
709
+
710
+ let functionLineIndex = -1;
711
+ for (let candidateIndex = index; candidateIndex < Math.min(lines.length, index + 4); candidateIndex += 1) {
712
+ const candidate = stripSwiftLineForSemanticScan(lines[candidateIndex] ?? '');
713
+ if (/\bfunc\s+[A-Za-z_][A-Za-z0-9_]*\s*\(/.test(candidate)) {
714
+ functionLineIndex = candidateIndex;
715
+ break;
716
+ }
717
+ }
718
+
719
+ if (functionLineIndex < 0) {
720
+ continue;
721
+ }
722
+
723
+ let braceDepth = 0;
724
+ let bodyStarted = false;
725
+ let bodyLineCount = 0;
726
+
727
+ for (let bodyIndex = functionLineIndex; bodyIndex < lines.length; bodyIndex += 1) {
728
+ const line = stripSwiftLineForSemanticScan(lines[bodyIndex] ?? '');
729
+ if (!bodyStarted && line.includes('{')) {
730
+ bodyStarted = true;
731
+ } else if (bodyStarted) {
732
+ const bodyOnly = line.replace(/[{}]/g, '').trim();
733
+ if (bodyOnly.length > 0) {
734
+ bodyLineCount += 1;
735
+ }
736
+ }
737
+
738
+ braceDepth += countTokenOccurrences(line, '{');
739
+ braceDepth -= countTokenOccurrences(line, '}');
740
+
741
+ if (bodyStarted && braceDepth <= 0) {
742
+ break;
743
+ }
744
+ }
745
+
746
+ if (bodyLineCount > 12) {
747
+ matches.push(index + 1, functionLineIndex + 1);
748
+ }
749
+ }
750
+
751
+ return sortedUniqueLines(matches);
752
+ };
753
+
754
+ export const hasSwiftLargeViewBuilderFunctionUsage = (source: string): boolean => {
755
+ return collectSwiftLargeViewBuilderFunctionLines(source).length > 0;
756
+ };
757
+
568
758
  export const hasSwiftUiInlineActionLogicUsage = (source: string): boolean => {
569
759
  const swiftSource = sanitizeSwiftSourceForMultilineRegex(source);
570
760
  const inlineButtonActionPattern =
@@ -575,6 +765,22 @@ export const hasSwiftUiInlineActionLogicUsage = (source: string): boolean => {
575
765
  return inlineButtonActionPattern.test(swiftSource) || inlineActionParameterPattern.test(swiftSource);
576
766
  };
577
767
 
768
+ export const collectSwiftAnimationWithoutReduceMotionLines = (source: string): readonly number[] => {
769
+ const swiftSource = sanitizeSwiftSourceForMultilineRegex(source);
770
+ if (/\baccessibilityReduceMotion\b|\bUIAccessibility\s*\.\s*isReduceMotionEnabled\b/.test(swiftSource)) {
771
+ return [];
772
+ }
773
+
774
+ return sortedUniqueLines([
775
+ ...collectSwiftRegexLines(source, /\bwithAnimation\s*(?:\(|\{)/),
776
+ ...collectSwiftRegexLines(source, /\.animation\s*\(/),
777
+ ]);
778
+ };
779
+
780
+ export const hasSwiftAnimationWithoutReduceMotionUsage = (source: string): boolean => {
781
+ return collectSwiftAnimationWithoutReduceMotionLines(source).length > 0;
782
+ };
783
+
578
784
  export const collectSwiftUiInlineActionLogicLines = (source: string): readonly number[] => {
579
785
  if (!hasSwiftUiInlineActionLogicUsage(source)) {
580
786
  return [];
@@ -723,6 +929,52 @@ export const hasSwiftPackageBranchDependencyUsage = (source: string): boolean =>
723
929
  return collectSwiftPackageBranchDependencyLines(source).length > 0;
724
930
  };
725
931
 
932
+ export const collectSwiftPackageToolsVersionBelow62Lines = (source: string): readonly number[] => {
933
+ const lines: number[] = [];
934
+
935
+ source.split(/\r?\n/).forEach((line, index) => {
936
+ const match = line.match(/^\s*\/\/\s*swift-tools-version\s*:\s*(\d+)(?:\.(\d+))?/);
937
+ if (!match) {
938
+ return;
939
+ }
940
+
941
+ const major = Number.parseInt(match[1] ?? '0', 10);
942
+ const minor = Number.parseInt(match[2] ?? '0', 10);
943
+ if (major < 6 || (major === 6 && minor < 2)) {
944
+ lines.push(index + 1);
945
+ }
946
+ });
947
+
948
+ return lines;
949
+ };
950
+
951
+ export const hasSwiftPackageToolsVersionBelow62Usage = (source: string): boolean => {
952
+ return collectSwiftPackageToolsVersionBelow62Lines(source).length > 0;
953
+ };
954
+
955
+ export const collectSwiftStrictConcurrencyBelowCompleteLines = (source: string): readonly number[] => {
956
+ const lines: number[] = [];
957
+
958
+ source.split(/\r?\n/).forEach((rawLine, index) => {
959
+ const line = stripSwiftLineForSemanticScan(rawLine);
960
+ const match = /\bSWIFT_STRICT_CONCURRENCY\s*=\s*([A-Za-z_][A-Za-z0-9_]*)\s*;?/.exec(line);
961
+ if (!match) {
962
+ return;
963
+ }
964
+
965
+ const value = (match[1] ?? '').trim().toLowerCase();
966
+ if (value === 'minimal' || value === 'targeted') {
967
+ lines.push(index + 1);
968
+ }
969
+ });
970
+
971
+ return sortedUniqueLines(lines);
972
+ };
973
+
974
+ export const hasSwiftStrictConcurrencyBelowCompleteUsage = (source: string): boolean => {
975
+ return collectSwiftStrictConcurrencyBelowCompleteLines(source).length > 0;
976
+ };
977
+
726
978
  export const hasSwiftOnAppearTaskUsage = (source: string): boolean => {
727
979
  return collectSwiftOnAppearTaskLines(source).length > 0;
728
980
  };
@@ -1751,6 +2003,42 @@ export const hasSwiftOnTapGestureUsage = (source: string): boolean => {
1751
2003
  });
1752
2004
  };
1753
2005
 
2006
+ export const collectSwiftOnTapGestureWithoutButtonTraitLines = (source: string): readonly number[] => {
2007
+ const sanitizedLines = sanitizeSwiftSourceForMultilineRegex(source).split(/\r?\n/);
2008
+ const originalLines = source.split(/\r?\n/);
2009
+ const matches: number[] = [];
2010
+
2011
+ for (let index = 0; index < sanitizedLines.length; index += 1) {
2012
+ if (!/\.onTapGesture\s*(?:\(|\{)/.test(sanitizedLines[index] ?? '')) {
2013
+ continue;
2014
+ }
2015
+
2016
+ const modifierWindow = sanitizedLines
2017
+ .slice(index, Math.min(sanitizedLines.length, index + 8))
2018
+ .join('\n');
2019
+
2020
+ if (/\.accessibilityAddTraits\s*\(\s*\.isButton\s*\)/.test(modifierWindow)) {
2021
+ continue;
2022
+ }
2023
+
2024
+ if (/\.accessibilityAddTraits\s*\(\s*AccessibilityTraits\s*\.\s*isButton\s*\)/.test(modifierWindow)) {
2025
+ continue;
2026
+ }
2027
+
2028
+ if (!/\.onTapGesture\s*(?:\(|\{)/.test(stripSwiftLineForSemanticScan(originalLines[index] ?? ''))) {
2029
+ continue;
2030
+ }
2031
+
2032
+ matches.push(index + 1);
2033
+ }
2034
+
2035
+ return sortedUniqueLines(matches);
2036
+ };
2037
+
2038
+ export const hasSwiftOnTapGestureWithoutButtonTraitUsage = (source: string): boolean => {
2039
+ return collectSwiftOnTapGestureWithoutButtonTraitLines(source).length > 0;
2040
+ };
2041
+
1754
2042
  export const hasSwiftStringFormatUsage = (source: string): boolean => {
1755
2043
  return scanCodeLikeSource(source, ({ source: swiftSource, index, current }) => {
1756
2044
  if (current !== 'S' || !hasIdentifierAt(swiftSource, index, 'String')) {
@@ -1856,6 +2144,22 @@ export const hasSwiftMakeSUTWithoutMemoryTrackingUsage = (source: string): boole
1856
2144
  return collectSwiftMakeSUTWithoutMemoryTrackingLines(source).length > 0;
1857
2145
  };
1858
2146
 
2147
+ export const collectSwiftDirectSUTInstantiationWithoutMakeSUTLines = (source: string): readonly number[] => {
2148
+ const sanitized = sanitizeSwiftSourceForMultilineRegex(source);
2149
+ if (/\bmakeSUT\s*\(/.test(sanitized)) {
2150
+ return [];
2151
+ }
2152
+
2153
+ return collectSwiftRegexLines(
2154
+ source,
2155
+ /^\s*(?:let|var)\s+sut\s*=\s*[A-Z][A-Za-z0-9_]*(?:<[^>]+>)?\s*\(/g
2156
+ );
2157
+ };
2158
+
2159
+ export const hasSwiftDirectSUTInstantiationWithoutMakeSUTUsage = (source: string): boolean => {
2160
+ return collectSwiftDirectSUTInstantiationWithoutMakeSUTLines(source).length > 0;
2161
+ };
2162
+
1859
2163
  export const hasSwiftLegacyXCTestImportUsage = (source: string): boolean => {
1860
2164
  if (!hasSwiftXCTestImportUsage(source)) {
1861
2165
  return false;
@@ -110,6 +110,11 @@ const isIOSSwiftPackageManifestPath = (path: string): boolean => {
110
110
  return path.replace(/\\/g, '/') === 'apps/ios/Package.swift';
111
111
  };
112
112
 
113
+ const isIOSXcodeProjectFilePath = (path: string): boolean => {
114
+ const normalized = path.replace(/\\/g, '/');
115
+ return normalized.startsWith('apps/ios/') && normalized.endsWith('/project.pbxproj');
116
+ };
117
+
113
118
  const isIOSPodfilePath = (path: string): boolean => {
114
119
  const normalized = path.replace(/\\/g, '/');
115
120
  return (
@@ -748,6 +753,10 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
748
753
  { platform: 'ios', pathCheck: isIOSPodfilePath, excludePaths: [], detect: detectsTrackedFilePresence, ruleId: 'heuristics.ios.dependencies.cocoapods.ast', code: 'HEURISTICS_IOS_DEPENDENCIES_COCOAPODS_AST', message: 'AST heuristic detected CocoaPods dependency files in an iOS project; Swift Package Manager remains the preferred baseline for new code.' },
749
754
  { platform: 'ios', pathCheck: isIOSCartfilePath, excludePaths: [], detect: detectsTrackedFilePresence, ruleId: 'heuristics.ios.dependencies.carthage.ast', code: 'HEURISTICS_IOS_DEPENDENCIES_CARTHAGE_AST', message: 'AST heuristic detected Carthage dependency files in an iOS project; Swift Package Manager remains the preferred baseline for new code.' },
750
755
  { platform: 'ios', pathCheck: isIOSSwiftPackageManifestPath, excludePaths: [], detect: TextIOS.hasSwiftPackageBranchDependencyUsage, locateLines: TextIOS.collectSwiftPackageBranchDependencyLines, primaryNode: (lines) => ({ kind: 'call', name: 'SwiftPM .package(..., branch: ...)', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: .package(..., exact:/from: version)', lines }], why: 'Branch-based SwiftPM dependencies drift over time and do not provide a reproducible iOS dependency graph.', impact: 'A consumer can build different code from the same commit when the remote branch moves, making production audits and regressions non-deterministic.', expected_fix: 'Pin the dependency to an exact version or an approved semantic version requirement in Package.swift; avoid branch-based dependencies outside explicitly approved experiments.', ruleId: 'heuristics.ios.dependencies.swiftpm-branch-dependency.ast', code: 'HEURISTICS_IOS_DEPENDENCIES_SWIFTPM_BRANCH_DEPENDENCY_AST', message: 'AST heuristic detected a branch-based SwiftPM dependency in iOS Package.swift; use specific versions for reproducible builds.' },
756
+ { platform: 'ios', pathCheck: isIOSSwiftPackageManifestPath, excludePaths: [], detect: TextIOS.hasSwiftPackageToolsVersionBelow62Usage, locateLines: TextIOS.collectSwiftPackageToolsVersionBelow62Lines, primaryNode: (lines) => ({ kind: 'property', name: 'Package.swift swift-tools-version directive', lines }), relatedNodes: (lines) => [{ kind: 'property', name: 'replacement: // swift-tools-version: 6.2', lines }], why: 'An iOS Swift package manifest below Swift tools 6.2 cannot guarantee the current Swift 6.2 language baseline expected by the project skills.', impact: 'Consumers can compile with an older toolchain mode and miss concurrency or language diagnostics that Pumuki expects to enforce.', expected_fix: 'Update the Package.swift directive to // swift-tools-version: 6.2 and verify the package with the repository Xcode/Swift toolchain.', ruleId: 'heuristics.ios.dependencies.swift-tools-version-below-6-2.ast', code: 'HEURISTICS_IOS_DEPENDENCIES_SWIFT_TOOLS_VERSION_BELOW_6_2_AST', message: 'AST heuristic detected Package.swift using swift-tools-version below 6.2.' },
757
+ { platform: 'ios', pathCheck: isIOSXcodeProjectFilePath, excludePaths: [], detect: TextIOS.hasSwiftStrictConcurrencyBelowCompleteUsage, locateLines: TextIOS.collectSwiftStrictConcurrencyBelowCompleteLines, primaryNode: (lines) => ({ kind: 'property', name: 'SWIFT_STRICT_CONCURRENCY below complete', lines }), relatedNodes: (lines) => [{ kind: 'property', name: 'replacement: SWIFT_STRICT_CONCURRENCY = complete', lines }], why: 'The iOS skill contract requires Complete strict concurrency checking so unsafe actor/sendability issues cannot pass silently.', impact: 'Minimal or targeted strict concurrency leaves parts of the module below the Swift concurrency safety baseline and can hide data-race warnings.', expected_fix: 'Set SWIFT_STRICT_CONCURRENCY = complete for the affected iOS build configuration after addressing surfaced warnings.', ruleId: 'heuristics.ios.concurrency.strict-concurrency-below-complete.ast', code: 'HEURISTICS_IOS_CONCURRENCY_STRICT_CONCURRENCY_BELOW_COMPLETE_AST', message: 'AST heuristic detected SWIFT_STRICT_CONCURRENCY below complete in an iOS Xcode project.' },
758
+ { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftNonPascalCaseTypeDeclarationUsage, locateLines: TextIOS.collectSwiftNonPascalCaseTypeDeclarationLines, primaryNode: (lines) => ({ kind: 'class', name: 'Swift type declaration without PascalCase', lines }), relatedNodes: (lines) => [{ kind: 'class', name: 'replacement: PascalCase type name', lines }], why: 'Swift type declarations should use PascalCase so public and internal APIs remain idiomatic, searchable and reviewable.', impact: 'Non-PascalCase type names make ownership boundaries less consistent and weaken automated remediation because the gate cannot rely on the declaration node name.', expected_fix: 'Rename the Swift class, struct, enum, actor or protocol declaration to PascalCase and update its references in the same slice.', ruleId: 'heuristics.ios.naming.non-pascal-case-type.ast', code: 'HEURISTICS_IOS_NAMING_NON_PASCAL_CASE_TYPE_AST', message: 'AST heuristic detected Swift type declaration without PascalCase.' },
759
+ { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftCellCreationWithoutReuseUsage, locateLines: TextIOS.collectSwiftCellCreationWithoutReuseLines, primaryNode: (lines) => ({ kind: 'call', name: 'UIKit cell created without reuse in cell provider', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: dequeueReusableCell(withIdentifier:for:)', lines }], why: 'UITableView and UICollectionView cell providers must reuse cells instead of allocating a fresh cell for every item.', impact: 'Creating cells directly inside cellForRowAt or cellForItemAt degrades scrolling performance and bypasses UIKit reuse semantics.', expected_fix: 'Register the cell type or nib and return tableView/collectionView.dequeueReusableCell(withIdentifier:for:) from the cell provider.', ruleId: 'heuristics.ios.uikit.cell-without-reuse.ast', code: 'HEURISTICS_IOS_UIKIT_CELL_WITHOUT_REUSE_AST', message: 'AST heuristic detected UIKit cell provider creating cells without dequeueReusableCell.' },
751
760
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftForceUnwrap, locateLines: TextIOS.collectSwiftForceUnwrapLines, primaryNode: (lines) => ({ kind: 'member', name: 'force unwrap postfix !', lines }), relatedNodes: (lines) => [{ kind: 'member', name: 'replacement: guarded optional binding or explicit failure path', lines }], why: 'Force unwrap turns optional handling into a runtime crash path instead of a checked domain, UI or infrastructure decision.', impact: 'A nil value can terminate the app outside the error boundary, making production behavior non-deterministic and hard to recover or test.', expected_fix: 'Replace postfix ! with guard let, if let, nil coalescing, throwing validation, or an explicit fallback. In modern Swift tests prefer #require when the unwrap is part of an assertion contract.', ruleId: 'heuristics.ios.force-unwrap.ast', code: 'HEURISTICS_IOS_FORCE_UNWRAP_AST', message: 'AST heuristic detected force unwrap usage.' },
752
761
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftAnyViewUsage, locateLines: TextIOS.collectSwiftAnyViewLines, primaryNode: (lines) => ({ kind: 'call', name: 'type erasure wrapper AnyView', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: concrete View composition or @ViewBuilder branch', lines }], why: 'AnyView erases SwiftUI view identity and type information, hiding structural changes from the compiler and making diffing less predictable.', impact: 'SwiftUI may lose optimization opportunities, navigation/sheet branches become harder to reason about, and remediating UI regressions requires reading dynamic wrappers instead of concrete view composition.', expected_fix: 'Replace AnyView with concrete some View composition, @ViewBuilder branching, generic View parameters, or small extracted subviews that preserve static SwiftUI identity.', ruleId: 'heuristics.ios.anyview.ast', code: 'HEURISTICS_IOS_ANYVIEW_AST', message: 'AST heuristic detected AnyView usage.' },
753
762
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftAnyTypeErasureUsage, locateLines: TextIOS.collectSwiftAnyTypeErasureLines, primaryNode: (lines) => ({ kind: 'property', name: 'Swift Any/AnyObject/AnyHashable type erasure', lines }), relatedNodes: (lines) => [{ kind: 'member', name: 'replacement: generics, associated types, protocol boundary or concrete domain type', lines }], why: 'General Swift type erasure hides domain contracts that should be expressed with generics, associated types or explicit protocol boundaries.', impact: 'Callers lose compile-time guarantees, invalid states travel through the codebase and remediation becomes runtime/debug driven instead of type-system driven.', expected_fix: 'Replace Any, AnyObject or AnyHashable usage with a generic parameter, associated type, concrete value object or narrow protocol boundary.', ruleId: 'heuristics.ios.type-erasure.any.ast', code: 'HEURISTICS_IOS_TYPE_ERASURE_ANY_AST', message: 'AST heuristic detected Swift Any/AnyObject/AnyHashable type erasure in production code.' },
@@ -770,6 +779,9 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
770
779
  { 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.' },
771
780
  { 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.' },
772
781
  { 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.' },
782
+ { 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.' },
783
+ { 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().' },
784
+ { platform: 'ios', pathCheck: isSwiftTestPath, excludePaths: [], detect: TextIOS.hasSwiftMakeSUTWithoutMemoryTrackingUsage, locateLines: TextIOS.collectSwiftMakeSUTWithoutMemoryTrackingLines, primaryNode: (lines) => ({ kind: 'call', name: 'makeSUT() without trackForMemoryLeaks', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: trackForMemoryLeaks(sut)', lines }], why: 'A repository-specific XCTest quality contract that uses makeSUT must also register created SUT instances for memory-leak tracking.', impact: 'Tests can keep passing while leaked view models, views or controllers are introduced, weakening the brownfield XCTest quality baseline that Pumuki relies on.', expected_fix: 'Inside makeSUT(), assign the SUT to a local value, call trackForMemoryLeaks(sut), and then return the SUT. Preserve repository-specific makeSUT and memory tracking helpers when they are mandatory.', ruleId: 'heuristics.ios.testing.makesut-without-memory-tracking.ast', code: 'HEURISTICS_IOS_TESTING_MAKESUT_WITHOUT_MEMORY_TRACKING_AST', message: 'AST heuristic detected makeSUT() in an iOS test without trackForMemoryLeaks(sut).' },
773
785
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftNestedIfPyramidUsage, ruleId: 'heuristics.ios.maintainability.nested-if-pyramid.ast', code: 'HEURISTICS_IOS_MAINTAINABILITY_NESTED_IF_PYRAMID_AST', message: 'AST heuristic detected nested if pyramid in iOS code; prefer guard clauses and early returns.' },
774
786
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftProductionCommentUsage, ruleId: 'heuristics.ios.maintainability.comment-trivia.ast', code: 'HEURISTICS_IOS_MAINTAINABILITY_COMMENT_TRIVIA_AST', message: 'AST heuristic detected source comments in iOS production code; prefer self-documenting names and extracted concepts.' },
775
787
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftWarningSuppressionUsage, locateLines: TextIOS.collectSwiftWarningSuppressionLines, primaryNode: (lines) => ({ kind: 'member', name: 'Swift warning or lint suppression directive', lines }), relatedNodes: (lines) => [{ kind: 'member', name: 'replacement: fix the warned code or configure the exception centrally', lines }], why: 'Local warning and lint suppressions hide code-quality failures from the normal compiler/tooling feedback loop.', impact: 'Suppressed diagnostics can turn into future errors or let unsafe production code bypass Pumuki and repository skills without a visible remediation path.', expected_fix: 'Remove local swiftlint/swiftformat/periphery disable directives and #warning markers by fixing the underlying code, or move a justified project-wide exception into policy/config with traceability.', ruleId: 'heuristics.ios.maintainability.warning-suppression.ast', code: 'HEURISTICS_IOS_MAINTAINABILITY_WARNING_SUPPRESSION_AST', message: 'AST heuristic detected local warning/lint suppression in iOS production code; fix the underlying issue instead of hiding diagnostics.' },
@@ -827,6 +839,9 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
827
839
  { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftNonLazyScrollForEachUsage, locateLines: TextIOS.collectSwiftNonLazyScrollForEachLines, primaryNode: (lines) => ({ kind: 'call', name: 'ScrollView non-lazy stack with ForEach', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: LazyVStack or LazyHStack', lines }], why: 'A non-lazy stack inside ScrollView renders all rows eagerly instead of virtualizing collection content.', impact: 'Large lists can degrade performance and the gate must point to the ScrollView/ForEach pair, not the whole file.', expected_fix: 'Replace VStack/HStack under ScrollView with LazyVStack/LazyHStack when rendering collection rows.', ruleId: 'heuristics.ios.swiftui.non-lazy-scroll-foreach.ast', code: 'HEURISTICS_IOS_SWIFTUI_NON_LAZY_SCROLL_FOREACH_AST', message: 'AST heuristic detected ScrollView with a non-lazy stack feeding ForEach; LazyVStack/LazyHStack remain the preferred baseline for large scrollable collections.' },
828
840
  { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftViewBodyObjectCreationUsage, ruleId: 'heuristics.ios.swiftui.body-object-creation.ast', code: 'HEURISTICS_IOS_SWIFTUI_BODY_OBJECT_CREATION_AST', message: 'AST heuristic detected formatter object creation inside SwiftUI body; keep body simple and move expensive objects out of render paths.' },
829
841
  { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftUiImageDataDecodingUsage, ruleId: 'heuristics.ios.swiftui.image-data-decoding.ast', code: 'HEURISTICS_IOS_SWIFTUI_IMAGE_DATA_DECODING_AST', message: 'AST heuristic detected UIImage(data:) in SwiftUI presentation; downsample image data before rendering large images.' },
842
+ { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftUiManualRenderingWithoutImageRendererUsage, locateLines: TextIOS.collectSwiftUiManualRenderingWithoutImageRendererLines, primaryNode: (lines) => ({ kind: 'call', name: 'manual SwiftUI view rendering via UIHostingController', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: ImageRenderer(content:)', lines }], why: 'Manual UIHostingController plus UIGraphicsImageRenderer rendering snapshots SwiftUI imperatively instead of using the native ImageRenderer API.', impact: 'Rendering code becomes UIKit-bound, harder to test, and can drift from SwiftUI rendering semantics while the gate cannot point to the exact rendering node.', expected_fix: 'Replace manual UIHostingController/UIGraphicsImageRenderer/drawHierarchy rendering with ImageRenderer(content:) and read uiImage/cgImage from that renderer.', ruleId: 'heuristics.ios.swiftui.manual-rendering-without-imagerenderer.ast', code: 'HEURISTICS_IOS_SWIFTUI_MANUAL_RENDERING_WITHOUT_IMAGERENDERER_AST', message: 'AST heuristic detected manual SwiftUI view rendering without ImageRenderer.' },
843
+ { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftLargeViewBuilderFunctionUsage, locateLines: TextIOS.collectSwiftLargeViewBuilderFunctionLines, primaryNode: (lines) => ({ kind: 'call', name: 'large @ViewBuilder helper function', lines }), relatedNodes: (lines) => [{ kind: 'class', name: 'replacement: extracted SwiftUI subview', lines }], why: '@ViewBuilder helper functions are intended for small sections; large builders hide view structure inside a function body instead of explicit subviews.', impact: 'Large helper builders become hard to review, diff and test, and whole-file blocks are likely when the gate lacks the exact function node.', expected_fix: 'Extract the large @ViewBuilder function into one or more named SwiftUI subviews or reduce the helper to a small focused section.', ruleId: 'heuristics.ios.swiftui.large-viewbuilder-function.ast', code: 'HEURISTICS_IOS_SWIFTUI_LARGE_VIEWBUILDER_FUNCTION_AST', message: 'AST heuristic detected a large @ViewBuilder helper function; keep @ViewBuilder functions small and extract complex sections into subviews.' },
844
+ { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftAnimationWithoutReduceMotionUsage, locateLines: TextIOS.collectSwiftAnimationWithoutReduceMotionLines, primaryNode: (lines) => ({ kind: 'call', name: 'SwiftUI animation without reduce motion guard', lines }), relatedNodes: (lines) => [{ kind: 'property', name: 'replacement: accessibilityReduceMotion / UIAccessibility.isReduceMotionEnabled', lines }], why: 'SwiftUI animations must respect the system reduce motion preference.', impact: 'Users who reduce motion can still receive animated transitions, making the UI less accessible.', expected_fix: 'Read @Environment(\\.accessibilityReduceMotion) or UIAccessibility.isReduceMotionEnabled and disable or replace animations when reduce motion is enabled.', ruleId: 'heuristics.ios.accessibility.animation-without-reduce-motion.ast', code: 'HEURISTICS_IOS_ACCESSIBILITY_ANIMATION_WITHOUT_REDUCE_MOTION_AST', message: 'AST heuristic detected SwiftUI animation without reduce motion handling.' },
830
845
  { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftUiInlineActionLogicUsage, locateLines: TextIOS.collectSwiftUiInlineActionLogicLines, primaryNode: (lines) => ({ kind: 'call', name: 'SwiftUI Button action with inline logic', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: extracted action method', lines }], why: 'Inline branching or async work in a SwiftUI action makes the view declaration own behavior instead of delegating to a named action.', impact: 'Reviewers and agents cannot remediate a blocked view safely when the finding lacks the exact Button action node.', expected_fix: 'Extract the action body to a named method or view model command and reference that method from Button.', ruleId: 'heuristics.ios.swiftui.inline-action-logic.ast', code: 'HEURISTICS_IOS_SWIFTUI_INLINE_ACTION_LOGIC_AST', message: 'AST heuristic detected inline logic inside a SwiftUI action handler; action handlers should reference methods and keep view declarations focused.' },
831
846
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftNavigationViewUsage, ruleId: 'heuristics.ios.navigation-view.ast', code: 'HEURISTICS_IOS_NAVIGATION_VIEW_AST', message: 'AST heuristic detected NavigationView usage.' },
832
847
  { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftUntypedNavigationLinkDestinationUsage, ruleId: 'heuristics.ios.swiftui.untyped-navigation-link-destination.ast', code: 'HEURISTICS_IOS_SWIFTUI_UNTYPED_NAVIGATION_LINK_DESTINATION_AST', message: 'AST heuristic detected untyped NavigationLink destination usage; prefer NavigationLink(value:) with navigationDestination(for:) for type-safe navigation.' },
@@ -834,6 +849,7 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
834
849
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftCornerRadiusUsage, locateLines: TextIOS.collectSwiftCornerRadiusLines, primaryNode: (lines) => ({ kind: 'call', name: 'SwiftUI cornerRadius modifier', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: clipShape with rounded rectangle style', lines }], why: 'cornerRadius is a legacy shape shortcut that hides the shape semantics used by modern SwiftUI styling.', impact: 'The fix is local, but without line evidence the gate forces unsafe file-wide remediation.', expected_fix: 'Replace .cornerRadius(...) with .clipShape(.rect(cornerRadius: ...)) or a named reusable shape/style token.', ruleId: 'heuristics.ios.corner-radius.ast', code: 'HEURISTICS_IOS_CORNER_RADIUS_AST', message: 'AST heuristic detected cornerRadius usage.' },
835
850
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftTabItemUsage, ruleId: 'heuristics.ios.tab-item.ast', code: 'HEURISTICS_IOS_TAB_ITEM_AST', message: 'AST heuristic detected tabItem usage.' },
836
851
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftOnTapGestureUsage, ruleId: 'heuristics.ios.on-tap-gesture.ast', code: 'HEURISTICS_IOS_ON_TAP_GESTURE_AST', message: 'AST heuristic detected onTapGesture usage where Button may be preferred.' },
852
+ { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftOnTapGestureWithoutButtonTraitUsage, locateLines: TextIOS.collectSwiftOnTapGestureWithoutButtonTraitLines, primaryNode: (lines) => ({ kind: 'call', name: 'SwiftUI onTapGesture without button accessibility trait', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: Button or accessibilityAddTraits(.isButton)', lines }], why: 'A tappable SwiftUI element that is not a Button must expose button semantics to assistive technologies.', impact: 'VoiceOver users can encounter an interactive element without the expected button trait, and the gate must point to the exact tap modifier.', expected_fix: 'Prefer Button for interactive controls. If onTapGesture is required, add .accessibilityAddTraits(.isButton) to the same element.', ruleId: 'heuristics.ios.accessibility.on-tap-without-button-trait.ast', code: 'HEURISTICS_IOS_ACCESSIBILITY_ON_TAP_WITHOUT_BUTTON_TRAIT_AST', message: 'AST heuristic detected onTapGesture without button accessibility trait.' },
837
853
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftStringFormatUsage, ruleId: 'heuristics.ios.string-format.ast', code: 'HEURISTICS_IOS_STRING_FORMAT_AST', message: 'AST heuristic detected String(format:) usage.' },
838
854
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftScrollViewShowsIndicatorsUsage, ruleId: 'heuristics.ios.scrollview-shows-indicators.ast', code: 'HEURISTICS_IOS_SCROLLVIEW_SHOWS_INDICATORS_AST', message: 'AST heuristic detected ScrollView(showsIndicators: false) usage.' },
839
855
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftSheetIsPresentedUsage, ruleId: 'heuristics.ios.sheet-is-presented.ast', code: 'HEURISTICS_IOS_SHEET_IS_PRESENTED_AST', message: 'AST heuristic detected .sheet(isPresented:) usage where .sheet(item:) may be preferred.' },
@@ -3,7 +3,7 @@ import test from 'node:test';
3
3
  import { iosRules } from './ios';
4
4
 
5
5
  test('iosRules define reglas heurísticas locked para plataforma ios', () => {
6
- assert.equal(iosRules.length, 106);
6
+ assert.equal(iosRules.length, 117);
7
7
 
8
8
  const ids = iosRules.map((rule) => rule.id);
9
9
  assert.deepEqual(ids, [
@@ -28,6 +28,9 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
28
28
  'heuristics.ios.memory.strong-delegate.ast',
29
29
  'heuristics.ios.memory.strong-self-escaping-closure.ast',
30
30
  'heuristics.ios.memory.unowned-self-capture.ast',
31
+ 'heuristics.ios.memory.manual-management.ast',
32
+ 'heuristics.ios.testing.makesut-without-memory-tracking.ast',
33
+ 'heuristics.ios.testing.direct-sut-instantiation-without-makesut.ast',
31
34
  'heuristics.ios.maintainability.nested-if-pyramid.ast',
32
35
  'heuristics.ios.maintainability.comment-trivia.ast',
33
36
  'heuristics.ios.maintainability.warning-suppression.ast',
@@ -46,6 +49,10 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
46
49
  'heuristics.ios.dependencies.cocoapods.ast',
47
50
  'heuristics.ios.dependencies.carthage.ast',
48
51
  'heuristics.ios.dependencies.swiftpm-branch-dependency.ast',
52
+ 'heuristics.ios.dependencies.swift-tools-version-below-6-2.ast',
53
+ 'heuristics.ios.concurrency.strict-concurrency-below-complete.ast',
54
+ 'heuristics.ios.naming.non-pascal-case-type.ast',
55
+ 'heuristics.ios.uikit.cell-without-reuse.ast',
49
56
  'heuristics.ios.security.userdefaults-sensitive-data.ast',
50
57
  'heuristics.ios.security.insecure-transport.ast',
51
58
  'heuristics.ios.localization.localizable-strings.ast',
@@ -88,6 +95,9 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
88
95
  'heuristics.ios.swiftui.non-lazy-scroll-foreach.ast',
89
96
  'heuristics.ios.swiftui.body-object-creation.ast',
90
97
  'heuristics.ios.swiftui.image-data-decoding.ast',
98
+ 'heuristics.ios.swiftui.manual-rendering-without-imagerenderer.ast',
99
+ 'heuristics.ios.swiftui.large-viewbuilder-function.ast',
100
+ 'heuristics.ios.accessibility.animation-without-reduce-motion.ast',
91
101
  'heuristics.ios.swiftui.inline-action-logic.ast',
92
102
  'heuristics.ios.navigation-view.ast',
93
103
  'heuristics.ios.swiftui.untyped-navigation-link-destination.ast',
@@ -95,6 +105,7 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
95
105
  'heuristics.ios.corner-radius.ast',
96
106
  'heuristics.ios.tab-item.ast',
97
107
  'heuristics.ios.on-tap-gesture.ast',
108
+ 'heuristics.ios.accessibility.on-tap-without-button-trait.ast',
98
109
  'heuristics.ios.string-format.ast',
99
110
  'heuristics.ios.scrollview-shows-indicators.ast',
100
111
  'heuristics.ios.sheet-is-presented.ast',
@@ -144,6 +155,50 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
144
155
  byId.get('heuristics.ios.memory.unowned-self-capture.ast')?.then.code,
145
156
  'HEURISTICS_IOS_MEMORY_UNOWNED_SELF_CAPTURE_AST'
146
157
  );
158
+ assert.equal(
159
+ byId.get('heuristics.ios.memory.manual-management.ast')?.then.code,
160
+ 'HEURISTICS_IOS_MEMORY_MANUAL_MANAGEMENT_AST'
161
+ );
162
+ assert.equal(
163
+ byId.get('heuristics.ios.dependencies.swift-tools-version-below-6-2.ast')?.then.code,
164
+ 'HEURISTICS_IOS_DEPENDENCIES_SWIFT_TOOLS_VERSION_BELOW_6_2_AST'
165
+ );
166
+ assert.equal(
167
+ byId.get('heuristics.ios.concurrency.strict-concurrency-below-complete.ast')?.then.code,
168
+ 'HEURISTICS_IOS_CONCURRENCY_STRICT_CONCURRENCY_BELOW_COMPLETE_AST'
169
+ );
170
+ assert.equal(
171
+ byId.get('heuristics.ios.naming.non-pascal-case-type.ast')?.then.code,
172
+ 'HEURISTICS_IOS_NAMING_NON_PASCAL_CASE_TYPE_AST'
173
+ );
174
+ assert.equal(
175
+ byId.get('heuristics.ios.uikit.cell-without-reuse.ast')?.then.code,
176
+ 'HEURISTICS_IOS_UIKIT_CELL_WITHOUT_REUSE_AST'
177
+ );
178
+ assert.equal(
179
+ byId.get('heuristics.ios.testing.makesut-without-memory-tracking.ast')?.then.code,
180
+ 'HEURISTICS_IOS_TESTING_MAKESUT_WITHOUT_MEMORY_TRACKING_AST'
181
+ );
182
+ assert.equal(
183
+ byId.get('heuristics.ios.testing.direct-sut-instantiation-without-makesut.ast')?.then.code,
184
+ 'HEURISTICS_IOS_TESTING_DIRECT_SUT_INSTANTIATION_WITHOUT_MAKESUT_AST'
185
+ );
186
+ assert.equal(
187
+ byId.get('heuristics.ios.swiftui.manual-rendering-without-imagerenderer.ast')?.then.code,
188
+ 'HEURISTICS_IOS_SWIFTUI_MANUAL_RENDERING_WITHOUT_IMAGERENDERER_AST'
189
+ );
190
+ assert.equal(
191
+ byId.get('heuristics.ios.swiftui.large-viewbuilder-function.ast')?.then.code,
192
+ 'HEURISTICS_IOS_SWIFTUI_LARGE_VIEWBUILDER_FUNCTION_AST'
193
+ );
194
+ assert.equal(
195
+ byId.get('heuristics.ios.accessibility.animation-without-reduce-motion.ast')?.then.code,
196
+ 'HEURISTICS_IOS_ACCESSIBILITY_ANIMATION_WITHOUT_REDUCE_MOTION_AST'
197
+ );
198
+ assert.equal(
199
+ byId.get('heuristics.ios.accessibility.on-tap-without-button-trait.ast')?.then.code,
200
+ 'HEURISTICS_IOS_ACCESSIBILITY_ON_TAP_WITHOUT_BUTTON_TRAIT_AST'
201
+ );
147
202
  assert.equal(
148
203
  byId.get('heuristics.ios.maintainability.nested-if-pyramid.ast')?.then.code,
149
204
  'HEURISTICS_IOS_MAINTAINABILITY_NESTED_IF_PYRAMID_AST'