Warnings as errors RFC

RFC: Selective Warnings as Errors in tuist generate

Summary

Allow project maintainers to promote specific generation warnings to errors via GenerationOptions in Tuist.swift, so that tuist generate fails immediately when those warnings are detected.

Motivation

Today, tuist generate emits warnings at the end of generation that are easy to miss. For example, the “outdated dependencies” warning tells the user to run tuist install, but since generation succeeds, developers often overlook it and end up debugging subtle breakages caused by stale dependencies.

A real-world example from a Tuist adopter:

I missed the outdated dependencies warning, then had a subtle breakage that took me a moderate amount of time to track down. It would be better if we could just fail immediately.

There was a prior discussion about adding a --strict flag that would treat all warnings as errors, but that approach is too blunt — teams may not be able to address every warning immediately (e.g., static side effects warnings during a migration period). What’s needed is a way to selectively promote specific warnings to errors, similar to Swift 6’s warnings-as-errors model.

Importantly, this should be a manifest-level configuration (in GenerationOptions), not a CLI flag, so that platform teams can enforce policy for all developers rather than relying on each individual to pass the right flags.

Prior Art

  • Swift compiler: -warnings-as-errors flag and, since Swift 6, selective #warning / -Werror=<category> support.
  • ESLint: Rules can be individually set to "off", "warn", or "error".
  • Gradle: Specific deprecation warnings can be promoted to errors via configuration.
  • Bazel: --check_direct_dependencies=error promotes a specific warning to an error.

All of these share the same pattern: a default severity that can be overridden per-category.

Proposed Solution

New GenerationOptions Parameter

Add a new warningsAsErrors parameter to Tuist.GenerationOptions in Tuist.swift:

// ProjectDescription

extension Tuist.GenerationOptions {
    public enum WarningsAsErrors: Codable, Equatable, Sendable {
        /// No warnings are promoted to errors (default, current behavior).
        case none
        /// All generation warnings are treated as errors.
        case all
        /// Only the specified warning categories are treated as errors.
        case only(Set<GenerationWarning>)
    }
}

Warning Categories

Define an enum of known warning categories:

// ProjectDescription

public enum GenerationWarning: String, Codable, Equatable, Hashable, Sendable {
    /// Dependencies are outdated compared to the resolved package graph.
    case outdatedDependencies
    /// A static product is linked from multiple targets, risking side effects.
    case staticSideEffects
    /// A scheme references targets that cannot be found.
    case schemeTargetNotFound
    /// Project configurations are mismatched across the graph.
    case mismatchedConfigurations
    /// Duplicate product names exist among dependencies.
    case duplicateProductNames
}

Usage

// Tuist.swift

let tuist = Tuist(
    project: .tuist(
        generationOptions: .options(
            warningsAsErrors: .only([.outdatedDependencies])
        )
    )
)

Or to enforce all warnings as errors:

let tuist = Tuist(
    project: .tuist(
        generationOptions: .options(
            warningsAsErrors: .all
        )
    )
)

Behavior

When warningsAsErrors is configured and a matching warning is triggered:

  1. The warning is displayed as an error (red, not yellow).
  2. Generation fails with a non-zero exit code.
  3. The error message includes the same content as the warning, plus guidance on how to resolve it.

When a warning is not in the promoted set, the current behavior is preserved — the warning is printed at the end and generation succeeds.

Implementation Sketch

The core change is in the linting pipeline. Currently, LintingIssue has a fixed severity set at creation time. The proposed approach:

  1. After collecting all linting issues in Generator, check the warningsAsErrors configuration.
  2. For each .warning-severity issue that matches a promoted category, elevate it to .error.
  3. The existing error-handling path (which already throws LintingError for .error issues) takes care of the rest.

This requires adding a category: GenerationWarning? field to LintingIssue so that warnings can be identified by type rather than by string matching on the reason.

Key files to modify:

  • Sources/ProjectDescription/ConfigGenerationOptions.swift — add warningsAsErrors option
  • Sources/TuistCore/Models/LintingIssue.swift — add optional category field
  • Sources/TuistKit/Generator/Generator.swift — elevate matching warnings before printing
  • Sources/TuistGenerator/Linter/GraphLinter.swift — tag warnings with categories
  • Sources/TuistGenerator/Linter/StaticProductsGraphLinter.swift — tag warnings
  • Sources/TuistLoader/Loaders/SwiftPackageManagerGraphLoader.swift — tag outdated dependencies warning

Relationship to staticSideEffectsWarningTargets

The existing staticSideEffectsWarningTargets option controls which targets trigger static side effects warnings. The new warningsAsErrors option is orthogonal — it controls what happens when a warning is triggered. Both can coexist:

.options(
    staticSideEffectsWarningTargets: .excluding(["LegacyModule"]),
    warningsAsErrors: .only([.staticSideEffects, .outdatedDependencies])
)

This means: “Don’t warn about LegacyModule’s static side effects, but if any other target triggers the warning, fail the build.”

Alternatives Considered

1. --strict CLI flag

Treats all warnings as errors. Too blunt — teams in migration periods can’t suppress specific warnings. Also requires each developer to remember to pass the flag, making it unsuitable for enforcing team policy.

2. Per-warning suppression instead of promotion

An .ignoreWarnings([...]) option that suppresses specific warnings. This is the inverse of the proposal and could complement it, but doesn’t solve the core problem of making critical warnings enforceable. Could be added later.

3. CLI flag instead of manifest option

A --warnings-as-errors flag on tuist generate. This puts the burden on individual developers and doesn’t allow platform teams to enforce policy. The manifest-level approach is preferred because it’s checked into source control and applies to everyone.

Open Questions

  1. Should we also support --warnings-as-errors as a CLI flag? It could be useful for CI environments that want stricter checks than local development. The manifest option could serve as the baseline, with the CLI flag providing an override. I’m leaning to no for now and we can add it if this becomes a need.
1 Like

Very onboard with the plan.

I’d say let’s do if teams ask for it.

PR to implement this: feat(cli): add warningsAsErrors generation option by fortmarek · Pull Request #9574 · tuist/tuist · GitHub