A small experimental library containing generic types for performing search and replacement on Swift Syntax trees
SwiftSyntaxSearch
A small experimental library containing generic types for performing search and replacement on Swift Syntax trees.
Requirements
Swift 5.7
Searching
From a given syntax tree:
class AClass {
init() {
var decl: Int = 0
}
func member1() {
var decl2: Int = 0
var decl: Int = 1
}
func member2() {
var decl: Int = 0, decl2: Int = 0
}
}
We can query for all variable declarations whose first pattern binding index binds an identifier decl
to an initial value of 0
with the following search term:
let declOf0Search = SyntaxSearchTerm<VariableDeclSyntax>
.child(
\VariableDeclSyntax.bindings[index: 0],
matches:
(\PatternBindingSyntax.pattern).matches(
as: IdentifierPatternSyntax.self,
SyntaxReplacer<IdentifierPatternSyntax>
.token(\.identifier, matches: "decl")
) &&
(\PatternBindingSyntax.initializer?.value).matches(
as: IntegerLiteralExprSyntax.self,
SyntaxReplacer<IntegerLiteralExprSyntax>
.token(\.digits, matches: "0")
)
)
And search for the syntax tree like so:
syntax.findAllDepthFirst(declOf0Search)
// Returns syntax nodes:
// var decl: Int = 0
// var decl: Int = 0, decl2: Int = 0
Find and Replace
From a given syntax tree:
class AClass {
init() {
var decl: Int = 0
}
func member(_ param: Int = 1) {
var decl: Int = 2
}
}
let global = 3
We can find and replace all variable declarations that bind an integer of value 0
or 1
, and invoke a closure to construct a replacement to the syntax node:
let declOf0Or2Replacer =
SyntaxReplacer<IntegerLiteralExprSyntax>(searchTerm:
.or([
SyntaxReplacer<IntegerLiteralExprSyntax>
.token(\.digits, matches: "0"),
SyntaxReplacer<IntegerLiteralExprSyntax>
.token(\.digits, matches: "2"),
])
) { node in
node.withDigits(
SyntaxFactory.makeIntegerLiteral("50_" + node.digits.text)
)
}
And create a new the syntax tree with the replacements applied like so:
syntax.replacingAll(declOf0Or2Replacer)
// Prints the new syntax tree:
// class AClass {
// init() {
// var decl: Int = 50_0
// }
// func member(_ param: Int = 1) {
// var decl: Int = 50_2
// }
// }
//
// let global = 3
Creating search terms
The following syntaxes are available and produce the same result:
// Keypath-based binding
(\PatternBindingSyntax.pattern).matches(
as: IdentifierPatternSyntax.self,
SyntaxSearchTerm<IdentifierPatternSyntax>
.token(\.identifier, matches: "decl")
)
// Struct creation
SyntaxSearchTerm<PatternBindingSyntax>
.child(
// KeyPath<PatternBindingSyntax, T>
\.pattern,
// Cast `T` to IdentifierPatternSyntax, and if successful, invokes the matcher, otherwise matching fails.
castTo: IdentifierPatternSyntax.self,
// Match IdentifierPatternSyntax.identifier (a TokenSyntax) with a given StringMatcher (string literals match with `==`)
matches:
SyntaxSearchTerm<IdentifierPatternSyntax>
.token(\.identifier, matches: "decl")
)
// Appending to existing search term
let emptySearch = SyntaxSearchTerm<PatternBindingSyntax>()
let declIdentSearch = emptySearch
.child(
// KeyPath<PatternBindingSyntax, T>
\.pattern,
// Cast `T` to IdentifierPatternSyntax, and if successful, invokes the matcher, otherwise matching fails.
castTo: IdentifierPatternSyntax.self,
// Match IdentifierPatternSyntax.identifier (a TokenSyntax) with a given StringMatcher (string literals match with `==`)
matches:
SyntaxSearchTerm<IdentifierPatternSyntax>
.token(\.identifier, matches: "decl")
)
Search terms that inspect tokens can use the shortcut KeyPath<_, TokenSyntax>.==
to generate token string matches like with SyntaxSearchTerm.token(\.identifier, matches: "decl")
:
let declIdentSearch: SyntaxSearchTerm<IdentifierPatternSyntax>
declIdentSearch = \.identifier == "decl" // equivalent to declIdentSearch = .token(\.identifier, matches: "decl")
StringMatcher
A simple enum-based string matcher that performs matches based on equality, prefix, suffix or string containment. Used by SyntaxSearchTerm
to perform token-based string equality:
// StringMatcher.exact
let exact = StringMatcher.exact("a text")
print(exact.matches("a text")) // true
print(exact.matches("")) // false
print(exact.matches("A Text")) // false
print(exact.matches("a string containing a text with prefix and suffix")) // false
print(exact.matches("a text with suffix")) // false
print(exact.matches("prefix and then a text")) // false
// StringMatcher.contains
let contains = StringMatcher.contains("a text")
print(contains.matches("a text")) // true
print(contains.matches("")) // false
print(contains.matches("A Text")) // false
print(contains.matches("a string containing a text with prefix and suffix")) // true
print(contains.matches("a text with suffix")) // true
print(contains.matches("prefix and then a text")) // true
// StringMatcher.prefix
let prefix = StringMatcher.prefix("a text")
print(prefix.matches("a text")) // true
print(prefix.matches("")) // false
print(prefix.matches("A Text")) // false
print(prefix.matches("a string containing a text with prefix and suffix")) // false
print(prefix.matches("a text with suffix")) // true
print(prefix.matches("prefix and then a text")) // false
// StringMatcher.suffix
let suffix = StringMatcher.suffix("a text")
print(suffix.matches("a text")) // true
print(suffix.matches("")) // false
print(suffix.matches("A Text")) // false
print(suffix.matches("a string containing a text with prefix and suffix")) // false
print(suffix.matches("a text with suffix")) // false
print(suffix.matches("prefix and then a text")) // true