diff --git a/.mise/tasks/release b/.mise/tasks/release index e480a3c29..bb33f0bfe 100755 --- a/.mise/tasks/release +++ b/.mise/tasks/release @@ -115,14 +115,6 @@ fi cd .. gh release create --latest="${is_latest}" -F .release/release_notes.md "${version}" ".release/${zip_filename}" ".release/${zip_artifactbundle}" ".release/${bazel_zip_filename}" -# Homebrew -if [ $is_latest = false ]; then - echo "Not releasing beta to Homebrew." -else - cd ../homebrew-periphery - cat periphery.rb.template | sed s/__VERSION__/${version}/ | sed s/__SHA256__/${sha256}/ > Casks/periphery.rb - git add Casks/periphery.rb - git commit -m "${version}" - git push origin master - cd ../periphery -fi \ No newline at end of file +echo "Next steps:" +echo "* Update Homebrew formula" +echo "* Update Bazel Central Registry" diff --git a/CHANGELOG.md b/CHANGELOG.md index 493868e6c..e74cf7d6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,17 @@ ##### Breaking +**3.0 is a major breaking change and requires some manual migration, please see the [3.0 Migration Guide](https://github.com/peripheryapp/periphery/wiki/3.0-Migration-Guide).** + - Support for installing via CocoaPods has been removed. - Removed support for Swift 5.9/Xcode 15.2. +- Periphery is now available directly from Homebrew, and the `peripheryapp/periphery` tap is no longer updated. To migrate run the following: +``` +brew remove periphery +brew untap peripheryapp/periphery +brew update +brew install periphery +``` ##### Enhancements @@ -11,7 +20,9 @@ ##### Bug Fixes -- None. +- Fix numerous issues where generated code could not be scanned. +- Fix support for Xcode's new folder format. +- Fix cloning private Swift package repositories. ## 2.21.2 (2024-11-01) diff --git a/README.md b/README.md index 434218fd0..c430e8cd3 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,6 @@ - [Installation](#installation) - [How To Use](#how-to-use) -- [How It Works](#how-it-works) - [Analysis](#analysis) - [Function Parameters](#function-parameters) - [Protocols](#protocols-1) @@ -49,7 +48,7 @@ ### [Homebrew](https://brew.sh/) ```sh -brew install peripheryapp/periphery/periphery +brew install periphery ``` ### [Mint](https://github.com/yonaskolb/mint) @@ -58,43 +57,48 @@ brew install peripheryapp/periphery/periphery mint install peripheryapp/periphery ``` +### [Bazel](https://bazel.build/) + +```python +bazel_dep(name = "periphery", version = "") +use_repo(use_extension("@periphery//bazel:generated.bzl", "generated"), "periphery_generated") +``` + +```sh +bazel run @periphery -- scan --bazel +``` + ## How To Use ### The `scan` Command -The scan command is Periphery's primary function. To begin a guided setup, simply change to your project directory and run: +The scan command is Periphery's primary function. To begin a guided setup, change to your project directory and run: ```sh periphery scan --setup ``` -> Guided setup only works for Xcode and SwiftPM projects, to use Periphery with non-Apple build systems such as Bazel, see [Build Systems](#build-systems). - -After answering a few questions, Periphery will print out the full scan command and execute it. +The guided setup will detect your project type and configure a few options. After answering a few questions, Periphery will print out the full scan command and execute it. The guided setup is only intended for introductory purposes, once you are familiar with Periphery you can try some more advanced options, all of which can be seen with `periphery help scan`. -To get coherent results from Periphery, it's crucial to understand the implications of the build targets you choose to analyze. For example, imagine a project consisting of three targets: App, Lib and Tests. The App target imports Lib, and the Tests targets imports both App and Lib. If you were to provide all three to the `--targets` option then Periphery will be able to analyze your project as a whole. However, if you only choose to analyze App and Lib, but not Tests, Periphery may report some instances of unused code that are _only_ referenced by Tests. Therefore when you suspect Periphery has provided an incorrect result, it's important to consider the targets that you have chosen to analyze. +To get the most from Periphery, it’s important to understand how it works. Periphery first builds your project; it does this to generate the “index store”. The index store contains detailed information about the declarations (class, struct, func, etc.) in your project and their references to other declarations. Using this store, Periphery builds an in-memory graph of the relational structure of your project, supplementing it with additional information obtained by parsing each source file. Next, the graph is mutated to make it more suitable for detecting unused code, e.g. marking your project’s entry points. Finally, the graph is traversed from its roots to identify unreferenced declarations. -If your project consists of one or more standalone frameworks that do not also contain some kind of application that consume their interfaces, then you'll need to tell Periphery to assume that all public declarations are in fact used by including the `--retain-public` option. +> **Tip** +> +> The index store only contains information about source files in the build targets compiled during the build phase. If a given class is only referenced in a source file that was not compiled, then Periphery will identify the class as unused. It’s important to ensure you build all the targets you expect to contain references. For an Xcode project, this is controlled using the `-—schemes` option. For a Swift package, all targets are built automatically. -For projects that are mixed Objective-C & Swift, it's highly recommend you [read about the implications](#objective-c) this can have on your results. +If your project consists of one or more standalone frameworks that do not also contain some kind of application that consumes their interfaces, you need to tell Periphery to assume that all public declarations are used with the `--retain-public` option. + +For projects that are mixed Objective-C & Swift, it's highly recommended you [read about the implications](#objective-c) this can have on your results. ### Configuration Once you've settled upon the appropriate options for your project, you may wish to persist them in a YAML configuration file. The simplest way to achieve this is to run Periphery with the `--verbose` option. Near the beginning of the output you will see the `[configuration:begin]` section with your configuration formatted as YAML below. Copy & paste the configuration into `.periphery.yml` in the root of your project folder. You can now simply run `periphery scan` and the YAML configuration will be used. -## How It Works - -Periphery first builds your project. For Xcode projects the schemes provided via the `--schemes` option are built using `xcodebuild`. For Swift Package Manager projects, the individual targets provided via the `--targets` option are built using `swift build`. The Swift compiler employs a technique called index-while-building to populate an index store that contains information about the structure of your project's source code. - -After your project is built, Periphery performs an indexing phase. For every source file that is a member of the targets provided via the `--targets` option, Periphery obtains its structural information from the index store and builds its own internal graph representation of your project. Periphery also analyzes each file's abstract syntax tree (AST) to fill in some details not provided by the index store. - -Once indexing is complete, Periphery analyzes the graph to identify unused code. This phase consists of a number of steps that mutate the graph to make it easier to identify specific scenarios of unused code. The final step walks the graph from its roots to identify declarations that are no longer referenced. - ## Analysis -The goal of Periphery is to report instances of unused _declarations_. A declaration is a `class`, `struct`, `protocol`, `function`, `property`, `constructor`, `enum`, `typealias`, `associatedtype`, etc. As you'd expect, Periphery is able to identify simple unreferenced declarations, e.g a `class` that is no longer used anywhere in your codebase. +The goal of Periphery is to report instances of unused _declarations_. A declaration is a `class`, `struct`, `protocol`, `function`, `property`, `constructor`, `enum`, `typealias`, `associatedtype`, etc. As you'd expect, Periphery can identify simple unreferenced declarations, e.g. a `class` that is no longer used anywhere in your codebase. Periphery can also identify more advanced instances of unused code. The following section explains these in detail. @@ -155,7 +159,7 @@ class InformalGreeter: BaseGreeter { #### Foreign Protocols & Classes -Unused parameters of protocols or classes defined in foreign modules (e.g Foundation) are always ignored, since you do not have access to modify the base function declaration. +Unused parameters of protocols or classes defined in foreign modules (e.g. Foundation) are always ignored since you do not have access to modify the base function declaration. #### fatalError Functions @@ -240,11 +244,11 @@ let myClass = MyClass() myClass.perform() ``` -Here we can see that `MyProtocol` is itself used, and cannot be removed. However, since `unusedProperty` is never called on `MyConformingClass`, Periphery is able to identify that the declaration of `unusedProperty` in `MyProtocol` is also unused and can be removed along with the unused implementation of `unusedProperty`. +Here we can see that `MyProtocol` is itself used, and cannot be removed. However, since `unusedProperty` is never called on `MyConformingClass`, Periphery can identify that the declaration of `unusedProperty` in `MyProtocol` is also unused and can be removed along with the unused implementation of `unusedProperty`. ### Enumerations -Along with being able to identify unused enumerations, Periphery can also identify individual unused enum cases. Plain enums that are not raw representable, i.e that _don't_ have a `String`, `Character`, `Int` or floating-point value type can be reliably identified. However, enumerations that _do_ have a raw value type can be dynamic in nature, and therefore must be assumed to be used. +Along with being able to identify unused enumerations, Periphery can also identify individual unused enum cases. Plain enums that are not raw representable, i.e. that _don't_ have a `String`, `Character`, `Int`, or floating-point value type can be reliably identified. However, enumerations that _do_ have a raw value type can be dynamic, and therefore must be assumed to be used. Let's clear this up with a quick example: @@ -260,7 +264,7 @@ func someFunction(value: String) { } ``` -There's no direct reference to the `myCase` case, so it's reasonable to expect it _might_ no longer be needed, however if it were removed we can see that `somethingImportant` would never be called if `someFunction` were passed the value of `"myCase"`. +There's no direct reference to the `myCase` case, so it's reasonable to expect it _might_ no longer be needed, however, if it were removed we can see that `somethingImportant` would never be called if `someFunction` were passed the value of `"myCase"`. ### Assign-only Properties @@ -279,7 +283,7 @@ class MyClass { In some cases this may be the intended behavior, therefore you have a few options available to silence such results: - Retain individual properties using [Comment Commands](#comment-commands). -- Retain all assign-only properties by their type with `--retain-assign-only-property-types`. Given types must match their exact usage in the property declaration (sans optional question mark), e.g `String`, `[String]`, `Set`. Periphery is unable to resolve inferred property types, therefore in some instances you may need to add explicit type annotations to your properties. +- Retain all assign-only properties by their type with `--retain-assign-only-property-types`. Given types must match their exact usage in the property declaration (sans optional question mark), e.g. `String`, `[String]`, `Set`. Periphery is unable to resolve inferred property types, therefore in some instances, you may need to add explicit type annotations to your properties. - Disable assign-only property analysis entirely with `--retain-assign-only-properties`. ### Redundant Public Accessibility @@ -301,13 +305,13 @@ Periphery will likely produce false positives for targets with mixed Swift and O Periphery cannot analyze Objective-C code since types may be dynamically typed. -By default Periphery does not assume that declarations accessible by the Objective-C runtime are in use. If your project is a mix of Swift & Objective-C, you can enable this behavior with the `--retain-objc-accessible` option. Swift declarations that are accessible by the Objective-C runtime are those that are explicitly annotated with `@objc` or `@objcMembers`, and classes that inherit `NSObject` either directly or indirectly via another class. +By default, Periphery does not assume that declarations accessible by the Objective-C runtime are in use. If your project is a mix of Swift & Objective-C, you can enable this behavior with the `--retain-objc-accessible` option. Swift declarations that are accessible by the Objective-C runtime are those that are explicitly annotated with `@objc` or `@objcMembers`, and classes that inherit `NSObject` either directly or indirectly via another class. -Alternatively, the `--retain-objc-annotated` can be used to only retain declarations that are explicitly annotated with `@objc` or `@objcMembers`. Types that inherit `NSObject` are not retained unless they have the explicit annotations. This option may uncover more unused code, but with the caveat that some of the results may be incorrect if the declaration is in fact used in Objective-C code. To resolve these incorrect results you must add an `@objc` annotation to the declaration. +Alternatively, the `--retain-objc-annotated` can be used to only retain declarations that are explicitly annotated with `@objc` or `@objcMembers`. Types that inherit `NSObject` are not retained unless they have explicit annotations. This option may uncover more unused code, but with the caveat that some of the results may be incorrect if the declaration is used in Objective-C code. To resolve these incorrect results you must add an `@objc` annotation to the declaration. ### Codable -Swift synthesizes additional code for `Codable` types that is not visible to Periphery, and can result in false positives for properties not directly referenced from non-synthesized code. If your project contains many such types, you can retain all properties on `Codable` types with `--retain-codable-properties`. Alternatively, you can retain properties only on `Encodable` types with `--retain-encodable-properties`. +Swift synthesizes additional code for `Codable` types that is not visible to Periphery and can result in false positives for properties not directly referenced from non-synthesized code. If your project contains many such types, you can retain all properties on `Codable` types with `--retain-codable-properties`. Alternatively, you can retain properties only on `Encodable` types with `--retain-encodable-properties`. If `Codable` conformance is declared by a protocol in an external module not scanned by Periphery, you can instruct Periphery to identify the protocols as `Codable` with `--external-codable-protocols "ExternalProtocol"`. @@ -321,7 +325,7 @@ If your project contains Interface Builder files (such as storyboards and XIBs), ## Comment Commands -For whatever reason, you may want to keep some unused code. Source code comment commands can be used to ignore specific declarations, and exclude them from the results. +For whatever reason, you may want to keep some unused code. Source code comment commands can be used to ignore specific declarations and exclude them from the results. An ignore comment command can be placed directly on the line above any declaration to ignore it, and all descendent declarations: @@ -350,7 +354,7 @@ class MyClass {} ## Xcode Integration -Before setting up Xcode integration, we highly recommend you first get Periphery working in a terminal, as you will be using the exact same command via Xcode. +Before setting up Xcode integration, we highly recommend you first get Periphery working in a terminal, as you will be using the same command via Xcode. ### Step 1: Create an Aggregate Target @@ -358,7 +362,7 @@ Select your project in the Project Navigator and click the + button at the botto ![Step 1](assets/xcode-integration/1.png) -Choose a name for the new target, e.g "Periphery" or "Unused Code". +Choose a name for the new target, e.g. "Periphery" or "Unused Code". ![Step 2](assets/xcode-integration/2.png) @@ -384,7 +388,7 @@ You're ready to roll. You should now see the new scheme in the dropdown. Select ## Excluding Files -Both exclusion options described below accept a Bash v4 style path glob, either absolute or relative to your project directory. You can delimit multiple globs with a space, e.g `--option "Sources/Single.swift" "**/Generated/*.swift" "**/*.{xib,storyboard}"`. +Both exclusion options described below accept a Bash v4 style path glob, either absolute or relative to your project directory. You can delimit multiple globs with a space, e.g. `--option "Sources/Single.swift" "**/Generated/*.swift" "**/*.{xib,storyboard}"`. ### Excluding Results @@ -392,7 +396,13 @@ To exclude the results from certain files, pass the `--report-exclude ` o ### Excluding Indexed Files -To exclude files from being indexed, pass the `--index-exclude ` option to the `scan` command. Excluding files from the index phase means that any declarations and references contained within the files will not be seen by Periphery. Periphery will be behave as if the files do not exist. For example, this option can be used to exclude generated code that holds references to non-generated code, or exclude all `.xib` and `.storyboard` files that hold references to code. +Excluding files from the indexing phase means that any declarations and references contained within the files will not be seen by Periphery. Periphery will behave as if the files do not exist. + +To exclude files from being indexed, there are a few options: + +1. Use `--exclude-targets "TargetA" "TargetB"` to exclude all source files in the chosen targets. +2. Use `--exclude-tests` to exclude all test targets. +3. Use `--index-exclude "file.swift" "path/*.swift"` to exclude individual source files. ### Retaining File Declarations @@ -400,7 +410,7 @@ To retain all declarations in files, pass the `--retain-files ` option to ## Continuous Integration -When integrating Periphery into a CI pipeline, you can potentially skip the build phase if your pipeline has already done so, e.g to run tests. This can be achieved using the `--skip-build` option. However, you also need to tell Periphery the location of the index store using `--index-store-path`. This location is dependent on your project type. +When integrating Periphery into a CI pipeline, you can potentially skip the build phase if your pipeline has already done so, e.g. to run tests. This can be achieved using the `--skip-build` option. However, you also need to tell Periphery the location of the index store using `--index-store-path`. This location is dependent on your project type. Note that when using `--skip-build` and `--index-store-path` it's vital that the index store contains data for all of the targets you specify via `--targets`. For example, if your pipeline previously built the targets 'App' and 'Lib', the index store will only contain data for the files in those targets. You cannot then instruct Periphery to scan additional targets, e.g 'Extension', or 'UnitTests'. @@ -410,20 +420,33 @@ The index store generated by `xcodebuild` exists in DerivedData at a location de ### SwiftPM -By default, Periphery looks for the index store at `.build/debug/index/store`. Therefore, if you intend to run Periphery directly after calling `swift test`, you can omit the `--index-store-path` option, and Periphery will use the index store created when the project was built for testing. However if this isn't the case, then you must provide Periphery the location of the index store with `--index-store-path`. +By default, Periphery looks for the index store at `.build/debug/index/store`. Therefore, if you intend to run Periphery directly after calling `swift test`, you can omit the `--index-store-path` option and Periphery will use the index store created when the project was built for testing. However, if this isn't the case, then you must provide Periphery the location of the index store with `--index-store-path`. ## Build Systems -Periphery can analyze projects using third-party build systems such as Bazel, though it cannot drive them automatically like SwiftPM and xcodebuild. Instead, you need to specify the index store location and provide a file-target mapping file. - -A file-target mapping file contains a simple mapping of source files to build targets. You will need to generate this file yourself using the appropriate tooling for your build system. The format is as follows: +Periphery can analyze projects using other build systems, though it cannot drive them automatically like SPM, Xcode and Bazel. Instead, you need to specify the location of indexstore and other resource files. The format is as follows: ```json { - "file_targets": { - "path/to/file_a.swift": ["TargetA"], - "path/to/file_b.swift": ["TargetB", "TargetC"] - } + "indexstores": [ + "path/to/file.indexstore" + ], + "test_targets": [ + "MyTests" + ], + "plists": [ + "path/to/file.plist" + ], + "xibs": [ + "path/to/file.xib", + "path/to/file.storyboard" + ], + "xcdatamodels": [ + "path/to/file.xcdatamodel" + ], + "xcmappingmodels": [ + "path/to/file.xcmappingmodel" + ] } ``` @@ -431,10 +454,10 @@ A file-target mapping file contains a simple mapping of source files to build ta > > Relative paths are assumed to be relative to the current directory. -You can then invoke periphery as follows: +You can then invoke Periphery as follows: ```sh -periphery scan --file-targets-path map.json --index-store-path index/store +periphery scan --generic-project-config config.json ``` > **Tip** @@ -447,11 +470,11 @@ Periphery supports both macOS and Linux. macOS supports both Xcode and Swift Pac ## Troubleshooting -### Erroneous results in one or more files, such as false-positives and incorrect source file locations +### Erroneous results in one or more files, such as false positives and incorrect source file locations It's possible for the index store to become corrupt, or out of sync with the source file. This can happen, for example, if you forcefully terminate (^C) a scan. To rectify this, you can pass the `--clean-build` flag to the scan command to force removal of existing build artifacts. -### Code referenced within preprocessor macro conditional branch is unused +### Code referenced within a preprocessor macro conditional branch is unused When Periphery builds your project it uses the default build configuration, which is typically 'debug'. If you use preprocessor macros to conditionally compile code, Periphery will only have visibility into the branches that are compiled. In the example below, `releaseName` will be reported as unused as it is only referenced within the non-debug branch of the macro. @@ -470,11 +493,11 @@ struct BuildInfo { } ``` -You've a few options to workaround this: +You have a few options to workaround this: - Use [Comment Commands](#comment-commands) to explicitly ignore `releaseName`. - Filter the results to remove known instances. -- Run Periphery once for each build configuration and merge the results. You can pass arguments to the underlying build by specifying them after `--`, e.g `periphery scan ... -- -configuration release`. +- Run Periphery once for each build configuration and merge the results. You can pass arguments to the underlying build by specifying them after `--`, e.g. `periphery scan ... -- -configuration release`. ### Swift package is platform-specific @@ -485,10 +508,10 @@ As a workaround, you can manually build the Swift package with `xcodebuild` and Example: ```sh -# 1. use xcodebuild +# 1. Use xcodebuild xcodebuild -scheme MyScheme -destination 'platform=iOS Simulator,OS=16.2,name=iPhone 14' -derivedDataPath '../dd' clean build -# 2. use produced index store for scanning +# 2. Use produced index store for scanning periphery scan --skip-build --index-store-path '../dd/Index.noindex/DataStore/' ``` @@ -498,12 +521,10 @@ Due to some underlying bugs in Swift, Periphery may in some instances report inc | ID | Title | | :--- | :--- | -| [56559](https://github.com/apple/swift/issues/56559) | Index store does not relate constructor referenced via Self | | [56541](https://github.com/apple/swift/issues/56541) | Index store does not relate static property getter used as subscript key | | [56327](https://github.com/apple/swift/issues/56327) | Index store does not relate objc optional protocol method implemented in subclass | | [56189](https://github.com/apple/swift/issues/56189) | Index store should relate appendInterpolation from string literals | | [56165](https://github.com/apple/swift/issues/56165) | Index store does not relate constructor via literal notation | -| [49641](https://github.com/apple/swift/issues/49641) | Index does not include reference to constructor of class/struct with generic types | ## Sponsors ![Sponsors](assets/sponsor-20.svg)