SwiftFormat As a Package Dependency

I have finally gotten far enough down my to‐do list to investigate using SwiftFormat. The read‐me says:

users can depend on swift-format as a Swift Package Manager dependency and import the SwiftFormat module, which contains the entry points into the formatter's diagnostic and correction behavior.

However the SwiftFormat product only includes the SwiftFormat module. It does not seem possible to do much of anything without also cheating and importing internal‐only modules like SwiftFormatConfiguration. For example, the only visible initializer for the SwiftLinter type requires a Configuration instance, but that type is neither provided nor exported by the only module marked for consumption. Is that an oversight?

Right now the package manager does not block internal modules from being imported by clients, so it can be worked around, but if one day it does there will be trouble. On the other hand, when the import model is finally sorted out (@_exported, @_implementationOnly, etc.) it will probably be impossible to end up in this situation in the first place.

So the questions are:

  • Which modules are intended for client use, despite what the manifest says?
  • Can those modules be added to the library product declaration?

@allevato

1 Like

We've definitely been a bit loose about this since the majority of uses right now are the command line tool. Thanks for bringing it up!

SwiftFormatConfiguration is intended to be a public module, since it's needed by the public interfaces of the SwiftFormatter and SwiftLinter APIs. It's factored out into its own module since it needs to be used by both the high-level API module and the various low-level internal implementation modules. I'd say it's safe to import it for the foreseeable future; we don't have any immediate plans to change that.

Given that SwiftFormat is the only other public interface (and the main entry point) for programmatic use, it probably does make sense to have it re-export SwiftFormatConfiguration for ease of use. I'd feel more gung-ho about it if we had a more stable story there than @_exported right now, though.

(Historical aside: SwiftFormatPrettyPrint was even intended to be a public standalone module, the idea being that it might be useful as a generic token-driven layout engine and we'd eventually move the SwiftSyntax-related bits up into other modules. That didn't really pan out in reality though, and was probably a premature generalization.)

Would it be reasonable to adjust the manifest to this?

.library(name: "SwiftFormat", targets: [
    "SwiftFormat",
    "SwiftFormatConfiguration" // ← Simply adding this.
]),

I’m not opposed. Feel free to send us a PR if you like.

For my own curiosity, does SPM do anything with those values or is it purely for documentation purposes right now? (I don’t get to use SPM as much as I’d like so sadly I have some gaps in my knowledge.)

The effect would be much more obvious if the modules did not depend on each other. In cases like this where they do, SwiftPM will have already put the lower‐level one in the right place so that it works by accident anyway. But increasingly SwiftPM’s isn’t the only thing using the manifest. A lot of accidentally working things suddenly broke when Xcode added support for packages, because it went about the build process its own way and things no longer lined up. I’ve learned the hard way not to rely on anything that appears to work with SwiftPM, but lacks any official documentation saying it is an intentionally supported set‐up.

I probably will eventually, but I don’t know when I will get to it.

1 Like

I have another question. Is the WhitespaceLinter supposed to respect the configuration? I’ve been trying to apply only one rule at a time so I can verify the result, but even with the rules set to [:], the linter reports a ton of warnings. Most of them are [Indentation], but there are a few [AddLines] and [LineLength] too. From glancing at the source it looks like they are all rules handled by the WhitespaceLinter.

WhitespaceLinter respects the configuration, but it depends on what you mean by that. To elaborate, the formatter operates in two "phases":

Phase 1 applies the rules from the SwiftFormatRules module that are enabled in the rules dictionary in the configuration. These rules tend to be checks/corrections that do syntactic transformations that don't strictly operate on whitespace. (However, some of those rules are whitespace-oriented, and we're considering whether they should be moved to phase 2.)

Phase 2 uses the SwiftFormatPrettyPrint module to take the output from phase 1 and canonicalize whitespace. The pretty-printing algorithm walks the syntax tree and uses the structure to determine where lines should be allowed to break, and then based on the line length configuration and some other settings (e.g., whether existing line breaks should be respected), lays the source text out appropriately. WhitespaceLinter works by running the pretty-printer and then diffing the input and output to determine where the violations are.

Right now, we don't have a way to disable phase 2 except as a debugging option, but that's all-or-nothing. It's a known issue because we also need to be able to support disabling the pretty printer at certain points to handle range-based formatting and disable/enable comment directives.

I was hoping to disable everything to start with and activate one thing at a time, to more clearly see what it is doing. I am trying to sort out how well the various pieces work. For example, I would be very happy to let swift‐format handle indenting all by itself. But I’m hesitant about letting it insert line breaks until I can see how well it plays with other tooling. There are more important tools in the workflow that respond to directives in comments which need to be on the same line as the code they are related to. It might still work, but to figure that out, I need its behaviour (temporarily) restricted to only that rule in order to see what it actually does.

If you disable all the rules in the configuration and pass DebugOptions.disablePrettyPrint when you create the formatter/linter instance, I think that should get you to a fairly "no-op" state. Setting respectsExistingLineBreaks to true will also reduce some of the opinionatedness of the pretty-printer, by telling it to respect discretionary line breaks in the original source code (n.b., only those that occur where the pretty-printer says breaks are permitted in the first place).

But, to keep things simple/focused for now we haven't really created a number of different knobs for different parts of the pretty printer like indentation vs. line-breaking, so you may run into some limitations there if you really want to enable things piecemeal. I'm not opposed to that in the future, it's just not something we've currently prioritized.

Just to be clear, piecemeal application isn’t the end goal. It’s just to ease the switch by going about it incrementally.

disablePrettyPrint does not affect SwiftLinter (only SwiftFormatter).

Thanks for reporting that—it was unintentional and I've fixed it (as you already noticed!). I also cherry-picked that change (among others) into the swift-5.1-branch.

1 Like

I’m a long way from trying everything, but so far I really like the way it works. It does two things well that few such tools do:

  • It doesn’t try to line indents up with characters:
    func 一二三(一: Int,
             二: Int, // No you fool of a formatter,
             三: Int // these three parameters do not line up!
    ) {}
    
    func 一二三(
      一: Int, // SwiftFormat’s style is so much more reliable.
      二: Int,
      三: Int
    ) {}
    
  • It leaves comments alone. Remember the directives for other tools I was worried about? None of them broke when I applied line breaking. :sunglasses:
2 Likes

I created a pull request for the manifest products. Its #80.

1 Like