SwiftLint icon indicating copy to clipboard operation
SwiftLint copied to clipboard

Feature Request: Custom Rules written in Swift

Open cltnschlosser opened this issue 3 years ago • 4 comments

Currently custom rules are limited to regular expressions which means they miss out on a lot of the power that the built-in rules can take advantage of. At my company we have the need for custom linting (which can't be upstreamed because the rules are project specific). What we currently do is the following:

  1. Fork SwiftLint.
  2. Add custom rules in a new directory under Sources/SwiftLintFramework/Rules/
  3. Run sourcery to generate PrimaryRuleList
  4. Use Mint to install/run our fork of SwiftLint

This works pretty well, but there is a bit of pain for adopting changes from upstream (As any fork as). Normally we develop in a release branch and then rebase(or cherry-pick) our custom commits onto the next release branch.

Questions for maintainers and the community:

  1. First, is there a better way to do this that already exists? (Always smart to ask this question)
  2. Is something like a plugin architecture something that makes sense for SwiftLint? SwiftLint --plugin=/path/to/MyPlugin.framework which could allow plugins to register new rules. I have a vague idea of how this might work and what changes would be required in SwiftLint if people want to discuss this further. (Likely requires dylibs or use of @_cdecl)

Alternative approaches that I've thought of are:

  1. Refactoring SwiftLint/SwiftLintFramework to make it easier to include them as a package dependency without duplicating too much of the existing logic. Make SwiftLintFramework (or new intermediary framework) contain more of the logic required to run SwiftLint, so that it can be easier to run from code and allow injection of additional Rules. Ideally you have a main.swift that's as simple as SwiftLint.run(customRuleLists: [MyCustomRules()]).
  2. Refactoring SwiftLint/SwiftLintFramework to make a fork more lightweight. This is the least impactful, but also the least work. Could make it easier to inject custom rules without modifying PrimaryRuleList (Might reduce conflicts when updating fork).

Thoughts?

cltnschlosser avatar Feb 07 '21 16:02 cltnschlosser

This is definitely a key feature enhancement for larger organizations to use SwiftLint. Forking is indeed the current solution.

Not sure if you saw https://github.com/realm/SwiftLint/issues/2529 but I think a plugin system is already discussed as the right approach here.

With alternative 1, is the idea that SwiftLint releases will be both as a unix executable and available as a .framework to use in your own exe?

ebgraham avatar Mar 18 '21 20:03 ebgraham

Not sure if you saw #2529 but I think a plugin system is already discussed as the right approach here.

No, I hadn't seen that effort. I subscribed to the thread now. Does look like an easier approach than trying to load something from a dynamic framework.

With alternative 1, is the idea that SwiftLint releases will be both as a unix executable and available as a .framework to use in your own exe?

Yeah. It technically already is released this way with Swift Package Manager (SwiftLintFramework), but currently there is a lot of logic in the executable itself, so there would be a lot of code duplication between a custom linter using SwiftLintFramework and SwiftLint itself. I was thinking a refactor that moves this project to a model like https://github.com/apple/swift-driver/blob/main/Sources/swift-driver/main.swift where the executable is rather simple and all the logic exists in framework. I think SwiftLint would have a bit more complexity in its executable than swift-driver because the framework shouldn't be handling arguments. I guess you could add SwiftLintConfiguration or something that parses arguments and gives you resulting commands to run (that a custom executable could modify, ex: inject more rules).

I guess it could be as simple as moving everything but the main.swift into a framework and having https://github.com/realm/SwiftLint/blob/4039eff9848a02447e840cc93447c14fdca16495/Source/swiftlint/main.swift#L11 be the customization point. A custom variant of SwiftLint would pull in SwiftLint as a package and then call mainHandlingDeprecatedCommands(additionalRules: []) with optional additional rules.

cltnschlosser avatar Mar 19 '21 01:03 cltnschlosser

@ebgraham I'm just going to ping you, because I accidentally left an edit half typed here and I'm not sure how much of it was not in the original message that you may have seen.

cltnschlosser avatar Mar 19 '21 19:03 cltnschlosser

I've explored a lot of different technical options for this over the years. From invoking the Swift compiler when running SwiftLint, forking the SwiftLint process, running a daemon, communicating with plugins via XPC, and other things. I've never been happy with the developer experience or the performance of any of my explorations. Until now.

I've just pushed up a PR that adds support for writing private, native custom SwiftLint rules written in Swift just like all of SwiftLint's official built-in rules: https://github.com/realm/SwiftLint/pull/4039

It does require building SwiftLint using Bazel, so it won't be something everyone's comfortable with, but if you're willing to try it I think it has the potential to be really powerful. Especially if we end up going forward with a SwiftSyntax rule DSL like what's being proposed in https://github.com/realm/SwiftLint/pull/4023

jpsim avatar Jul 26 '22 05:07 jpsim

With #4039 this is now resolved. More info in this twitter thread: https://twitter.com/simjp/status/1558168288026312705

jpsim avatar Aug 23 '22 20:08 jpsim