Skip to content

#Preview detection has false positives when support code is ONLY called from #Preview code that will be kept #1061

@danwood

Description

@danwood

Describe the bug

When Periphery detects code that is only referenced from #Preview macro blocks, it correctly identifies that code as unused. However, the current detection has two related problems:

  1. False positives on preview-support code. If a declaration is referenced from multiple #Preview blocks, and at least one of those previews is for a view that is used in the app, that declaration should be kept. Currently, all #Preview macro expansions are un-retained equally, so code that exists to support a legitimate preview gets flagged as unused.

    For example, a helper like previewPadding() or a view like ViewOnlyUsedInPreview might appear in both a "dead" preview (one that only previews unused views) and a "live" preview (one that previews a view used in the app). The helper should be kept because the live preview needs it, but Periphery flags it as unused.

  2. The #Preview block itself is never reported. When a #Preview block exclusively references unused code (views not used anywhere in the app), Periphery flags the referenced views but does not flag the #Preview block itself. If the warning is heeded and the view is removed, this would leave an orphaned preview in the codebase that the developer has no reason to keep. Ideally, Periphery would report the dead #Preview block as unused (similar to behavior of a PreviewProvider preview), giving the developer a clear signal to remove it along with the dead code it references.

The root cause is that the algorithm treats all #Preview blocks identically without distinguishing between previews that exercise live app code and previews that only reference otherwise-dead code.

Reproduction

The attached PreviewPeriphery Xcode project demonstrates the scenarios. The key file is ContentView.swift, which contains:

  • ContentView and UsedView — views used by the app (via @main entry point). Should be kept.
  • ViewOnlyUsedInPreview and previewPadding() — only referenced from previews, but one of those previews is for UsedView (live app code). Should be kept, but currently flagged.
  • UnusedView, CompletelyUnusedView, DeadChainedView, DeadChainedHelper — views not used anywhere in the app, only referenced from dead previews. Should be flagged as unused.
  • Three dead #Preview blocks — each exclusively references unused views. These should themselves be reported as unused, but currently are not.
  • Three live #Preview blocks — each references at least one view that is used in the app (UsedView or ContentView). These should not be reported.
  • Old-style PreviewProvider structs for both a used and an unused view, to verify consistent behavior across preview styles.

Environment

periphery version: 3.4.0
Apple Swift version 6.2.3 (swiftlang-6.2.3.3.21 clang-1700.6.3.2)
Target: arm64-apple-macosx26.0
Xcode 26.2
Build version 17C52

PreviewPeriphery.zip

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions