CheekyGhost-Labs/IndexStore
Swift library providing a query-based approach for searching for and working with Apple's indexstore-db library
Note:
The Apple IndexStoreDB Library is not considered stable yet as it has no resolvable semvar tags. This project points at a release branch found on the repo and is actively maintained.
Features:
- Query symbols and occurrences in your Swift source code
- Find the occurrences and references of symbols
- Access detailed information about symbols, such as their USR, source file location, parent, inheritance, and more
- Access detailed information about symbols (location, kind, parent, etc.)
- Filter and customize queries with various options, such as restricting to the project directory or specific source files
- Supports both Swift and Objective-C code
- Retrieve symbols conforming to a specific protocol
- Retrieve symbols subclassing a specific class
- Find invocations of a specific symbol
- Check if a symbol is invoked by a test case
- Identify empty extensions
### High Level Topics:
`IndexStore` is a layer of abstraction over the `indexstore-db`, aimed at simplifying queries and making codebase exploration more intuitive. Here are the key concepts that drive its functionality:
##### SourceSymbol:
At its core, every distinct named entity in your code, such as functions, classes, or variables, is represented as a symbol. When you query for symbols in `IndexStore`, you're seeking its primary definition and associated metadata.
##### USR (Unified Symbol Resolution:
Think of USR as the unique fingerprint of a symbol. It's a consistent identifier that ensures even if a symbol appears in various places or across projects, we know it's the same entity.
##### Occurrence:
`SourceSymbol` instances aren't just static entities; they live and breathe throughout your code. When you query for occurrences of a symbol, it aims to capture every specific instance or reference of a symbol in your project, revealing the footprint of that symbol in your codebase.
##### Related Occurrences:
Beyond just finding where a symbol exists or occurs, it's often vital to understand its broader context and relationships. Related Occurrences do just that, fetching symbols and instances that share a defined relationship with the queried symbol. This can encompass overrides, implementations, associations, and more, providing a richer tapestry of how a particular symbol interplays with others in your project.
### Getting Started:
To use `IndexStore` in your project, you'll need to instantiate an index with a valid `Configuration` instance.
The `Configuration` holds, among other things, paths to the project directory, libIndexStore, and IndexStoreDB database. The only required value to get started is the `projectDirectory` location, which is the working directory of the project you are assessing.
By default the configuration will automatically resolve the required `indexStorePath` and `libIndexStorePath` based on the running process. This will use `xcode-select` and `ProcessInfo().environment` to derive the index store details for the project within the `projectDirectory`.
You can also override this by providing your own values.
#### Instantiating:
Once you have your configuration ready, you can create an `IndexStore` instance:
```swift
// Manual Configuration
let configuration = Configuration(projectDirectory: "path/to/project/root")
instanceUnderTest = IndexStore(configuration: configuration)
```
The `Configuration` is also `Decodable` and can be built from a JSON file:
```swift
let configuration = try Configuration.fromJson(at: configPath)
instanceUnderTest = IndexStore(configuration: configuration)
```
#### Basic Usage
Once you have a configured `IndexStore` instance, you can begin querying for symbols:
1. Import `IndexStore`:
```swift
import IndexStore
```
2. Use the `IndexStore` instance to query for symbols, occurrences, or other information:
```swift
// Query for functions by name
let results = indexStore.querySymbols(.functions("someFunctionName"))
// Find all class symbols
let classSymbols = indexStore.querySymbols(.kinds([.class]))
// Find all extensions of a type
let results = indexStore.querySymbols(.extensions(ofType: "MyClass"))
// Find all extensions of a class within specific source files
let results = indexStore.querySymbols(.extensions(in: ["path", "path"], matching: "XCTest"))
// Find all invocations of a function symbol
let function = indexStore.querySymbols(.functions("someFunctionName"))[0]
let results = indexStore.invocationsOfSymbol(function)
// Find all symbols declared in a specific file
let symbols = indexStore.querySymbols(
.withSourceFiles(["/path/to/your/project/SourceFile.swift"])
.withKinds(SourceKind.allCases)
.withRoles(.all)
)
```
#### Occurrence Lookups
Once you have a symbol, whether by general querying or by plucking things from other results, you can also look up occurrences by a symbol (or usr). You can additionally provide a valid query to further filter results.
```swift
// Find UIColor declaration
let colorSymbol indexStore.querySymbols(
.withQuery("UIColor")
.withAnchorStart(true)
.withAnchorEnd(true)
.withRestrictingToProjectDirectory(false)
.withKinds([.class])
.first!
// Look up any occurrences of the UIColor symbol
let occurrences = indexStore.queryOccurrences(ofSymbol: colorSymbol, query: .empty)
```
#### Relation Lookups
Once you have a symbol, whether by general querying or by plucking things from other results, you can also look up related occurrences by a symbol (or usr). You can additionally provide a valid query to further filter results.
```swift
// Find UIColor declaration
let colorSymbol indexStore.querySymbols(
.withQuery("UIColor")
.withAnchorStart(true)
.withAnchorEnd(true)
.withRestrictingToProjectDirectory(false)
.withKinds([.class])
.first!
// Look up any occurrences of the UIColor symbol
let occurrences = indexStore.queryRelatedOccurrences(ofSymbol: colorSymbol, query: .empty)
```
#### Convenience Methods
IndexStore provides convenience methods for common static analysis tasks:
- Find symbols conforming to a specific protocol:
```swift
let conformingSymbols = indexStore.sourceSymbols(conformingToProtocol: "SomeProtocol")
```
- Find symbols subclassing a specific class:
```swift
let subclassingSymbols = indexStore.sourceSymbols(subclassing: "SomeClass")
```
- Find invocations of a specific symbol:
```swift
let invocations = indexStore.invocationsOfSymbol(someSymbol)
```
- Check if a symbol is invoked by a test case:
```swift
let isInvokedByTestCase = indexStore.isSymbolInvokedByTestCase(someSymbol)
```
- Identify empty extensions:
```swift
let emptyExtensions = indexStore.sourceSymbols(forEmptyExtensionsMatching: "SomeType")
```
#### Delegate & Out-of-Date Unit Tracking
`IndexStore` supports delegation via `IndexStoreDelegate` to observe index store activity and out-of-date unit detection.
**Setting a delegate:**
```swift
class MyDelegate: IndexStoreDelegate {
func indexStore(_ store: IndexStore, didUpdatePendingUnitCount pendingUnitCount: Int) {
print("Pending units: \(pendingUnitCount)")
}
func indexStore(_ store: IndexStore, didDetectOutOfDateUnit unit: UnitInfo) {
print("Out-of-date unit detected: \(unit.unitName) — \(unit.triggerHintDescription)")
}
func indexStore(_ store: IndexStore, didProcessOutOfDateUnit trackedUnit: TrackedUnit) {
print("Unit '\(trackedUnit.unit.unitName)' processed — status: \(trackedUnit.status)")
}
}
let delegate = MyDelegate()
indexStore.delegate = delegate
```
All delegate methods are optional except `didUpdatePendingUnitCount` and `didDetectOutOfDateUnit`. The `didProcessOutOfDateUnit` callback has a default empty implementation so existing adopters are not broken.
**Inspecting out-of-date units:**
When out-of-date detection is enabled, units reported by IndexStoreDB are tracked with a status lifecycle of `.outOfDate` -> `.processing` -> `.processed`:
```swift
// Get all units currently marked as out-of-date
let staleUnits = indexStore.outOfDateUnits
// Get a full snapshot of all tracked units (outOfDate, processing, and processed)
let allTracked = indexStore.trackedUnits
```
**Processing out-of-date units:**
You can request the index store to re-process specific out-of-date units, or all of them at once:
```swift
// Process all out-of-date units
indexStore.processOutOfDateUnits()
// Or inspect and process a subset
let staleUnits = indexStore.outOfDateUnits
let selected = staleUnits.filter { $0.unit.unitName.contains("MyModule") }
indexStore.processOutOfDateUnits(selected)
```
Each unit transitions through `.processing` (while being re-imported) and then `.processed` once complete. The delegate's `didProcessOutOfDateUnit` callback fires when the unit reaches `.processed`.
**Clearing processed history:**
```swift
// Remove only units that have reached `.processed` status
indexStore.clearProcessedUnits()
// Remove all tracked units regardless of status
indexStore.clearAllTrackedUnits()
```Installation
Swift Package Manager
Add the following to your Package.swift file:
let package = Package(
// name, platforms, products, etc.
dependencies: [
// other dependencies
.package(url: "https://github.com/CheekyGhost-Labs/IndexStore.git", branch: "release/3.3"),
],
targets: [
.executableTarget(name: "<command-line-tool>", dependencies: [
// other dependencies
.product(name: "IndexStore", package: "IndexStore")
]),
// other targets
]
)License
IndexStore is available under the MIT license. See the LICENSE file for more info.
Contribution
Submitting a Bug Report
Swift Markdown tracks all bug reports with GitHub Issues. You can use the "IndexStore" component for issues and feature requests specific to IndexStore. When you submit a bug report we ask that you are descriptive and include as much information as possible to document or re-create the issue.
Submitting a Feature Request
For feature requests, please feel free to file a GitHub issue
Don't hesitate to submit a feature request if you see a way IndexStore can be improved to better meet your needs.
Contributing to IndexStore
Due to Apple/Swift IndexStoreDB Library repo using branches for releases rather than tagging stable versions, the IndexStore repo can't follow the traditional semvar and Git Flow approach.
The approach IndexStore takes for releases is:
- main: contains the latest stable release
- develop: contains the latest stable changes pending release
- release/<major>.minor: contains released code
A release branch will have a semantic version without accounting for patch updates. For example
release/1.0
release/1.1
release/3.2
etc- any
patchchanges (bug fixes and improvements that don't change the public interface) will be pulled into the appropriate release branch as needed.
- any
minorupdates (publicly visible changes that are backwards compatible) will get their own release branch.
- any
majorupdates (publicly visible changes that are not backwards compatible) will get their own release branch.
Releases will still be tagged for when the Apple IndexStoreDB Library becomes stable. This will also allow us to manage patch releases easier too.
For the most part, pull requests should be made against the develop branch to coordinate releases with multiple features and fixes. This also provides a means to test from the develop branch in the wild to further test pending releases. Once a release is ready it will be merged into main and release branches created/updated from the main branch.
If a fix for an older version is being made, the pull request can be made against the intended release branch, and the change can be worked into the other branches with the help of maintainers as needed.
To get started:
- Fork the repository: Start by creating a fork of the project to your own GitHub account.
- Clone the forked repository: After forking, clone your forked repository to your local machine so you can make changes.
git clone https://github.com/CheekyGhost-Labs/IndexStore.git- Create a new branch: Before making changes, create a new branch for your feature or bug fix. Use a descriptive name that reflects the purpose of your changes.
git switch -c your-feature-branch- Follow the Swift Language Guide: Ensure that your code adheres to the Swift Language Guide for styling and syntax conventions.
- Make your changes: Implement your feature or bug fix, following the project's code style and best practices. Don't forget to add tests and update documentation as needed.
- Commit your changes: Commit your changes with a descriptive and concise commit message. Use the imperative mood, and explain what your commit does, rather than what you did.
# Feature
git commit -m "Feature: Adding convenience method for resolving awesomeness"
# Bug
git commit -m "Bug: Fixing issue where awesome query was not including awesome"- Pull the latest changes from the upstream: Before submitting your changes, make sure to pull the latest changes from the upstream repository and merge them into your branch. This helps to avoid any potential merge conflicts.
git pull origin develop- Push your changes: Push your changes to your forked repository on GitHub.
git push origin your-feature-branch- Submit a pull request: Finally, create a pull request from your forked repository to the original repository, targeting the
developbranch. Fill in the pull request template with the necessary details, and wait for the project maintainers to review your contribution.
Unit Testing
Please ensure you add unit tests for any changes. The aim is not 100% coverage, but rather meaningful test coverage that ensures your changes are behaving as expected without negatively effecting existing behaviour.
Please note that the project maintainers may ask you to make changes to your contribution or provide additional information. Be open to feedback and willing to make adjustments as needed. Once your pull request is approved and merged, your changes will become part of the project!
Package Metadata
Repository: CheekyGhost-Labs/IndexStore
Stars: 53
Forks: 5
Open issues: 1
Default branch: develop
Primary language: swift
License: MIT
Topics: apple, ios, macos, swift, xcode
README: README.md