Skip to content

Add #examples and #corrections macros to DRY up rule description#6794

Open
ZevEisenberg wants to merge 9 commits into
realm:mainfrom
ZevEisenberg:examples-macro
Open

Add #examples and #corrections macros to DRY up rule description#6794
ZevEisenberg wants to merge 9 commits into
realm:mainfrom
ZevEisenberg:examples-macro

Conversation

@ZevEisenberg

@ZevEisenberg ZevEisenberg commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

When I wrote the Example type in #3040, it improved the experience of writing and debugging a rule (because test failures would point at precisely the example case that failed the test). It did this by bundling up the file and line number for each example, which can be passed along to the test assertion functions. But it came at the expense of a lot of repetitive Example("...") call sites.

Now that macros exist, I propose using them to keep the benefits of Example while cleaning up the call sites. The new macros transform [String] and [String: String] to [Example] and [Example: Example], respectively.

If it's helpful for review, here's a Claude-generated script that confirms that the changes to the rule files are just adding the macro and removing the Example wrapper:
review_mechanical_changes.py

Alternatives/Questions

  1. If you like this format, would you want to add an internal regex lint rule to encourage using the macros in new rules? It's encouraged in the doc comment, but not required.
  2. Instead of individual macros for array and dictionary, I considered a macro that would replace the entire init of RuleDescription. But this seemed like an overreach, and would prevent falling back to passing variables or non-literals, which limits flexibility and experimentation.
  3. How do we feel about naming? I think we could consolidate both macros into a single overloaded one called #examples, but that seemed too cute and not clear enough at the point of use.

@SwiftLintBot

SwiftLintBot commented Jun 28, 2026

Copy link
Copy Markdown
1 Warning
⚠️ Big PR
19 Messages
📖 Building this branch resulted in a binary size of 28297.82 KiB vs 28200.24 KiB when built on main (0% larger).
📖 Linting Aerial with this PR took 0.67 s vs 0.65 s on main (3% slower).
📖 Linting Alamofire with this PR took 0.97 s vs 0.93 s on main (4% slower).
📖 Linting Brave with this PR took 6.02 s vs 6.03 s on main (0% faster).
📖 Linting DuckDuckGo with this PR took 25.56 s vs 25.54 s on main (0% slower).
📖 Linting Firefox with this PR took 10.4 s vs 10.41 s on main (0% faster).
📖 Linting Kickstarter with this PR took 7.52 s vs 7.35 s on main (2% slower).
📖 Linting Moya with this PR took 0.39 s vs 0.37 s on main (5% slower).
📖 Linting NetNewsWire with this PR took 2.36 s vs 2.38 s on main (0% faster).
📖 Linting Nimble with this PR took 0.57 s vs 0.57 s on main (0% slower).
📖 Linting PocketCasts with this PR took 6.92 s vs 7.0 s on main (1% faster).
📖 Linting Quick with this PR took 0.33 s vs 0.33 s on main (0% slower).
📖 Linting Realm with this PR took 2.49 s vs 2.46 s on main (1% slower).
📖 Linting Sourcery with this PR took 1.51 s vs 1.49 s on main (1% slower).
📖 Linting Swift with this PR took 4.26 s vs 4.24 s on main (0% slower).
📖 Linting SwiftLintPerformanceTests with this PR took 0.16 s vs 0.16 s on main (0% slower).
📖 Linting VLC with this PR took 1.09 s vs 1.14 s on main (4% faster).
📖 Linting Wire with this PR took 15.23 s vs 15.21 s on main (0% slower).
📖 Linting WordPress with this PR took 9.8 s vs 9.84 s on main (0% faster).

Generated by 🚫 Danger

@ZevEisenberg ZevEisenberg changed the title Add #examples and #examplesDictionary macros to DRY up rule description Add #examples and #examplesDictionary macros to DRY up rule description Jun 29, 2026

@SimplyDanny SimplyDanny left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, @ZevEisenberg, for keeping an eye on test example setup over the years! 👍

A few thoughts, remarks and ideas from my side:

  • Ideally, protocol conformances support default parameters at some point. Then we can make Example ExpressibleByStringLiteral and so avoid any manual conversion.
  • I'd prefer to only have a single macro #examples that accepts either an array or a dictionary. Usage should be clear from the context of usage alone.
  • It might be worth supporting some of Examples parameters as macro arguments as well. There are a few cases where a parameter must be applied to all examples or at least a subset of them.
  • To further reduce boilerplate, we could try to get rid of the brackets wrapping the examples by using variadic parameters. At least for arrays, that should work.
  • The dictionary case is a little more tricky. Instead of a dictionary, we could have a list of CorrectionExample objects that wrap two Examples. They could be created with a custom operator =>, e.g.
    corrections: #examples(
      "let foo = 1" => "let bar = 1",
      ...
    )

Comment thread Source/SwiftLintCore/Helpers/Macros.swift Outdated
Comment thread Source/SwiftLintCore/Helpers/Macros.swift Outdated
Comment thread Source/SwiftLintCoreMacros/Examples.swift
Comment on lines +63 to +67
let example: ExprSyntax = if let fileID = context.location(
of: expression, at: .afterLeadingTrivia, filePathMode: .fileID
), let filePath = context.location(
of: expression, at: .afterLeadingTrivia, filePathMode: .filePath
) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a bit hard to read. I'd separate declaration from optional binding in the if expression. And actually, it should be an error if there's no source location.

Co-authored-by: Danny Mösch <danny.moesch@icloud.com>
@ZevEisenberg

ZevEisenberg commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

It might be worth supporting some of Examples parameters as macro arguments as well. There are a few cases where a parameter must be applied to all examples or at least a subset of them.

@SimplyDanny can you say more? Does this affect any code in the project today, or is it hypothetical?

Co-authored-by: Danny Mösch <danny.moesch@icloud.com>
@ZevEisenberg ZevEisenberg marked this pull request as draft June 30, 2026 18:27
@ZevEisenberg

Copy link
Copy Markdown
Contributor Author

@SimplyDanny can you say more? Does this affect any code in the project today, or is it hypothetical?

Oh, actually, I see what you mean. I had missed a bunch of places where we call Example directly, and some of them use extra utility functions like wrapExample in DiscouragedOptionalBooleanRuleExamples.swft. I'll see what I can do about those.

@ZevEisenberg

Copy link
Copy Markdown
Contributor Author

Another one I'm seeing in DuplicateImportsRuleExamples.swift:

            Example("""
            ↓import A.B.C
            ↓import A.B
            import A

            """, excludeFromDocumentation: true): Example("""
                import A

                """),

The approach I'm currently trying: making #examples and #examplesDictionary take more permissive types, allowing us to mix and match strings with Example instances, and then adding an overloaded init to Example that takes an Example and then replaces its file and line.

I'll also revisit the macro names as you suggest. What do you think about changing the dictionary one to #corrections([Example: Example])?

@SimplyDanny

Copy link
Copy Markdown
Collaborator

I'll also revisit the macro names as you suggest. What do you think about changing the dictionary one to #corrections([Example: Example])?

Also fine. However, just using #examples as well would not require people to remember two names (I know, it's a weak argument. 😅). By the type expected by corrections, the compiler shouldn't be confused about which variant to pick.

@ZevEisenberg

Copy link
Copy Markdown
Contributor Author

One other option I'm thinking about, and I'd love to get your take on: it's going to be annoying to have two different styles for examples:

#examples([
    "Some example",
    Example("exclude this one", excludeFromDocumentation: true),
])

I'm wondering what you might think about using a fluent API like this:

#examples([
    "Some example",
    "exclude this one".excludeFromDocumentation(),
])

There could also be a function for overriding configuration. They would fit nicely with the existing skip* functions, which I would also augment to be callable on strings(after first converting them to Examples). It would make the Example type more complex than you were proposing, but I think it would make the call sites nicer and more discoverable.

@ZevEisenberg

Copy link
Copy Markdown
Contributor Author
  • I opted to rename #examplesDictionary to #corrections. Have a look, and if you still prefer, I'll see if I can get the overloaded #examples version working.
  • Added a fluent API for turning strings into examples, and used that to convert a bunch more examples in the app.
  • As a result of the fluent API, I had to relax the argument of #examples to [Any] and the argument of #corrections to [AnyHashable: Any]. If you pass something bogus, the macro will expand it but things like Example(1234) will then fail to compile. Does this seem acceptable?
  • I added import SwiftLintCore to all files that use the macro, since it fixes an issue I was seeing where syntax highlighting for macros wasn't working, and "Expand Macro" was missing from the context menu.
  • I can try the variadic and custom operator suggestions if you'd like, but my sense is that a) an extra character at the top and bottom of a big stack of examples doesn't really matter that much, and b) the custom operator is a pretty heavy solution, and may increase cognitive load for people reading it, whereas everyone already knows how a dictionary looks and works.

This is ready for another review!

@ZevEisenberg ZevEisenberg marked this pull request as ready for review July 1, 2026 04:27
@ZevEisenberg

Copy link
Copy Markdown
Contributor Author

Fixed a build failure

@ZevEisenberg ZevEisenberg changed the title Add #examples and #examplesDictionary macros to DRY up rule description Add #examples and #corrections macros to DRY up rule description Jul 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants