Skip to content
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

Implement a warning or error if an unrecognized Rule is specified in .swift-format #607

Closed
natikgadzhi opened this issue Sep 2, 2023 · 2 comments · Fixed by #612
Closed

Comments

@natikgadzhi
Copy link
Contributor

Right now, if one made a typo in a rule name in .swift-format and then ran the formatter, it will ignore the typo and, obviously, not apply the rule.

I was working on swiftlang/swift-syntax#2145, and of course, that's precisely what I did. Oh, nice, our imports are already ordered, right?

Proposed solution

Since we know the list of all available rules, we could verify that each string in the Rules block matches an existing rule.

// Config.swift
//
// If the `rules` key is not present at all, default it to the built-in set
// so that the behavior is the same as if the configuration had been
// default-initialized. To get an empty rules dictionary, one can explicitly
// set the `rules` key to `{}`.
self.rules =
  try container.decodeIfPresent([String: Bool].self, forKey: .rules)
  ?? defaults.rules

It should be relatively easy, we could just generate an enum with all rule names, and use that as the key when decoding the rules.

Should I draft this in a PR?

@allevato
Copy link
Member

allevato commented Sep 3, 2023

This is a great idea for improvement, but I think it's a little bit more complicated than you've described here.

The configuration decoding logic doesn't have any way to pass information out of it other than "here's a successfully parsed config" or "here's an error because it failed". So if we want a warning, we have to check it in the frontend after decoding it; otherwise, we can make it an error during the actual decoding process.

The only real value to a warning—aside from just being a little more relaxed—would be to retain compatibility of newer config files with older versions of swift-format, but I don't think that's something we really need to do. So just throwing an error would be easier to implement.

I don't know if we need all the ceremony of generating a new enum for this, though. We already generate a Dictionary with all the rule names as keys here, so after decoding self.rules we could just check that all the decoded rule names exist in that dictionary (doesn't matter what the value is), and throw a decoding error if there's an unrecognized name.

Feel free to send a PR!

@natikgadzhi
Copy link
Contributor Author

natikgadzhi commented Sep 3, 2023

The configuration decoding logic doesn't have any way to pass information out of it other than "here's a successfully parsed config" or "here's an error because it failed". So if we want a warning, we have to check it in the frontend after decoding it; otherwise, we can make it an error during the actual decoding process.

I've explored a bit yesterday, and yep — I think one of the questions is at what level should that check be. It could be in Configuration.decode, but that Codable implementation, in theory, doesn't have to be aware of any domain logic. It can just parse JSON into what it states — [String:Bool].

Then, there's ConfigurationLoader that's relatively simple — perhaps that's the right level. But I gravitate to the next one:

We could put that into Frontend. I'm looking to confirm how exactly it loads config, seems like it has at least two ways to do it, but essentially, we could validate the configuration after we load it, and emit a warning in that context, which is nice and easy because Frontend has diagnosticsEngine on it already.

The only real value to a warning—aside from just being a little more relaxed—would be to retain compatibility of newer config files with older versions of swift-format, but I don't think that's something we really need to do. So just throwing an error would be easier to implement.

If we decide to go with an error, than the simplest solution by far is a check in Configuration.decode, like this:

self.rules = try container.decode(...)

// If any rules in the decoded configuration are not supported by the registry,
// emit them into the diagnosticsEngine as warnings.
// That way they will be printed out, but we'll continue execution on the valid rules.
let invalidRules = self.rules.filter { !RuleRegistry.rules.keys.contains($0.key) }
if !invalidRules.isEmpty {
  throw ConfigurationError.invalidConfiguration("Configuration contains an unsupported rule: \(invalidRules.keys.joined(separator: ", "))"
}

But, if we stick with a warning, we could do that check after we decode, and that would make it so any projects that have a broken config today won't blow up their CI when they update.

I don't know if we need all the ceremony of generating a new enum for this, though.

Yep, I think RuleRegistry.rules is sufficient!

Here's what I have now (rough draft, copy and pasting):

// In frontend.swift

  private func configuration(
    at configurationFileURL: URL?,
    orInferredFromSwiftFileAt swiftFileURL: URL?
  ) -> Configuration? {
    // If an explicit configuration file path was given, try to load it and fail if it cannot be
    // loaded. (Do not try to fall back to a path inferred from the source file path.)
    if let configurationFileURL = configurationFileURL {
      do {
        let configuration = try configurationLoader.configuration(at: configurationFileURL)

        // If any rules in the decoded configuration are not supported by the registry,
        // emit them into the diagnosticsEngine as warnings.
        // That way they will be printed out, but we'll continue execution on the valid rules.
        let invalidRules = configuration.rules.filter { !RuleRegistry.rules.keys.contains($0.key) }
        if !invalidRules.isEmpty {
          diagnosticsEngine.emitWarning("Configuration contains an unsupported rule: \(invalidRules.keys.joined(separator: ", "))", location: nil)
          return nil
        }

        return configuration
      } catch {
        diagnosticsEngine.emitError("Unable to read configuration: \(error.localizedDescription)")
        return nil
      }
    }

    // If no explicit configuration file path was given but a `.swift` source file path was given,
    // then try to load the configuration by inferring it based on the source file path.
    if let swiftFileURL = swiftFileURL {
      do {
        if let configuration = try configurationLoader.configuration(forSwiftFileAt: swiftFileURL) {
          return configuration
        }
        // Fall through to the default return at the end of the function.
      } catch {
        diagnosticsEngine.emitError(
          "Unable to read configuration for \(swiftFileURL.path): \(error.localizedDescription)")
        return nil
      }
    }

    // If neither path was given (for example, formatting standard input with no assumed filename)
    // or if there was no configuration found by inferring it from the source file path, return the
    // default configuration.
    return Configuration()
  }

I think if I just make that check run in both branches of loading configuration, we'll be fine.

Expect a PR in a few hours, depending on my 3 y/o daughter's mood. So far, she does not really understand why swift-format needs to emit warnings 👯‍♀️

natikgadzhi added a commit to natikgadzhi/swift-format that referenced this issue Sep 4, 2023
**Summary**:
- Helps developers surface any errors and typos in their `.swift-format`
  config files by showing a warning when running `swift-format` if the
  configuration includes invalid rules.
- The actual check is in `Frontend.swift`.
- The dictionary of rules for verification is in `RuleRegistry`, which
  is made `public` to be able to import it in `swift-format` target.

**Motivation**:
- Closes swiftlang#607
natikgadzhi added a commit to natikgadzhi/swift-format that referenced this issue Sep 4, 2023
**Summary**:
- Helps developers surface any errors and typos in their `.swift-format`
  config files by showing a warning when running `swift-format` if the
  configuration includes invalid rules.
- The actual check is in `Frontend.swift`.
- The dictionary of rules for verification is in `RuleRegistry`, which
  is made `public` to be able to import it in `swift-format` target.

**Motivation**:
- Closes swiftlang#607
natikgadzhi added a commit to natikgadzhi/swift-format that referenced this issue Sep 4, 2023
**Summary**:
- Helps developers surface any errors and typos in their `.swift-format`
  config files by showing a warning when running `swift-format` if the
  configuration includes invalid rules.
- The actual check is in `Frontend.swift`.
- The dictionary of rules for verification is in `RuleRegistry`, which
  is made `public` to be able to import it in `swift-format` target.

**Motivation**:
- Closes swiftlang#607
natikgadzhi added a commit to natikgadzhi/swift-format that referenced this issue Sep 5, 2023
**Summary**:
- Helps developers surface any errors and typos in their `.swift-format`
  config files by showing a warning when running `swift-format` if the
  configuration includes invalid rules.
- The actual check is in `Frontend.swift`.
- The dictionary of rules for verification is in `RuleRegistry`, which
  is made `public` to be able to import it in `swift-format` target.

**Motivation**:
- Closes swiftlang#607
natikgadzhi added a commit to natikgadzhi/swift-format that referenced this issue Sep 6, 2023
**Summary**:
- Helps developers surface any errors and typos in their `.swift-format`
  config files by showing a warning when running `swift-format` if the
  configuration includes invalid rules.
- The actual check is in `Frontend.swift`.
- The dictionary of rules for verification is in `RuleRegistry`, which
  is made `public` to be able to import it in `swift-format` target.

**Motivation**:
- Closes swiftlang#607

Apply suggestions from code review

Co-authored-by: Tony Allevato <tony.allevato@gmail.com>

Cleaning up based on PR review
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 a pull request may close this issue.

2 participants