pumuki 6.3.306 → 6.3.308
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/CHANGELOG.md +6 -0
- package/core/facts/detectors/text/ios.test.ts +495 -0
- package/core/facts/detectors/text/ios.ts +492 -0
- package/core/facts/extractHeuristicFacts.ts +17 -4
- package/core/rules/presets/heuristics/ios.test.ts +58 -1
- package/core/rules/presets/heuristics/ios.ts +239 -0
- package/integrations/config/skillsDetectorRegistry.ts +94 -0
- package/integrations/evidence/buildEvidence.ts +46 -0
- package/integrations/lifecycle/adapter.templates.json +50 -1
- package/integrations/lifecycle/doctor.ts +41 -5
- package/integrations/lifecycle/preWriteLease.ts +5 -1
- package/package.json +1 -1
- package/scripts/framework-menu-matrix-canary-evidence.ts +42 -1
- package/scripts/framework-menu-matrix-canary-lib.ts +25 -2
- package/scripts/framework-menu-matrix-canary-scenario.ts +3 -1
- package/scripts/framework-menu-matrix-canary-types.ts +6 -1
- package/scripts/framework-menu-system-notifications-macos-dialog-payload.ts +2 -1
- package/scripts/framework-menu-system-notifications-payloads-blocked.ts +76 -2
- package/scripts/framework-menu-system-notifications-payloads.ts +1 -0
- package/skills.lock.json +1 -1
|
@@ -736,8 +736,8 @@ type TextDetectorRegistryEntry = {
|
|
|
736
736
|
readonly platform: 'ios' | 'android';
|
|
737
737
|
readonly pathCheck: (path: string) => boolean;
|
|
738
738
|
readonly excludePaths: ReadonlyArray<(path: string) => boolean>;
|
|
739
|
-
readonly detect: (content: string) => boolean;
|
|
740
|
-
readonly locateLines?: (content: string) => readonly number[];
|
|
739
|
+
readonly detect: (content: string, path: string) => boolean;
|
|
740
|
+
readonly locateLines?: (content: string, path: string) => readonly number[];
|
|
741
741
|
readonly primaryNode?: (lines: readonly number[]) => HeuristicFact['primary_node'];
|
|
742
742
|
readonly relatedNodes?: (lines: readonly number[]) => HeuristicFact['related_nodes'];
|
|
743
743
|
readonly why?: string;
|
|
@@ -754,8 +754,15 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
|
|
|
754
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.' },
|
|
755
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
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: isIOSSwiftPackageManifestPath, excludePaths: [], detect: TextIOS.hasSwiftPackageDefaultIsolationNotMainActorUsage, locateLines: TextIOS.collectSwiftPackageDefaultIsolationNotMainActorLines, primaryNode: (lines) => ({ kind: 'call', name: 'SwiftPM .defaultIsolation not MainActor', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: .defaultIsolation(MainActor.self)', lines }], why: 'SwiftPM targets with an explicit non-MainActor default isolation do not match the iOS presentation baseline expected by the concurrency skills.', impact: 'SwiftUI and app state can compile under a module isolation model that differs from the reviewed iOS baseline, weakening actor-boundary enforcement.', expected_fix: 'Use .defaultIsolation(MainActor.self) for UI-heavy iOS modules, or isolate exceptional modules explicitly with documented actor boundaries.', ruleId: 'heuristics.ios.concurrency.swiftpm-default-isolation-not-mainactor.ast', code: 'HEURISTICS_IOS_CONCURRENCY_SWIFTPM_DEFAULT_ISOLATION_NOT_MAINACTOR_AST', message: 'AST heuristic detected Package.swift .defaultIsolation not set to MainActor.' },
|
|
758
|
+
{ platform: 'ios', pathCheck: isIOSSwiftPackageManifestPath, excludePaths: [], detect: TextIOS.hasSwiftPackageStrictConcurrencyBelowCompleteUsage, locateLines: TextIOS.collectSwiftPackageStrictConcurrencyBelowCompleteLines, primaryNode: (lines) => ({ kind: 'call', name: 'SwiftPM StrictConcurrency below complete', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: complete strict concurrency baseline', lines }], why: 'SwiftPM strict concurrency settings below complete can leave sendability and actor diagnostics unenforced in iOS packages.', impact: 'Minimal or targeted strict concurrency in Package.swift weakens the same safety baseline that Xcode build settings must enforce.', expected_fix: 'Remove targeted/minimal StrictConcurrency overrides and run the package under the complete Swift concurrency baseline expected by the repo.', ruleId: 'heuristics.ios.concurrency.swiftpm-strict-concurrency-below-complete.ast', code: 'HEURISTICS_IOS_CONCURRENCY_SWIFTPM_STRICT_CONCURRENCY_BELOW_COMPLETE_AST', message: 'AST heuristic detected Package.swift StrictConcurrency below complete.' },
|
|
757
759
|
{ 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.' },
|
|
760
|
+
{ platform: 'ios', pathCheck: isIOSXcodeProjectFilePath, excludePaths: [], detect: TextIOS.hasSwiftDefaultActorIsolationNotMainActorUsage, locateLines: TextIOS.collectSwiftDefaultActorIsolationNotMainActorLines, primaryNode: (lines) => ({ kind: 'property', name: 'SWIFT_DEFAULT_ACTOR_ISOLATION not MainActor', lines }), relatedNodes: (lines) => [{ kind: 'property', name: 'replacement: SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor', lines }], why: 'The iOS concurrency skill requires projects to validate default actor isolation instead of leaving UI-heavy code under an unsafe or ambiguous isolation baseline.', impact: 'A non-MainActor default isolation can let SwiftUI and presentation state cross actor boundaries without the project-wide protection expected by the skills contract.', expected_fix: 'Set SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor for the affected iOS build configuration, or isolate exceptional modules explicitly with documented actor boundaries.', ruleId: 'heuristics.ios.concurrency.default-actor-isolation-not-mainactor.ast', code: 'HEURISTICS_IOS_CONCURRENCY_DEFAULT_ACTOR_ISOLATION_NOT_MAINACTOR_AST', message: 'AST heuristic detected SWIFT_DEFAULT_ACTOR_ISOLATION not set to MainActor in an iOS Xcode project.' },
|
|
758
761
|
{ 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.' },
|
|
762
|
+
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftCrossFeatureImportUsage, locateLines: TextIOS.collectSwiftCrossFeatureImportLines, primaryNode: (lines) => ({ kind: 'member', name: 'cross-feature Swift import', lines }), relatedNodes: (lines) => [{ kind: 'member', name: 'replacement: SharedKernel / routing contract / local feature boundary', lines }], why: 'Feature-first iOS modules must not import sibling features directly; bounded contexts communicate through shared kernel contracts, navigation routes or application-level orchestration.', impact: 'A direct feature-to-feature import couples release cadence, state ownership and navigation behavior across bounded contexts, making product slices harder to isolate and review.', expected_fix: 'Move the shared type to SharedKernel, expose a narrow route/command contract, or orchestrate the collaboration from an application/root layer instead of importing a sibling feature.', ruleId: 'heuristics.ios.architecture.cross-feature-import.ast', code: 'HEURISTICS_IOS_ARCHITECTURE_CROSS_FEATURE_IMPORT_AST', message: 'AST heuristic detected a Swift feature importing another feature module directly.' },
|
|
763
|
+
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftLayerDirectionViolationUsage, locateLines: TextIOS.collectSwiftLayerDirectionViolationLines, primaryNode: (lines) => ({ kind: 'member', name: 'forbidden import for Clean Architecture layer', lines }), relatedNodes: (lines) => [{ kind: 'member', name: 'replacement: move dependency to allowed layer or depend on protocol/value object', lines }], why: 'Clean Architecture iOS layers must point inward: Domain cannot import UI, persistence or concrete networking frameworks; Application cannot import UI or concrete infrastructure; Presentation cannot import persistence/network implementation frameworks directly.', impact: 'Layer direction violations make feature slices depend on concrete frameworks instead of domain/application contracts, increasing coupling and making remediation unsafe across bounded contexts.', expected_fix: 'Move framework-specific code to Infrastructure or Presentation as appropriate, expose a narrow protocol/value object in Domain/Application, and inject the implementation from the composition root.', ruleId: 'heuristics.ios.architecture.layer-direction-violation.ast', code: 'HEURISTICS_IOS_ARCHITECTURE_LAYER_DIRECTION_VIOLATION_AST', message: 'AST heuristic detected an import that violates Clean Architecture layer direction in iOS code.' },
|
|
764
|
+
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftExcessivePublicApiUsage, locateLines: TextIOS.collectSwiftExcessivePublicApiLines, primaryNode: (lines) => ({ kind: 'class', name: 'excessive public/open Swift API surface', lines }), relatedNodes: (lines) => [{ kind: 'property', name: 'replacement: internal/private/fileprivate visibility by default', lines }], why: 'App-target Swift code should expose only the minimum API surface; public/open declarations in app implementation files usually bypass module encapsulation.', impact: 'Excessive public APIs make implementation details part of the external contract, increasing coupling and weakening review of atomic feature slices.', expected_fix: 'Remove public/open from app implementation declarations unless the file is a real exported SDK/module surface. Prefer internal by default and private/fileprivate for local implementation details.', ruleId: 'heuristics.ios.architecture.excessive-public-api.ast', code: 'HEURISTICS_IOS_ARCHITECTURE_EXCESSIVE_PUBLIC_API_AST', message: 'AST heuristic detected excessive public/open Swift API surface in iOS app code.' },
|
|
765
|
+
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftEndpointEnumUsage, locateLines: TextIOS.collectSwiftEndpointEnumLines, primaryNode: (lines) => ({ kind: 'class', name: 'Swift APIEndpoint enum declaration', lines }), relatedNodes: (lines) => [{ kind: 'class', name: 'replacement: struct APIEndpoint: Sendable with static factories', lines }], why: 'Endpoint catalogs modeled as enums force the same type to change for every new backend route, which is the opposite of a data-driven OCP endpoint model.', impact: 'Feature work accumulates central enum edits and switch/case drift instead of adding endpoint values locally with explicit path, method, query and body data.', expected_fix: 'Replace endpoint enums with a Sendable APIEndpoint struct/value object and feature-local static factory methods that return configured endpoint instances.', ruleId: 'heuristics.ios.networking.endpoint-enum-ocp.ast', code: 'HEURISTICS_IOS_NETWORKING_ENDPOINT_ENUM_OCP_AST', message: 'AST heuristic detected API endpoint modeled as enum; use a data-driven APIEndpoint struct to preserve OCP.' },
|
|
759
766
|
{ 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.' },
|
|
760
767
|
{ 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.' },
|
|
761
768
|
{ 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.' },
|
|
@@ -771,6 +778,7 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
|
|
|
771
778
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftOperationQueueUsage, ruleId: 'heuristics.ios.operation-queue.ast', code: 'HEURISTICS_IOS_OPERATION_QUEUE_AST', message: 'AST heuristic detected OperationQueue usage.' },
|
|
772
779
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftTaskDetachedUsage, ruleId: 'heuristics.ios.task-detached.ast', code: 'HEURISTICS_IOS_TASK_DETACHED_AST', message: 'AST heuristic detected Task.detached usage.' },
|
|
773
780
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftAsyncWithoutAwaitUsage, ruleId: 'heuristics.ios.concurrency.async-without-await.ast', code: 'HEURISTICS_IOS_CONCURRENCY_ASYNC_WITHOUT_AWAIT_AST', message: 'AST heuristic detected a private async function without await; remove async unless a protocol/override boundary requires it.' },
|
|
781
|
+
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftDummyAwaitUsage, locateLines: TextIOS.collectSwiftDummyAwaitLines, primaryNode: (lines) => ({ kind: 'call', name: 'Swift dummy await', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: remove async or justify protocol/override suppression', lines }], why: 'Dummy awaits such as await Task.yield() or zero-duration Task.sleep mask async_without_await instead of modelling real suspension.', impact: 'Production code gains meaningless suspension points and hides the real API design problem from SwiftLint and Pumuki.', expected_fix: 'Remove the dummy await. If async is not required, remove async; if a protocol/override requires it, keep the signature and use a narrow documented suppression instead.', ruleId: 'heuristics.ios.concurrency.dummy-await.ast', code: 'HEURISTICS_IOS_CONCURRENCY_DUMMY_AWAIT_AST', message: 'AST heuristic detected a dummy await used as a Swift concurrency lint workaround.' },
|
|
774
782
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftEmptyCatchUsage, ruleId: 'heuristics.ios.error.empty-catch.ast', code: 'HEURISTICS_IOS_ERROR_EMPTY_CATCH_AST', message: 'AST heuristic detected an empty Swift catch block; handle, log, or propagate the error.' },
|
|
775
783
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftNSErrorThrowUsage, locateLines: TextIOS.collectSwiftNSErrorThrowLines, primaryNode: (lines) => ({ kind: 'call', name: 'throw NSError(...)', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: typed Swift Error enum case', lines }], why: 'NSError throws bypass the typed Swift error boundary that should model network, domain or infrastructure failures explicitly.', impact: 'Callers receive an untyped Foundation error instead of a remediable enum case, making recovery, tests and user-facing handling less deterministic.', expected_fix: 'Define a domain-specific Error enum such as NetworkError or AppError and throw typed cases instead of constructing NSError directly.', ruleId: 'heuristics.ios.error.nserror-throw.ast', code: 'HEURISTICS_IOS_ERROR_NSERROR_THROW_AST', message: 'AST heuristic detected throw NSError(...) in iOS production code; use typed Swift Error enums such as NetworkError or AppError.' },
|
|
776
784
|
{ 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.' },
|
|
@@ -808,6 +816,7 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
|
|
|
808
816
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftFixedFontSizeUsage, locateLines: TextIOS.collectSwiftFixedFontSizeLines, primaryNode: (lines) => ({ kind: 'call', name: 'fixed font size API', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: Dynamic Type semantic text style or scaled metric', lines }], why: 'Fixed font sizes bypass Dynamic Type unless explicitly scaled through the text system.', impact: 'Accessibility regressions become hard to remediate if the gate reports only the file instead of the exact font call.', expected_fix: 'Use semantic SwiftUI text styles such as .headline/.body, Font.TextStyle, or UIFontMetrics/scaled metrics when a custom size is required.', ruleId: 'heuristics.ios.accessibility.fixed-font-size.ast', code: 'HEURISTICS_IOS_ACCESSIBILITY_FIXED_FONT_SIZE_AST', message: 'AST heuristic detected fixed font sizing in iOS production code; Dynamic Type semantic text styles remain the preferred baseline.' },
|
|
809
817
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftPhysicalTextAlignmentUsage, ruleId: 'heuristics.ios.localization.physical-text-alignment.ast', code: 'HEURISTICS_IOS_LOCALIZATION_PHYSICAL_TEXT_ALIGNMENT_AST', message: 'AST heuristic detected physical left/right text alignment in iOS production code; leading/trailing remain the preferred RTL-safe baseline.' },
|
|
810
818
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftMainThreadBlockingSleepUsage, ruleId: 'heuristics.ios.performance.blocking-sleep.ast', code: 'HEURISTICS_IOS_PERFORMANCE_BLOCKING_SLEEP_AST', message: 'AST heuristic detected blocking sleep usage in iOS production code; async clocks, suspension or cancellable scheduling remain the preferred baseline.' },
|
|
819
|
+
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftThreadCentricDebuggingUsage, locateLines: TextIOS.collectSwiftThreadCentricDebuggingLines, primaryNode: (lines) => ({ kind: 'call', name: 'Swift thread-centric debugging API', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: isolation domain / Instruments / debugger', lines }], why: 'Swift Concurrency tasks are not bound to a fixed thread; production code should reason about actor isolation and structured concurrency instead of Thread.current or pthread identity.', impact: 'Thread-centric checks can produce misleading diagnostics, stale assumptions and Swift 6 async-context compile failures.', expected_fix: 'Remove Thread.current, Thread.isMainThread and pthread thread identity checks from production code; use @MainActor/custom actors, task-local context, Instruments or debugger tooling instead.', ruleId: 'heuristics.ios.concurrency.thread-centric-debugging.ast', code: 'HEURISTICS_IOS_CONCURRENCY_THREAD_CENTRIC_DEBUGGING_AST', message: 'AST heuristic detected thread-centric debugging in Swift Concurrency code.' },
|
|
811
820
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftIconOnlyControlWithoutAccessibilityLabelUsage, locateLines: TextIOS.collectSwiftIconOnlyControlWithoutAccessibilityLabelLines, primaryNode: (lines) => ({ kind: 'call', name: 'icon-only SwiftUI control', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: add accessibilityLabel to the control', lines }], why: 'Icon-only controls have no accessible name unless the label is supplied explicitly.', impact: 'VoiceOver and UI automation users cannot identify or operate the control reliably when the gate reports only the file.', expected_fix: 'Add .accessibilityLabel(...) to the icon-only control or replace the control with a visible text label.', ruleId: 'heuristics.ios.accessibility.icon-only-control-label.ast', code: 'HEURISTICS_IOS_ACCESSIBILITY_ICON_ONLY_CONTROL_LABEL_AST', message: 'AST heuristic detected an icon-only SwiftUI control without accessibilityLabel; explicit accessible labels remain the preferred baseline.' },
|
|
812
821
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftInteractiveControlWithoutAccessibilityIdentifierUsage, locateLines: TextIOS.collectSwiftInteractiveControlWithoutAccessibilityIdentifierLines, ruleId: 'heuristics.ios.accessibility.missing-accessibility-identifier.ast', code: 'HEURISTICS_IOS_ACCESSIBILITY_MISSING_ACCESSIBILITY_IDENTIFIER_AST', message: 'AST heuristic detected an interactive SwiftUI control without accessibilityIdentifier; stable identifiers are required for UI automation and traceability.' },
|
|
813
822
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftBindableMissingForObservableBindingUsage, locateLines: TextIOS.collectSwiftBindableMissingForObservableBindingUsageLines, ruleId: 'heuristics.ios.swiftui.missing-bindable-observable-binding.ast', code: 'HEURISTICS_IOS_SWIFTUI_MISSING_BINDABLE_OBSERVABLE_BINDING_AST', message: 'AST heuristic detected an injected @Observable used as a binding without @Bindable.' },
|
|
@@ -815,6 +824,7 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
|
|
|
815
824
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftPreconcurrencyUsage, ruleId: 'heuristics.ios.preconcurrency.ast', code: 'HEURISTICS_IOS_PRECONCURRENCY_AST', message: 'AST heuristic detected @preconcurrency usage.' },
|
|
816
825
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftNonisolatedUnsafeUsage, ruleId: 'heuristics.ios.nonisolated-unsafe.ast', code: 'HEURISTICS_IOS_NONISOLATED_UNSAFE_AST', message: 'AST heuristic detected nonisolated(unsafe) usage.' },
|
|
817
826
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftAssumeIsolatedUsage, ruleId: 'heuristics.ios.assume-isolated.ast', code: 'HEURISTICS_IOS_ASSUME_ISOLATED_AST', message: 'AST heuristic detected assumeIsolated usage.' },
|
|
827
|
+
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftLongAsyncOperationWithoutCancellationCheckUsage, locateLines: TextIOS.collectSwiftLongAsyncOperationWithoutCancellationCheckLines, primaryNode: (lines) => ({ kind: 'call', name: 'long async loop without Task cancellation check', lines }), relatedNodes: (lines) => [{ kind: 'member', name: 'replacement: Task.isCancelled or Task.checkCancellation() in loop body', lines }], why: 'Long-running async loops must cooperate with Swift structured cancellation instead of continuing work after the parent task has been cancelled.', impact: 'Cancelled screens, sync jobs or background flows can keep doing network, persistence or CPU work, causing stale UI updates, wasted resources and flaky tests.', expected_fix: 'Check Task.isCancelled or call try Task.checkCancellation() inside long-running async loops, then return or throw CancellationError through the current async boundary.', ruleId: 'heuristics.ios.concurrency.long-task-without-cancellation-check.ast', code: 'HEURISTICS_IOS_CONCURRENCY_LONG_TASK_WITHOUT_CANCELLATION_CHECK_AST', message: 'AST heuristic detected a long async operation without cooperative Task cancellation checks.' },
|
|
818
828
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftObservableObjectUsage, ruleId: 'heuristics.ios.observable-object.ast', code: 'HEURISTICS_IOS_OBSERVABLE_OBJECT_AST', message: 'AST heuristic detected ObservableObject usage.' },
|
|
819
829
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftLegacyPreviewProviderUsage, ruleId: 'heuristics.ios.swiftui.legacy-preview-provider.ast', code: 'HEURISTICS_IOS_SWIFTUI_LEGACY_PREVIEW_PROVIDER_AST', message: 'AST heuristic detected PreviewProvider usage; use #Preview macros for modern SwiftUI previews.' },
|
|
820
830
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftLegacySwiftUiObservableWrapperUsage, ruleId: 'heuristics.ios.legacy-swiftui-observable-wrapper.ast', code: 'HEURISTICS_IOS_LEGACY_SWIFTUI_OBSERVABLE_WRAPPER_AST', message: 'AST heuristic detected @StateObject/@ObservedObject usage in a modern SwiftUI path.' },
|
|
@@ -851,6 +861,8 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
|
|
|
851
861
|
{ 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.' },
|
|
852
862
|
{ 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.' },
|
|
853
863
|
{ 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.' },
|
|
864
|
+
{ platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftGlassInteractiveOnStaticElementUsage, locateLines: TextIOS.collectSwiftGlassInteractiveOnStaticElementLines, primaryNode: (lines) => ({ kind: 'call', name: 'SwiftUI glassEffect interactive style on static element', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: remove interactive() or make the element tappable/focusable', lines }], why: 'Liquid Glass interactive styling must be reserved for elements that actually respond to input or focus.', impact: 'Static content can look tappable or focusable, creating misleading affordances and accessibility drift.', expected_fix: 'Remove .interactive() from decorative/static glass, or wrap the element in Button/NavigationLink, add a real action, focusability, or button accessibility semantics.', ruleId: 'heuristics.ios.swiftui.glass-interactive-static-element.ast', code: 'HEURISTICS_IOS_SWIFTUI_GLASS_INTERACTIVE_STATIC_ELEMENT_AST', message: 'AST heuristic detected Liquid Glass .interactive() on a static SwiftUI element.' },
|
|
865
|
+
{ platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftGlassEffectIDWithoutNamespaceUsage, locateLines: TextIOS.collectSwiftGlassEffectIDWithoutNamespaceLines, primaryNode: (lines) => ({ kind: 'call', name: 'SwiftUI glassEffectID without Namespace', lines }), relatedNodes: (lines) => [{ kind: 'property', name: 'replacement: @Namespace / Namespace.ID', lines }], why: 'glassEffectID participates in morphing transitions and must be tied to explicit SwiftUI Namespace ownership.', impact: 'Morphing Liquid Glass transitions can become unstable or misleading when namespace ownership is implicit or missing.', expected_fix: 'Declare @Namespace in the owning view or pass Namespace.ID explicitly to child views before using glassEffectID.', ruleId: 'heuristics.ios.swiftui.glasseffectid-without-namespace.ast', code: 'HEURISTICS_IOS_SWIFTUI_GLASSEFFECTID_WITHOUT_NAMESPACE_AST', message: 'AST heuristic detected glassEffectID without @Namespace or Namespace.ID.' },
|
|
854
866
|
{ 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.' },
|
|
855
867
|
{ 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.' },
|
|
856
868
|
{ 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.' },
|
|
@@ -864,6 +876,7 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
|
|
|
864
876
|
{ platform: 'ios', pathCheck: isIOSSwiftTestPath, excludePaths: [], detect: TextIOS.hasSwiftLegacyExpectationDescriptionUsage, locateLines: TextIOS.collectSwiftLegacyExpectationDescriptionLines, primaryNode: (lines) => ({ kind: 'call', name: 'legacy XCTest expectation(description:) call', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: await confirmation or awaited fulfillment flow', lines }], why: 'Legacy expectation(description:) scaffolding keeps async tests coupled to XCTest-style callbacks instead of expressing confirmation intent directly.', impact: 'Tests can remain harder to read and migrate because the assertion flow is split between expectation creation, callback fulfillment and a later wait.', expected_fix: 'Prefer await confirmation(...) for callback confirmation, or pair legacy expectations with await fulfillment(of:timeout:) when the target still requires XCTest compatibility.', ruleId: 'heuristics.ios.testing.legacy-expectation-description.ast', code: 'HEURISTICS_IOS_TESTING_LEGACY_EXPECTATION_DESCRIPTION_AST', message: 'AST heuristic detected expectation(description:) usage without modern fulfillment/confirmation flow.' },
|
|
865
877
|
{ platform: 'ios', pathCheck: isIOSSwiftTestPath, excludePaths: [], detect: TextIOS.hasSwiftMixedTestingFrameworksUsage, locateLines: TextIOS.collectSwiftMixedTestingFrameworkLines, primaryNode: (lines) => ({ kind: 'class', name: 'mixed XCTestCase and Swift Testing suite', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: isolate XCTest compatibility from Swift Testing suites', lines }], why: 'Mixing XCTestCase and Swift Testing markers in the same file makes the test contract ambiguous and hides whether the target is legacy XCTest compatibility or modern Swift Testing.', impact: 'Migration work becomes harder to audit because one file can carry two lifecycle models, assertion styles and setup conventions at once.', expected_fix: 'Split XCTest compatibility tests and Swift Testing suites into separate files, or migrate the legacy XCTestCase suite fully to import Testing with @Suite/@Test and #expect/#require.', ruleId: 'heuristics.ios.testing.mixed-frameworks.ast', code: 'HEURISTICS_IOS_TESTING_MIXED_FRAMEWORKS_AST', message: 'AST heuristic detected XCTestCase and Swift Testing markers mixed in the same test file without explicit compatibility reason.' },
|
|
866
878
|
{ platform: 'ios', pathCheck: isIOSSwiftTestPath, excludePaths: [], detect: TextIOS.hasSwiftQuickNimbleUsage, locateLines: TextIOS.collectSwiftQuickNimbleLines, primaryNode: (lines) => ({ kind: 'class', name: 'QuickSpec legacy test suite', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: Swift Testing @Suite/@Test with #expect/#require', lines }], why: 'Quick and Nimble introduce a third-party BDD lifecycle and matcher vocabulary that diverges from the native Swift Testing contract expected for new tests.', impact: 'New tests become harder to migrate, audit and run consistently because they depend on legacy DSL hooks instead of Swift Testing suites and assertions.', expected_fix: 'For new tests, replace QuickSpec/describe/context/it/expect with native Swift Testing @Suite/@Test and #expect/#require. Keep existing Quick/Nimble only as explicit brownfield legacy until migrated.', ruleId: 'heuristics.ios.testing.quick-nimble.ast', code: 'HEURISTICS_IOS_TESTING_QUICK_NIMBLE_AST', message: 'AST heuristic detected Quick/Nimble legacy test nodes; use native Swift Testing for new test code.' },
|
|
879
|
+
{ platform: 'ios', pathCheck: isIOSSwiftTestPath, excludePaths: [], detect: TextIOS.hasSwiftThirdPartyUiTestFrameworkUsage, locateLines: TextIOS.collectSwiftThirdPartyUiTestFrameworkLines, primaryNode: (lines) => ({ kind: 'call', name: 'third-party UI test framework node', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: native XCUITest XCUIApplication/XCUIElement flow', lines }], why: 'The iOS skill baseline requires native XCUITest for UI testing instead of third-party UI automation runtimes.', impact: 'Third-party UI test DSLs create runner-specific waits, matchers and actions that Pumuki cannot enforce with the native XCTest/XCUITest contract.', expected_fix: 'Replace KIF, EarlGrey, Detox, Appium or Calabash test nodes with native XCUITest using XCUIApplication, XCUIElement queries, accessibility identifiers and repository-approved wait helpers.', ruleId: 'heuristics.ios.testing.third-party-ui-test-framework.ast', code: 'HEURISTICS_IOS_TESTING_THIRD_PARTY_UI_TEST_FRAMEWORK_AST', message: 'AST heuristic detected third-party iOS UI test framework usage; use native XCUITest.' },
|
|
867
880
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftNSManagedObjectBoundaryUsage, ruleId: 'heuristics.ios.core-data.nsmanagedobject-boundary.ast', code: 'HEURISTICS_IOS_CORE_DATA_NSMANAGEDOBJECT_BOUNDARY_AST', message: 'AST heuristic detected NSManagedObject in a shared boundary.' },
|
|
868
881
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftNSManagedObjectAsyncBoundaryUsage, ruleId: 'heuristics.ios.core-data.nsmanagedobject-async-boundary.ast', code: 'HEURISTICS_IOS_CORE_DATA_NSMANAGEDOBJECT_ASYNC_BOUNDARY_AST', message: 'AST heuristic detected NSManagedObject in an async boundary.' },
|
|
869
882
|
{ platform: 'ios', pathCheck: isIOSApplicationOrPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftCoreDataLayerLeakUsage, ruleId: 'heuristics.ios.core-data.layer-leak.ast', code: 'HEURISTICS_IOS_CORE_DATA_LAYER_LEAK_AST', message: 'AST heuristic detected Core Data APIs leaking into application/presentation code.' },
|
|
@@ -974,9 +987,9 @@ export const extractHeuristicFacts = (
|
|
|
974
987
|
platformDetected &&
|
|
975
988
|
entry.pathCheck(fileFact.path) &&
|
|
976
989
|
(entry.excludePaths ?? []).every((exclude) => !exclude(fileFact.path)) &&
|
|
977
|
-
entry.detect(fileFact.content)
|
|
990
|
+
entry.detect(fileFact.content, fileFact.path)
|
|
978
991
|
) {
|
|
979
|
-
const lines = entry.locateLines?.(fileFact.content);
|
|
992
|
+
const lines = entry.locateLines?.(fileFact.content, fileFact.path);
|
|
980
993
|
heuristicFacts.push(
|
|
981
994
|
createHeuristicFact({
|
|
982
995
|
ruleId: entry.ruleId,
|
|
@@ -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,
|
|
6
|
+
assert.equal(iosRules.length, 131);
|
|
7
7
|
|
|
8
8
|
const ids = iosRules.map((rule) => rule.id);
|
|
9
9
|
assert.deepEqual(ids, [
|
|
@@ -19,13 +19,19 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
|
|
|
19
19
|
'heuristics.ios.dispatchsemaphore.ast',
|
|
20
20
|
'heuristics.ios.operation-queue.ast',
|
|
21
21
|
'heuristics.ios.task-detached.ast',
|
|
22
|
+
'heuristics.ios.concurrency.long-task-without-cancellation-check.ast',
|
|
22
23
|
'heuristics.ios.concurrency.async-without-await.ast',
|
|
24
|
+
'heuristics.ios.concurrency.dummy-await.ast',
|
|
23
25
|
'heuristics.ios.error.empty-catch.ast',
|
|
24
26
|
'heuristics.ios.error.nserror-throw.ast',
|
|
27
|
+
'heuristics.ios.networking.endpoint-enum-ocp.ast',
|
|
25
28
|
'heuristics.ios.swiftui.onappear-task.ast',
|
|
26
29
|
'heuristics.ios.swiftui.onchange-task.ast',
|
|
27
30
|
'heuristics.ios.swiftui.onchange-readonly-var.ast',
|
|
28
31
|
'heuristics.ios.memory.strong-delegate.ast',
|
|
32
|
+
'heuristics.ios.architecture.cross-feature-import.ast',
|
|
33
|
+
'heuristics.ios.architecture.layer-direction-violation.ast',
|
|
34
|
+
'heuristics.ios.architecture.excessive-public-api.ast',
|
|
29
35
|
'heuristics.ios.memory.strong-self-escaping-closure.ast',
|
|
30
36
|
'heuristics.ios.memory.unowned-self-capture.ast',
|
|
31
37
|
'heuristics.ios.memory.manual-management.ast',
|
|
@@ -51,7 +57,10 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
|
|
|
51
57
|
'heuristics.ios.dependencies.carthage.ast',
|
|
52
58
|
'heuristics.ios.dependencies.swiftpm-branch-dependency.ast',
|
|
53
59
|
'heuristics.ios.dependencies.swift-tools-version-below-6-2.ast',
|
|
60
|
+
'heuristics.ios.concurrency.swiftpm-default-isolation-not-mainactor.ast',
|
|
61
|
+
'heuristics.ios.concurrency.swiftpm-strict-concurrency-below-complete.ast',
|
|
54
62
|
'heuristics.ios.concurrency.strict-concurrency-below-complete.ast',
|
|
63
|
+
'heuristics.ios.concurrency.default-actor-isolation-not-mainactor.ast',
|
|
55
64
|
'heuristics.ios.naming.non-pascal-case-type.ast',
|
|
56
65
|
'heuristics.ios.uikit.cell-without-reuse.ast',
|
|
57
66
|
'heuristics.ios.security.userdefaults-sensitive-data.ast',
|
|
@@ -63,6 +72,7 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
|
|
|
63
72
|
'heuristics.ios.accessibility.fixed-font-size.ast',
|
|
64
73
|
'heuristics.ios.localization.physical-text-alignment.ast',
|
|
65
74
|
'heuristics.ios.performance.blocking-sleep.ast',
|
|
75
|
+
'heuristics.ios.concurrency.thread-centric-debugging.ast',
|
|
66
76
|
'heuristics.ios.accessibility.icon-only-control-label.ast',
|
|
67
77
|
'heuristics.ios.accessibility.missing-accessibility-identifier.ast',
|
|
68
78
|
'heuristics.ios.swiftui.missing-bindable-observable-binding.ast',
|
|
@@ -107,6 +117,8 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
|
|
|
107
117
|
'heuristics.ios.tab-item.ast',
|
|
108
118
|
'heuristics.ios.on-tap-gesture.ast',
|
|
109
119
|
'heuristics.ios.accessibility.on-tap-without-button-trait.ast',
|
|
120
|
+
'heuristics.ios.swiftui.glass-interactive-static-element.ast',
|
|
121
|
+
'heuristics.ios.swiftui.glasseffectid-without-namespace.ast',
|
|
110
122
|
'heuristics.ios.string-format.ast',
|
|
111
123
|
'heuristics.ios.scrollview-shows-indicators.ast',
|
|
112
124
|
'heuristics.ios.sheet-is-presented.ast',
|
|
@@ -120,6 +132,7 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
|
|
|
120
132
|
'heuristics.ios.testing.legacy-expectation-description.ast',
|
|
121
133
|
'heuristics.ios.testing.mixed-frameworks.ast',
|
|
122
134
|
'heuristics.ios.testing.quick-nimble.ast',
|
|
135
|
+
'heuristics.ios.testing.third-party-ui-test-framework.ast',
|
|
123
136
|
'heuristics.ios.core-data.nsmanagedobject-boundary.ast',
|
|
124
137
|
'heuristics.ios.core-data.nsmanagedobject-async-boundary.ast',
|
|
125
138
|
'heuristics.ios.core-data.layer-leak.ast',
|
|
@@ -140,10 +153,18 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
|
|
|
140
153
|
byId.get('heuristics.ios.task-detached.ast')?.then.code,
|
|
141
154
|
'HEURISTICS_IOS_TASK_DETACHED_AST'
|
|
142
155
|
);
|
|
156
|
+
assert.equal(
|
|
157
|
+
byId.get('heuristics.ios.concurrency.long-task-without-cancellation-check.ast')?.then.code,
|
|
158
|
+
'HEURISTICS_IOS_CONCURRENCY_LONG_TASK_WITHOUT_CANCELLATION_CHECK_AST'
|
|
159
|
+
);
|
|
143
160
|
assert.equal(
|
|
144
161
|
byId.get('heuristics.ios.concurrency.async-without-await.ast')?.then.code,
|
|
145
162
|
'HEURISTICS_IOS_CONCURRENCY_ASYNC_WITHOUT_AWAIT_AST'
|
|
146
163
|
);
|
|
164
|
+
assert.equal(
|
|
165
|
+
byId.get('heuristics.ios.concurrency.dummy-await.ast')?.then.code,
|
|
166
|
+
'HEURISTICS_IOS_CONCURRENCY_DUMMY_AWAIT_AST'
|
|
167
|
+
);
|
|
147
168
|
assert.equal(
|
|
148
169
|
byId.get('heuristics.ios.error.empty-catch.ast')?.then.code,
|
|
149
170
|
'HEURISTICS_IOS_ERROR_EMPTY_CATCH_AST'
|
|
@@ -164,10 +185,34 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
|
|
|
164
185
|
byId.get('heuristics.ios.dependencies.swift-tools-version-below-6-2.ast')?.then.code,
|
|
165
186
|
'HEURISTICS_IOS_DEPENDENCIES_SWIFT_TOOLS_VERSION_BELOW_6_2_AST'
|
|
166
187
|
);
|
|
188
|
+
assert.equal(
|
|
189
|
+
byId.get('heuristics.ios.concurrency.swiftpm-default-isolation-not-mainactor.ast')?.then.code,
|
|
190
|
+
'HEURISTICS_IOS_CONCURRENCY_SWIFTPM_DEFAULT_ISOLATION_NOT_MAINACTOR_AST'
|
|
191
|
+
);
|
|
192
|
+
assert.equal(
|
|
193
|
+
byId.get('heuristics.ios.concurrency.swiftpm-strict-concurrency-below-complete.ast')?.then.code,
|
|
194
|
+
'HEURISTICS_IOS_CONCURRENCY_SWIFTPM_STRICT_CONCURRENCY_BELOW_COMPLETE_AST'
|
|
195
|
+
);
|
|
167
196
|
assert.equal(
|
|
168
197
|
byId.get('heuristics.ios.concurrency.strict-concurrency-below-complete.ast')?.then.code,
|
|
169
198
|
'HEURISTICS_IOS_CONCURRENCY_STRICT_CONCURRENCY_BELOW_COMPLETE_AST'
|
|
170
199
|
);
|
|
200
|
+
assert.equal(
|
|
201
|
+
byId.get('heuristics.ios.concurrency.default-actor-isolation-not-mainactor.ast')?.then.code,
|
|
202
|
+
'HEURISTICS_IOS_CONCURRENCY_DEFAULT_ACTOR_ISOLATION_NOT_MAINACTOR_AST'
|
|
203
|
+
);
|
|
204
|
+
assert.equal(
|
|
205
|
+
byId.get('heuristics.ios.architecture.cross-feature-import.ast')?.then.code,
|
|
206
|
+
'HEURISTICS_IOS_ARCHITECTURE_CROSS_FEATURE_IMPORT_AST'
|
|
207
|
+
);
|
|
208
|
+
assert.equal(
|
|
209
|
+
byId.get('heuristics.ios.architecture.layer-direction-violation.ast')?.then.code,
|
|
210
|
+
'HEURISTICS_IOS_ARCHITECTURE_LAYER_DIRECTION_VIOLATION_AST'
|
|
211
|
+
);
|
|
212
|
+
assert.equal(
|
|
213
|
+
byId.get('heuristics.ios.architecture.excessive-public-api.ast')?.then.code,
|
|
214
|
+
'HEURISTICS_IOS_ARCHITECTURE_EXCESSIVE_PUBLIC_API_AST'
|
|
215
|
+
);
|
|
171
216
|
assert.equal(
|
|
172
217
|
byId.get('heuristics.ios.naming.non-pascal-case-type.ast')?.then.code,
|
|
173
218
|
'HEURISTICS_IOS_NAMING_NON_PASCAL_CASE_TYPE_AST'
|
|
@@ -200,6 +245,10 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
|
|
|
200
245
|
byId.get('heuristics.ios.accessibility.on-tap-without-button-trait.ast')?.then.code,
|
|
201
246
|
'HEURISTICS_IOS_ACCESSIBILITY_ON_TAP_WITHOUT_BUTTON_TRAIT_AST'
|
|
202
247
|
);
|
|
248
|
+
assert.equal(
|
|
249
|
+
byId.get('heuristics.ios.swiftui.glasseffectid-without-namespace.ast')?.then.code,
|
|
250
|
+
'HEURISTICS_IOS_SWIFTUI_GLASSEFFECTID_WITHOUT_NAMESPACE_AST'
|
|
251
|
+
);
|
|
203
252
|
assert.equal(
|
|
204
253
|
byId.get('heuristics.ios.maintainability.nested-if-pyramid.ast')?.then.code,
|
|
205
254
|
'HEURISTICS_IOS_MAINTAINABILITY_NESTED_IF_PYRAMID_AST'
|
|
@@ -240,6 +289,10 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
|
|
|
240
289
|
byId.get('heuristics.ios.architecture.tca-composable-architecture.ast')?.then.code,
|
|
241
290
|
'HEURISTICS_IOS_ARCHITECTURE_TCA_COMPOSABLE_ARCHITECTURE_AST'
|
|
242
291
|
);
|
|
292
|
+
assert.equal(
|
|
293
|
+
byId.get('heuristics.ios.testing.third-party-ui-test-framework.ast')?.then.code,
|
|
294
|
+
'HEURISTICS_IOS_TESTING_THIRD_PARTY_UI_TEST_FRAMEWORK_AST'
|
|
295
|
+
);
|
|
243
296
|
assert.equal(
|
|
244
297
|
byId.get('heuristics.ios.json.jsonserialization.ast')?.then.code,
|
|
245
298
|
'HEURISTICS_IOS_JSON_JSONSERIALIZATION_AST'
|
|
@@ -292,6 +345,10 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
|
|
|
292
345
|
byId.get('heuristics.ios.performance.blocking-sleep.ast')?.then.code,
|
|
293
346
|
'HEURISTICS_IOS_PERFORMANCE_BLOCKING_SLEEP_AST'
|
|
294
347
|
);
|
|
348
|
+
assert.equal(
|
|
349
|
+
byId.get('heuristics.ios.concurrency.thread-centric-debugging.ast')?.then.code,
|
|
350
|
+
'HEURISTICS_IOS_CONCURRENCY_THREAD_CENTRIC_DEBUGGING_AST'
|
|
351
|
+
);
|
|
295
352
|
assert.equal(
|
|
296
353
|
byId.get('heuristics.ios.accessibility.icon-only-control-label.ast')?.then.code,
|
|
297
354
|
'HEURISTICS_IOS_ACCESSIBILITY_ICON_ONLY_CONTROL_LABEL_AST'
|
|
@@ -218,6 +218,25 @@ export const iosRules: RuleSet = [
|
|
|
218
218
|
code: 'HEURISTICS_IOS_TASK_DETACHED_AST',
|
|
219
219
|
},
|
|
220
220
|
},
|
|
221
|
+
{
|
|
222
|
+
id: 'heuristics.ios.concurrency.long-task-without-cancellation-check.ast',
|
|
223
|
+
description: 'Detects long-running async Swift loops without Task cancellation checks.',
|
|
224
|
+
severity: 'WARN',
|
|
225
|
+
platform: 'ios',
|
|
226
|
+
locked: true,
|
|
227
|
+
when: {
|
|
228
|
+
kind: 'Heuristic',
|
|
229
|
+
where: {
|
|
230
|
+
ruleId: 'heuristics.ios.concurrency.long-task-without-cancellation-check.ast',
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
then: {
|
|
234
|
+
kind: 'Finding',
|
|
235
|
+
message:
|
|
236
|
+
'AST heuristic detected a long async operation without cooperative Task cancellation checks.',
|
|
237
|
+
code: 'HEURISTICS_IOS_CONCURRENCY_LONG_TASK_WITHOUT_CANCELLATION_CHECK_AST',
|
|
238
|
+
},
|
|
239
|
+
},
|
|
221
240
|
{
|
|
222
241
|
id: 'heuristics.ios.concurrency.async-without-await.ast',
|
|
223
242
|
description: 'Detects private async functions that do not await in iOS production code.',
|
|
@@ -237,6 +256,24 @@ export const iosRules: RuleSet = [
|
|
|
237
256
|
code: 'HEURISTICS_IOS_CONCURRENCY_ASYNC_WITHOUT_AWAIT_AST',
|
|
238
257
|
},
|
|
239
258
|
},
|
|
259
|
+
{
|
|
260
|
+
id: 'heuristics.ios.concurrency.dummy-await.ast',
|
|
261
|
+
description: 'Detects dummy awaits inserted to silence async_without_await instead of fixing the async boundary.',
|
|
262
|
+
severity: 'WARN',
|
|
263
|
+
platform: 'ios',
|
|
264
|
+
locked: true,
|
|
265
|
+
when: {
|
|
266
|
+
kind: 'Heuristic',
|
|
267
|
+
where: {
|
|
268
|
+
ruleId: 'heuristics.ios.concurrency.dummy-await.ast',
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
then: {
|
|
272
|
+
kind: 'Finding',
|
|
273
|
+
message: 'AST heuristic detected a dummy await used as a Swift concurrency lint workaround.',
|
|
274
|
+
code: 'HEURISTICS_IOS_CONCURRENCY_DUMMY_AWAIT_AST',
|
|
275
|
+
},
|
|
276
|
+
},
|
|
240
277
|
{
|
|
241
278
|
id: 'heuristics.ios.error.empty-catch.ast',
|
|
242
279
|
description: 'Detects Swift catch blocks that silently swallow errors.',
|
|
@@ -273,6 +310,25 @@ export const iosRules: RuleSet = [
|
|
|
273
310
|
code: 'HEURISTICS_IOS_ERROR_NSERROR_THROW_AST',
|
|
274
311
|
},
|
|
275
312
|
},
|
|
313
|
+
{
|
|
314
|
+
id: 'heuristics.ios.networking.endpoint-enum-ocp.ast',
|
|
315
|
+
description: 'Detects API endpoint catalogs modeled as Swift enums instead of data-driven endpoint values.',
|
|
316
|
+
severity: 'WARN',
|
|
317
|
+
platform: 'ios',
|
|
318
|
+
locked: true,
|
|
319
|
+
when: {
|
|
320
|
+
kind: 'Heuristic',
|
|
321
|
+
where: {
|
|
322
|
+
ruleId: 'heuristics.ios.networking.endpoint-enum-ocp.ast',
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
then: {
|
|
326
|
+
kind: 'Finding',
|
|
327
|
+
message:
|
|
328
|
+
'AST heuristic detected API endpoint modeled as enum; use a data-driven APIEndpoint struct to preserve OCP.',
|
|
329
|
+
code: 'HEURISTICS_IOS_NETWORKING_ENDPOINT_ENUM_OCP_AST',
|
|
330
|
+
},
|
|
331
|
+
},
|
|
276
332
|
{
|
|
277
333
|
id: 'heuristics.ios.swiftui.onappear-task.ast',
|
|
278
334
|
description: 'Detects Task launches from SwiftUI onAppear where .task can provide lifecycle cancellation.',
|
|
@@ -349,6 +405,61 @@ export const iosRules: RuleSet = [
|
|
|
349
405
|
code: 'HEURISTICS_IOS_MEMORY_STRONG_DELEGATE_AST',
|
|
350
406
|
},
|
|
351
407
|
},
|
|
408
|
+
{
|
|
409
|
+
id: 'heuristics.ios.architecture.cross-feature-import.ast',
|
|
410
|
+
description: 'Detects Swift feature modules importing sibling feature modules directly.',
|
|
411
|
+
severity: 'WARN',
|
|
412
|
+
platform: 'ios',
|
|
413
|
+
locked: true,
|
|
414
|
+
when: {
|
|
415
|
+
kind: 'Heuristic',
|
|
416
|
+
where: {
|
|
417
|
+
ruleId: 'heuristics.ios.architecture.cross-feature-import.ast',
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
then: {
|
|
421
|
+
kind: 'Finding',
|
|
422
|
+
message: 'AST heuristic detected a Swift feature importing another feature module directly.',
|
|
423
|
+
code: 'HEURISTICS_IOS_ARCHITECTURE_CROSS_FEATURE_IMPORT_AST',
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
id: 'heuristics.ios.architecture.layer-direction-violation.ast',
|
|
428
|
+
description: 'Detects Swift imports that violate iOS Clean Architecture layer direction.',
|
|
429
|
+
severity: 'WARN',
|
|
430
|
+
platform: 'ios',
|
|
431
|
+
locked: true,
|
|
432
|
+
when: {
|
|
433
|
+
kind: 'Heuristic',
|
|
434
|
+
where: {
|
|
435
|
+
ruleId: 'heuristics.ios.architecture.layer-direction-violation.ast',
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
then: {
|
|
439
|
+
kind: 'Finding',
|
|
440
|
+
message:
|
|
441
|
+
'AST heuristic detected an import that violates Clean Architecture layer direction in iOS code.',
|
|
442
|
+
code: 'HEURISTICS_IOS_ARCHITECTURE_LAYER_DIRECTION_VIOLATION_AST',
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
id: 'heuristics.ios.architecture.excessive-public-api.ast',
|
|
447
|
+
description: 'Detects excessive public/open API surface in iOS app implementation files.',
|
|
448
|
+
severity: 'WARN',
|
|
449
|
+
platform: 'ios',
|
|
450
|
+
locked: true,
|
|
451
|
+
when: {
|
|
452
|
+
kind: 'Heuristic',
|
|
453
|
+
where: {
|
|
454
|
+
ruleId: 'heuristics.ios.architecture.excessive-public-api.ast',
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
then: {
|
|
458
|
+
kind: 'Finding',
|
|
459
|
+
message: 'AST heuristic detected excessive public/open Swift API surface in iOS app code.',
|
|
460
|
+
code: 'HEURISTICS_IOS_ARCHITECTURE_EXCESSIVE_PUBLIC_API_AST',
|
|
461
|
+
},
|
|
462
|
+
},
|
|
352
463
|
{
|
|
353
464
|
id: 'heuristics.ios.memory.strong-self-escaping-closure.ast',
|
|
354
465
|
description: 'Detects strong self captures in escaping iOS closures.',
|
|
@@ -817,6 +928,42 @@ export const iosRules: RuleSet = [
|
|
|
817
928
|
code: 'HEURISTICS_IOS_DEPENDENCIES_SWIFT_TOOLS_VERSION_BELOW_6_2_AST',
|
|
818
929
|
},
|
|
819
930
|
},
|
|
931
|
+
{
|
|
932
|
+
id: 'heuristics.ios.concurrency.swiftpm-default-isolation-not-mainactor.ast',
|
|
933
|
+
description: 'Detects SwiftPM iOS manifests whose default isolation is not MainActor.',
|
|
934
|
+
severity: 'WARN',
|
|
935
|
+
platform: 'ios',
|
|
936
|
+
locked: true,
|
|
937
|
+
when: {
|
|
938
|
+
kind: 'Heuristic',
|
|
939
|
+
where: {
|
|
940
|
+
ruleId: 'heuristics.ios.concurrency.swiftpm-default-isolation-not-mainactor.ast',
|
|
941
|
+
},
|
|
942
|
+
},
|
|
943
|
+
then: {
|
|
944
|
+
kind: 'Finding',
|
|
945
|
+
message: 'AST heuristic detected Package.swift .defaultIsolation not set to MainActor.',
|
|
946
|
+
code: 'HEURISTICS_IOS_CONCURRENCY_SWIFTPM_DEFAULT_ISOLATION_NOT_MAINACTOR_AST',
|
|
947
|
+
},
|
|
948
|
+
},
|
|
949
|
+
{
|
|
950
|
+
id: 'heuristics.ios.concurrency.swiftpm-strict-concurrency-below-complete.ast',
|
|
951
|
+
description: 'Detects SwiftPM iOS manifests using targeted/minimal StrictConcurrency.',
|
|
952
|
+
severity: 'WARN',
|
|
953
|
+
platform: 'ios',
|
|
954
|
+
locked: true,
|
|
955
|
+
when: {
|
|
956
|
+
kind: 'Heuristic',
|
|
957
|
+
where: {
|
|
958
|
+
ruleId: 'heuristics.ios.concurrency.swiftpm-strict-concurrency-below-complete.ast',
|
|
959
|
+
},
|
|
960
|
+
},
|
|
961
|
+
then: {
|
|
962
|
+
kind: 'Finding',
|
|
963
|
+
message: 'AST heuristic detected Package.swift StrictConcurrency below complete.',
|
|
964
|
+
code: 'HEURISTICS_IOS_CONCURRENCY_SWIFTPM_STRICT_CONCURRENCY_BELOW_COMPLETE_AST',
|
|
965
|
+
},
|
|
966
|
+
},
|
|
820
967
|
{
|
|
821
968
|
id: 'heuristics.ios.concurrency.strict-concurrency-below-complete.ast',
|
|
822
969
|
description: 'Detects Xcode iOS targets with strict concurrency checking below complete.',
|
|
@@ -835,6 +982,25 @@ export const iosRules: RuleSet = [
|
|
|
835
982
|
code: 'HEURISTICS_IOS_CONCURRENCY_STRICT_CONCURRENCY_BELOW_COMPLETE_AST',
|
|
836
983
|
},
|
|
837
984
|
},
|
|
985
|
+
{
|
|
986
|
+
id: 'heuristics.ios.concurrency.default-actor-isolation-not-mainactor.ast',
|
|
987
|
+
description: 'Detects Xcode iOS targets whose default actor isolation is not MainActor.',
|
|
988
|
+
severity: 'WARN',
|
|
989
|
+
platform: 'ios',
|
|
990
|
+
locked: true,
|
|
991
|
+
when: {
|
|
992
|
+
kind: 'Heuristic',
|
|
993
|
+
where: {
|
|
994
|
+
ruleId: 'heuristics.ios.concurrency.default-actor-isolation-not-mainactor.ast',
|
|
995
|
+
},
|
|
996
|
+
},
|
|
997
|
+
then: {
|
|
998
|
+
kind: 'Finding',
|
|
999
|
+
message:
|
|
1000
|
+
'AST heuristic detected SWIFT_DEFAULT_ACTOR_ISOLATION not set to MainActor in an iOS Xcode project.',
|
|
1001
|
+
code: 'HEURISTICS_IOS_CONCURRENCY_DEFAULT_ACTOR_ISOLATION_NOT_MAINACTOR_AST',
|
|
1002
|
+
},
|
|
1003
|
+
},
|
|
838
1004
|
{
|
|
839
1005
|
id: 'heuristics.ios.naming.non-pascal-case-type.ast',
|
|
840
1006
|
description: 'Detects Swift type declarations that are not PascalCase.',
|
|
@@ -1034,6 +1200,24 @@ export const iosRules: RuleSet = [
|
|
|
1034
1200
|
code: 'HEURISTICS_IOS_PERFORMANCE_BLOCKING_SLEEP_AST',
|
|
1035
1201
|
},
|
|
1036
1202
|
},
|
|
1203
|
+
{
|
|
1204
|
+
id: 'heuristics.ios.concurrency.thread-centric-debugging.ast',
|
|
1205
|
+
description: 'Detects Thread.current, Thread.isMainThread and pthread thread identity checks in iOS production code.',
|
|
1206
|
+
severity: 'WARN',
|
|
1207
|
+
platform: 'ios',
|
|
1208
|
+
locked: true,
|
|
1209
|
+
when: {
|
|
1210
|
+
kind: 'Heuristic',
|
|
1211
|
+
where: {
|
|
1212
|
+
ruleId: 'heuristics.ios.concurrency.thread-centric-debugging.ast',
|
|
1213
|
+
},
|
|
1214
|
+
},
|
|
1215
|
+
then: {
|
|
1216
|
+
kind: 'Finding',
|
|
1217
|
+
message: 'AST heuristic detected thread-centric debugging in Swift Concurrency code.',
|
|
1218
|
+
code: 'HEURISTICS_IOS_CONCURRENCY_THREAD_CENTRIC_DEBUGGING_AST',
|
|
1219
|
+
},
|
|
1220
|
+
},
|
|
1037
1221
|
{
|
|
1038
1222
|
id: 'heuristics.ios.accessibility.icon-only-control-label.ast',
|
|
1039
1223
|
description: 'Detects icon-only SwiftUI controls without explicit accessibility labels.',
|
|
@@ -1845,6 +2029,42 @@ export const iosRules: RuleSet = [
|
|
|
1845
2029
|
code: 'HEURISTICS_IOS_ACCESSIBILITY_ON_TAP_WITHOUT_BUTTON_TRAIT_AST',
|
|
1846
2030
|
},
|
|
1847
2031
|
},
|
|
2032
|
+
{
|
|
2033
|
+
id: 'heuristics.ios.swiftui.glass-interactive-static-element.ast',
|
|
2034
|
+
description: 'Detects Liquid Glass interactive styling on static SwiftUI elements.',
|
|
2035
|
+
severity: 'WARN',
|
|
2036
|
+
platform: 'ios',
|
|
2037
|
+
locked: true,
|
|
2038
|
+
when: {
|
|
2039
|
+
kind: 'Heuristic',
|
|
2040
|
+
where: {
|
|
2041
|
+
ruleId: 'heuristics.ios.swiftui.glass-interactive-static-element.ast',
|
|
2042
|
+
},
|
|
2043
|
+
},
|
|
2044
|
+
then: {
|
|
2045
|
+
kind: 'Finding',
|
|
2046
|
+
message: 'AST heuristic detected Liquid Glass .interactive() on a static SwiftUI element.',
|
|
2047
|
+
code: 'HEURISTICS_IOS_SWIFTUI_GLASS_INTERACTIVE_STATIC_ELEMENT_AST',
|
|
2048
|
+
},
|
|
2049
|
+
},
|
|
2050
|
+
{
|
|
2051
|
+
id: 'heuristics.ios.swiftui.glasseffectid-without-namespace.ast',
|
|
2052
|
+
description: 'Detects SwiftUI glassEffectID usage without @Namespace or Namespace.ID ownership.',
|
|
2053
|
+
severity: 'WARN',
|
|
2054
|
+
platform: 'ios',
|
|
2055
|
+
locked: true,
|
|
2056
|
+
when: {
|
|
2057
|
+
kind: 'Heuristic',
|
|
2058
|
+
where: {
|
|
2059
|
+
ruleId: 'heuristics.ios.swiftui.glasseffectid-without-namespace.ast',
|
|
2060
|
+
},
|
|
2061
|
+
},
|
|
2062
|
+
then: {
|
|
2063
|
+
kind: 'Finding',
|
|
2064
|
+
message: 'AST heuristic detected glassEffectID without @Namespace or Namespace.ID.',
|
|
2065
|
+
code: 'HEURISTICS_IOS_SWIFTUI_GLASSEFFECTID_WITHOUT_NAMESPACE_AST',
|
|
2066
|
+
},
|
|
2067
|
+
},
|
|
1848
2068
|
{
|
|
1849
2069
|
id: 'heuristics.ios.string-format.ast',
|
|
1850
2070
|
description: 'Detects String(format:) usage in iOS production code.',
|
|
@@ -2084,6 +2304,25 @@ export const iosRules: RuleSet = [
|
|
|
2084
2304
|
code: 'HEURISTICS_IOS_TESTING_QUICK_NIMBLE_AST',
|
|
2085
2305
|
},
|
|
2086
2306
|
},
|
|
2307
|
+
{
|
|
2308
|
+
id: 'heuristics.ios.testing.third-party-ui-test-framework.ast',
|
|
2309
|
+
description: 'Detects third-party iOS UI test frameworks where native XCUITest is required.',
|
|
2310
|
+
severity: 'WARN',
|
|
2311
|
+
platform: 'ios',
|
|
2312
|
+
locked: true,
|
|
2313
|
+
when: {
|
|
2314
|
+
kind: 'Heuristic',
|
|
2315
|
+
where: {
|
|
2316
|
+
ruleId: 'heuristics.ios.testing.third-party-ui-test-framework.ast',
|
|
2317
|
+
},
|
|
2318
|
+
},
|
|
2319
|
+
then: {
|
|
2320
|
+
kind: 'Finding',
|
|
2321
|
+
message:
|
|
2322
|
+
'AST heuristic detected third-party iOS UI test framework usage; use native XCUITest.',
|
|
2323
|
+
code: 'HEURISTICS_IOS_TESTING_THIRD_PARTY_UI_TEST_FRAMEWORK_AST',
|
|
2324
|
+
},
|
|
2325
|
+
},
|
|
2087
2326
|
{
|
|
2088
2327
|
id: 'heuristics.ios.core-data.nsmanagedobject-boundary.ast',
|
|
2089
2328
|
description: 'Detects NSManagedObject usage in shared iOS boundaries.',
|