-
Notifications
You must be signed in to change notification settings - Fork 2.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add a declarative DSL to write rules that use SwiftSyntax #4023
base: main
Are you sure you want to change the base?
Conversation
Generated by 🚫 Danger |
Nice proof of concept! I'm supportive of something like this, but would like to see it in use before merging it. We have a handful of SwiftSyntax-based rules, maybe one of those would be a good candidate to migrate to this DSL? Thanks for the PR :) |
Codecov Report
@@ Coverage Diff @@
## master #4023 +/- ##
==========================================
- Coverage 92.46% 92.41% -0.05%
==========================================
Files 448 463 +15
Lines 22798 23160 +362
==========================================
+ Hits 21080 21404 +324
- Misses 1718 1756 +38
📣 Codecov can now indicate which changes are the most critical in Pull Requests. Learn more |
@jpsim thank you for the feedback! I've modified ForceCastRule to use the DSL. Please let me know if there any additional changes I can make. |
Apologies for the delay in getting back to you. If the main goal of introducing this DSL is to make it easier to build SwiftSyntax-based rules by removing ceremony I feel like this accomplishes the opposite, where you need to first write a Contrast this with how concise SwiftSyntax rules are today and I don't think this is an improvement. That being said, I do think there's an opportunity to remove some boilerplate involved with SwiftLint rules that use SwiftSyntax, but I don't think this is it. If I'm missing something, maybe we can find a way to have a call where you can sell me on the advantages here that I'm missing. |
As an exploration, I've pushed a prototype of what some SwiftSyntax/SwiftLint convenience helpers could look like in #4126 It allows rules to provide visitors/rewriters and the framework takes care of the rest: public struct MyCustomRule: SwiftSyntaxCorrectableRule, ConfigurationProviderRule {
public var configuration = SeverityConfiguration(.warning)
public init() {}
public static let description = RuleDescription(...)
public func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor? {
file.locationConverter.map(MyCustomRuleVisitor.init)
}
public func makeRewriter(file: SwiftLintFile) -> ViolationsSyntaxRewriter? {
file.locationConverter.map { locationConverter in
MyCustomRuleRewriter(
locationConverter: locationConverter,
disabledRegions: disabledRegions(file: file)
)
}
}
} That's a pretty declarative way to define rules, removing a lot of boilerplate. |
Hi JP, sorry for the late reply! I took a look at your PR and it looks great. I like how it standardizes how the SwiftSyntax rules validates a file and removes a lot of the ceremony needed. It also accounts for correctable rules which my PR does not. Regarding the goals of the DSL I proposed, I was hoping it would solve the use cases when we have a lint violation that is nested in nature. The biggest use case for these nested AST rules are for enforcing best practices for libraries and frameworks. As an example:
To write a visitor for this rule, we could write something like:
The problems with the above solution are:
With a DSL, we could change the above code to something like:
The above code solves problems 1 and 2 because at a glance we can see what we want the visitor to do: for all classes that inherit from component, look for call expressions with the name "expensiveFunction". It solves number 3 by adding lint modifiers such as inheritsFrom and name where we can abstract how we're getting this information in case swift syntax's API changes and it can be changed and tested all in one place. This is a simple example, but the complexity can grow quite large especially when we want more fine grained lint rules. I agree that the DSL introduces more boilerplate by creating a mapping from a DSL matcher to its respective visitor classes. Maybe we can leverage some code generation in order to reduce this boilerplate. I think this duplication is worth the benefits gained from the DSL as it would help in migrating current rules to SwiftSyntax, protect our rules from any changes to the SwiftSyntax API, and open the doors for custom native rules to be more powerful. |
This PR proposes an abstraction over writing rules that depend on SwiftSyntax. The abstraction will be a SwiftUI-like declarative DSL to write rules. The DSL will be made up of various
Matchers
that will map to common SwiftSyntax nodes such as class declarations, function call expressions...etc. As a proof of concept, this PR creates the MatchersClass
andDownCast
which match class declarations and "as" expressions respectively. To see it in use, we modify theForceCastRule
to use this new DSL.Proposal
A declarative DSL to write rules that search the AST produced by SwiftSyntax for certain nodes.
Motivation
At my company, we write a lot of lint rules that depend on the AST of Swift source code. A common lint rule is "prohibit the use of a deprecated function within classes that conform to a specific protocol". We've built a tool that does in the past called NEAL.
Adding SwiftSyntax unlocked powerful parsing and inspection of the Swift AST in SwiftLint. Currently, rules that use SwiftSyntax have a private
SyntaxVisitor
implementation that walks through the AST starting at the root and collects the positions of lint violations. The SyntaxVisitor is a powerful API, but it has some drawbacks.Drawbacks of SyntaxVisitor
visit
functions which correspond to the current syntax node being visited. This makes debugging and refactoring difficult.Goals
Design
The resultBuilder language feature is the perfect tool to simplify working with SyntaxVisitors since resultBuilders target list and tree structures. We'll use a resultBuilder to build up our SyntaxVisitors.
Let's see a rule written with this new DSL. Our example rule will be: "For all tests, prohibit the use of a deprecated API
deprecatedTestHelper()
"Our rule would look something like:
Here, the
validator
describes how to traverse the AST of the source file and collect violations.Class
andCallExpression
areMatchers
that will look for the equivalent syntax nodes. TheinheritsFrom
function is aLint Modifier
that will look inside of classes that inherit from XCTestCase. The closure expression afterClass
denotes a child visitor. For all children of the matching class declaration node, look for all call expressions with the name "deprecatedTestHelper".The actual lint violations will be raised by the nodes that do not have child visitors. In other words "leaf nodes" such as the CallExpression above will raise the lint violation for our rule, and its position in the Swift file will be collected by the visitor.
Implementation
Rules
To create a rule that uses this abstraction, conform to
SyntaxVisitorRule
. It requires you to implement avalidator
which is a collection of syntax visitors that will be used to traverse the AST of the Swift file and collect violations. It provides default functionality for taking those violation positions and turning them intoStyleViolations
.Validator
The validator is built by using a resultBuilder named
SyntaxVisitorRuleValidatorBuilder
. This resultBuilder allows us to define our rules with our custom DSL. The DSL is made up ofMatchers
andViolationSyntaxVisitors
.Matchers
Used to find a single Swift Syntax node. It can be filtered by applying
Lint Modifiers
which are methods which changes the attributes to look for. Matchers are responsible for creating ViolationSyntaxVisitors and its relationships to other matchers or visitors.ViolationSyntaxVisitors
Traverses the AST and collects the positions of a node if it violates according to the attributes it was passed.
Future Considerations
In the future, I hope to add more
Matchers
for various syntax nodes. We could also consider adding more operators such as anyOf() or all() or even scoped matchers (look only at the next immediate child node) to the DSL.Alternatives Considered
Define these rules in the config file
I considered defining these rules in the .swiftlint.yml file. Doing so would allow us to create rules without creating a new release of SwiftLint for each new rule. However, the maintenance of these config files would be expensive as we continue modifying these abstractions whenever we add more functionality or conform to updates from SwiftSyntax. The biggest downside is losing the debugging and testability capabilities of writing these rules in native Swift.