Add #examples and #corrections macros to DRY up rule description#6794
Conversation
Generated by 🚫 Danger |
#examples and #examplesDictionary macros to DRY up rule description
SimplyDanny
left a comment
There was a problem hiding this comment.
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
ExampleExpressibleByStringLiteraland so avoid any manual conversion. - I'd prefer to only have a single macro
#examplesthat 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
CorrectionExampleobjects that wrap twoExamples. They could be created with a custom operator=>, e.g.corrections: #examples( "let foo = 1" => "let bar = 1", ... )
@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 |
|
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 I'll also revisit the macro names as you suggest. What do you think about changing the dictionary one to |
Also fine. However, just using |
|
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 |
This is ready for another review! |
78a2147 to
71690c3
Compare
|
Fixed a build failure |
#examples and #examplesDictionary macros to DRY up rule description#examples and #corrections macros to DRY up rule description
|
I'm applying the feedback. By the way, the changes in this PR will obfuscate a lot of history on affected lines. Do you have any interest in using a We use it at work, but all our projects have a shared setup script you must run first, so it's not a hassle. But I'm not sure if there's something equivalent here, so it would just be a convenience for those who know about it (and for those browsing GitHub, where it's picked up automatically). Possibly more trouble than it's worth, but I wanted to mention it. |
71690c3 to
aef95c4
Compare
|
I pushed the |
|
Note This comment was written by Claude (via Claude Code), acting on @ZevEisenberg's behalf. Following up on the suggestion to make I ended up using a different mechanism that gives the same diagnostic win without a runtime footgun: a marker protocol public protocol ExampleConvertible {}
extension String: ExampleConvertible {}
extension Example: ExampleConvertible {}
@freestanding(expression)
public macro examples(_ examples: [any ExampleConvertible]) -> [Example] = …
@freestanding(expression)
public macro corrections(_ examples: [AnyHashable: any ExampleConvertible]) -> [Example: Example] = …An invalid element is now rejected at the call site. Given: #examples([
"let x = 1",
42, // ← not a valid example
])(In One caveat worth flagging: Why not
|
|
ready for another look |
SimplyDanny
left a comment
There was a problem hiding this comment.
I'm still in favor of the ExpressibleByStringLiteral/ExpressibleByStringInterpolation variant. It really gets the types of #examples and #corrections correct and throwing an error in its required initializer will rarely be a problem (if at all, only in test code).
To help overload resolution, the original Example initializer can use a named argument for the code. We won't use it often after this refactoring, so that would be acceptable.
Just adding it doesn't hurt. If applied, it helps to get blame clearer. If not ... 🤷♂️ So let's add it after merging when we have the final commit hash. |
0d1a819 to
95e4426
Compare
How do you feel about |
|
Ok, changes pushed with I also cleaned up a few concatenated strings that were clearer as multiline strings. I'd love to convert a gazillion strings to raw strings to make them more readable, but I will resist for this PR. Ready for another look! |
c3fb487 to
5ac3eb8
Compare
SimplyDanny
left a comment
There was a problem hiding this comment.
A few more remarks. Otherwise, looks pretty good now!
Both are fine for now. Unification can happen in a follow-up PR. 😉 |
…ons. # Conflicts: # Source/SwiftLintBuiltInRules/Rules/Style/ClosureSpacingRule.swift
Co-authored-by: Danny Mösch <danny.moesch@icloud.com>
Co-authored-by: Danny Mösch <danny.moesch@icloud.com>
…rt as many remaining Example call sites into bare strings.
… Xcode syntax highlighting and makes the Expand Macro command show up and work more reliably.
Give the #examples and #corrections macros precise parameter types ([any ExampleConvertible] and [AnyHashable: any ExampleConvertible]) instead of [Any]/[AnyHashable: Any]. Passing an unsupported element type now fails as a type error on the offending literal rather than as a macro-expansion failure inside generated code. Only String and Example conform to the marker protocol. Also drop the vestigial `+ ""` in LineLengthRule, since a bare String expression now type-checks directly as an example element. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… to account for #examples macro.
…y typed Examples as input. This requires changing Example("...") to Example(code: "...") or "...".asExample(), but the tradeoff is that it enables more precise and actionable errors if you pass the wrong type of thing to the #examples and #corrections macros. Also, did some cleanup: converted a few concatenated strings to multi-line strings.
5ac3eb8 to
b5a0e5e
Compare
|
Rebased and pushed fixes in response to comments. |
|
Ready to merge from your point of view, @ZevEisenberg? |
Yes! |


When I wrote the
Exampletype 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 repetitiveExample("...")call sites.Now that macros exist, I propose using them to keep the benefits of
Examplewhile 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
Examplewrapper:review_mechanical_changes.py
Alternatives/Questions
replacethe 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.#examples, but that seemed too cute and not clear enough at the point of use.