Hi everyone! I’m Filip Sakellariou and I’ll be working on implementing a qualified-lookup query API to SwiftSyntax’s SwiftLexicalLookup module along with my mentor Pavel Yaskevich (@xedin) over the summer as part of GSoC 2026. In this post, I want to understand use cases community members might have for this feature, and propose a preliminary API.
Introduction
As part of his 2024 GSoC project, Jakub implemented the SwiftLexicalLookup module that provides unqualified-lookup capabilities. Unqualified lookup is the process of determining the declaration to which a given identifier node refers. For instance, the following lookup for b:
struct MyStruct {
func myfunc(a: Int, b: Int?) {
if let b {
print(b) // <-- Lookup `b` here
}
}
}
would find the if let b declaration, the b: Int? function argument, and request further lookup in MyStruct.
Qualified lookup looks into a nominal type declaration and finds relevant declarations, providing another powerful AST facility that will allow build tools to build even more powerful macros and libraries. For instance, suppose we look up count declaration inside the Sub class:
class Base {
var count: Int { 0 } // <- Result 2️⃣
}
class Sub: Base {
override var count: Int { 1 } // <- Result 1️⃣
}
Then, we’d return the declarations
in the Sub class itself, and
in the superclass.
Motivation
Currently, build tools’ main way of performing qualified lookup is through SourceKitten. However, SourceKitten has to dynamically link the compiler backend’s SourceKit library, causing a multitude of hard-to-debug linker errors, especially on non-MacOS platforms like Linux. Apart from distribution challenges, SourceKit’s API is fundamentally designed to service Integrated Development Environments, and doesn’t naturally translate to general build-tool queries.
There’s already evidence that existing SourceKitten clients want to take advantage of SwiftSyntax’s more advanced AST facilities. For example, SwiftLint now utilizes SwiftLexicalScopes to perform unqualified lookup for some of its linting rules. Qualified lookup could offer more precise diagnostics and enable more linting rules (as I outline in the proposal). SwiftJava also uses unqualified lookup and could benefit from the proposed qualified lookup functionality.
Qualified lookup will provide a pure-Swift, reliable solution for build tools to reason about a type’s members, enabling more powerful build tools.
Detailed Design
Based on Doug, Jakub and Pavel’s comments on my initial GSoC proposal, I want to share an early draft of what the API might look like:
extension DeclGroupSyntax {
/// Search for members matching given identifier.
/// Parameters:
/// - lookUpPosition: Indicates position where we should
/// start lookup. Will enable future expansion that filters
/// by access control.
func lookupMember(
_ identifier: Identifier?,
from lookUpPosition: AbsolutePosition?,
with config: QualifiedLookupConfig = QualifiedLookupConfig()
) -> [QualifiedLookupResult]
In the method above, DeclGroupSyntax basically refers to any nominal type declaration (structs, enums, protocols, classes, actors) and extensions. Then, we specify the identifier we want to look up within this group syntax, along with the position where lookup should commence and additional configuration options. For now, configuration options include an optional ConfiguredRegions type for handling things like #if. Given this information, the qualified-lookup facility returns possible results:
enum QualifiedLookupResult {
/// Explicitly declared members.
case members([DeclSyntax], introducedIn: DeclSyntax)
/// Members declared in conditional extensions.
case conditionalMembers(
[DeclSyntax], introducedIn: DeclSyntax,
inheritanceClause: InheritanceClauseSyntax?,
genericClause: GenericWhereClauseSyntax?)
/// Implicit members in the given group declaration.
case implicitMembers([DeclSyntax], introducedIn: DeclGroupSyntaxType)
/// Need further lookup for `@dynamicMemberLookup`-annotated type
case lookForDynamicMembers(
dynamicMemberSubscripts: [SubscriptDeclSyntax],
introducedIn: DeclGroupSyntaxType)
/// Need further lookup for macro attributes.
case lookForMacros(
potentialMacroDecl: [DeclSyntax],
introducedIn: DeclGroupSyntaxType)
/// Look for any types we encountered in the lookup that weren't in
/// the SymbolTable syntax trees (e.g. the looked up type conforms to
/// a protocol from a different module).
case lookForSupertypes(
inheritedFrom: InheritanceClauseSyntax,
genericClause: GenericWhereClauseSyntax?)
}
More specifically, here’s what each result type means
-
Members
These are explicitly declared members, including: static/class/instance stored and computed properties, functions, subscripts and initializers, dynamic-member-lookup results, along with nested types, type aliases and associated types. Consider the following struct, for instance:
struct MyStruct<T> { func callAsFunction() {} }Qualified lookup within the MyStruct scope would return the
callAsFunction()function declaration. Note that qualified lookup won't surface operator functions, objc functions using dynamic lookup, and generic parameters like MyStruct.T (semantically wrong). -
Conditional members
These are members declared in conditional extensions, such as:
extension Array where Element == Int { func sum() -> Int { reduce(0, +) } }Here, qualified lookup will return the
sum()declaration in the extension above together with thewhere Element == Intclause. -
Implicit members
Members the user didn’t explicitly write, including
self,Typeand synthesized initializers. -
Prompt dynamic-member lookup
Types and protocols annotated with
@dynamicMemberLookuphave one or more subscripts with adynamicMemberstring or keypath argument. We defer to the type-checker to determine what members these subscripts can produce. -
Prompt macro-attribute lookup
Any unknown attributes that could be attached macros or property wrappers that expand to more declarations. For instance:
struct MyView { @State var myState = 0 }instructs the tooling to look up what the
@Stateattribute expands to in the variable declaration above (if anything). -
Prompt supertype lookup
Types can conform to protocols, classes may have superclasses and protocols may refine other protocols; these are all examples of "supertypes" on which we have to perform lookup. For example, if our group declaration is a struct conforming to some protocol, we might also need to look up declarations on that protocol.
Although this API is somewhat useful on its own, it can only access one group declaration, making it quite limited. So we can introduce a symbol table that tracks and caches lookup results for an entire source file:
struct SymbolTable {
/// Construct a table for caching symbol lookup
/// for the given file syntax.
init(fileSyntax: SourceFileSyntax)
}
extension DeclGroupSyntax {
/// Same as `lookupMember` above but can visit supertypes in
/// the symbol table's `fileSyntax`.
func lookupMember(
_ identifier: Identifier?,
from lookUpPosition: AbsolutePosition?,
using symbolTable: inout SymbolTable,
with config: QualifiedTableLookupConfig = QualifiedTableLookupConfig()
) -> [QualifiedLookupResult]
}
struct QualifiedTableLookupConfig {
/// Parameters:
/// - lookupSuperprotocols: Whether to recursively look up
/// inherited or conformed-to protocols.
/// - lookupSuperclasses: Whether to recursively look up
/// superclasses, and the protocols they conform to (if
/// `lookupSuperprotocols` is true).
init(
lookupSuperprotocols: Bool = true,
lookupSuperclasses: Bool = true,
configuredRegions: ConfiguredRegions? = nil)
}
As you can see, the symbol table allows the qualified-lookup facility to directly perform some supertype lookup. Here's an example:
// File.swift
protocol MyProto {
func myfunc()
}
extension MyProto {
func myfunc() {}
}
struct MyStruct: MyProto {}
Suppose we initialize SymbolTree with this entire file and look up myfunc in MyStruct. Qualified lookup can look into the MyProto "supertype" and directly find myfunc, instead of just prompting us to look into the MyProto protocol ourselves. Of course, we can configure whether this supertype lookup should occur for protocols, classes, both or neither.
Since this API works directly on syntax trees, it notably lacks important compiler functionality. We not attempt to perform overload resolution, constraint solving, operator lookup, or AnyObject dynamic lookup. These are left for the client to handle, as with unqualified lookup.
Feedback & Other Use Cases
Does the preliminary API look feasible or did I miss something? Please also let me know if you have a cool use case for qualified lookup so we can make sure that the new API handles it.
Thank you all for your feedback during the GSoC application phase and for taking the time to read this proposal.