IndexStore is a library providing that provides a query-based approach for searching for and working with source symbols within an indexed code base. It is built on top of the Apple IndexStoreDB Library, which provides access to the index data produced by the swift compiler.
With this library, you can easily search for and analyze symbols in your code, making it a powerful tool for building developer tools, static analyzers, and code refactoring utilities.
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.
- 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
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:
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.
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.
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.
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.
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.
Once you have your configuration ready, you can create an IndexStore
instance:
// 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:
let configuration = try Configuration.fromJson(at: configPath)
instanceUnderTest = IndexStore(configuration: configuration)
Once you have a configured IndexStore
instance, you can begin querying for symbols:
- Import
IndexStore
:
import IndexStore
- Use the
IndexStore
instance to query for symbols, occurrences, or other information:
// 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)
)
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.
// 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)
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.
// 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.queryRelatedOccurences(ofSymbol: colorSymbol, query: .empty)
IndexStore provides convenience methods for common static analysis tasks:
- Find symbols conforming to a specific protocol:
let conformingSymbols = indexStore.sourceSymbols(conformingToProtocol: "SomeProtocol")
- Find symbols subclassing a specific class:
let subclassingSymbols = indexStore.sourceSymbols(subclassing: "SomeClass")
- Find invocations of a specific symbol:
let invocations = indexStore.invocationsOfSymbol(someSymbol)
- Check if a symbol is invoked by a test case:
let isInvokedByTestCase = indexStore.isSymbolInvokedByTestCase(someSymbol)
- Identify empty extensions:
let emptyExtensions = indexStore.sourceSymbols(forEmptyExtensionsMatching: "SomeType")
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/2.0"),
],
targets: [
.executableTarget(name: "<command-line-tool>", dependencies: [
// other dependencies
.product(name: "IndexStore", package: "IndexStore")
]),
// other targets
]
)
IndexStore is available under the MIT license. See the LICENSE file for more info.
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.
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.
Due to Apple 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/.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
-
any
patch
changes (bug fixes and improvements that don't change the public interface) will be pulled into the appropriate release branch as needed. -
any
minor
updates (publicly visible changes that are backwards compatible) will get their own release branch. -
any
major
updates (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
develop
branch. Fill in the pull request template with the necessary details, and wait for the project maintainers to review your contribution.
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!