Swift Digest
SE-0522 | Swift Evolution

Source-Level Control Over Compiler Warnings

Proposal
SE-0522
Authors
Artem Chikin, Doug Gregor, Holly Borla
Review Manager
Tony Allevato
Status
Accepted

01 何が問題だったのか

SE-0443 で、Swift コンパイラには -Werror <group> / -Wwarning <group> といった診断グループ単位で警告の扱いを制御するフラグが導入されました。しかしこれらはいずれも モジュール全体 に効く粗い粒度のオプションであり、「モジュール全体では -Werror Deprecated を有効にしつつ、特定の関数の中でだけは警告のまま残したい」といった局所的な例外を表現する手段がありませんでした。

たとえば、非推奨 API の使用をプロジェクト全体でエラーとして扱うポリシーを敷いていたとしても、古いライブラリとの互換性を保つために一部の関数だけは非推奨 API を呼ばざるを得ないことがあります。

func bridgeToLegacySystem() {
  oldAPI() // error: 'oldAPI()' is deprecated [#Deprecated]
}

今のままでは、こうした局所的な例外を許すためにはモジュール全体のポリシーを緩めるしかありません。逆に、-warnings-as-errors のような厳しい設定を導入したいのに、限られた箇所で一時的な技術的負債として警告を抑制したいだけのために導入をためらう、といった状況も起きがちでした。

また、SE-0443 のレビュー時には「モジュール全体での警告の完全な抑制(-Wsuppress のような機能)は危険なのでフラグとしては提供しない」という判断がされていました。一方で、局所的に特定の診断グループを完全に無視したいケースは依然として存在し、これもソースコード側で狭い範囲に限定して表現できるなら、誤用のリスクを抑えつつ現実的なニーズに応えられます。

加えて、strict memory safety(SE-0458)のように、警告ベースで段階的に新しいルールを採用していくような言語機能では、モジュール全体に一律適用する前に「特定の宣言だけ先行して厳しめにする」「特定の宣言だけ一時的に例外扱いにする」といった細粒度の制御ができることが、普及のしやすさに直結します。宣言単位で警告の挙動を上書きできる仕組みが必要とされていました。

02 どのように解決されるのか

宣言に付けて、その宣言の字句スコープ内での診断グループの扱いを上書きする新しい属性 @diagnose を導入します。

@diagnose(ForeignReferenceType, as: error)
public func foo() {
  ...
}

この例では、foo のシグネチャと本体の字句スコープに限って -Werror ForeignReferenceType を指定したのと同じ効果が得られます。モジュール内の他のコードには影響しません。

属性の文法

@diagnose は次の形式で、第1引数に診断グループ名、第2引数に as: ラベル付きで挙動指定子を取ります。第3引数としてオプションで reason: ラベル付きの文字列リテラル(文字列補間不可)を取れます。

@diagnose(<group-identifier>, as: <behavior-specifier>[, reason: "..."])

<behavior-specifier> は次の3つのいずれかです。

  • error: そのグループの警告をエラーに昇格させます(-Werror <group> と同等)
  • warning: そのグループの警告を、抑制されていたりエラーに昇格されていたりしても警告のまま出力させます(-Wwarning <group> と同等)
  • ignored: そのグループの警告を完全に抑制します(コマンドラインには無い「-Wsuppress 相当」の局所版)

@diagnose が効くのはあくまで 警告 の診断に対してだけで、もともとエラーとして出る診断は、たとえ同じ診断グループに属していても抑制したり警告に落としたりはできません。

付けられる宣言

ほぼすべての宣言に適用でき、その宣言の 字句スコープ全体(シグネチャと本体)に効きます。

  • enum / struct / class / actor / protocol / extension
  • 関数、イニシャライザ、デイニシャライザ、subscript、演算子、マクロ宣言
  • computed property、アクセサ(get / set)、オブザーバ(willSet / didSet
  • typealias、associatedtype、enum case(これらは字句スコープを開かないので、シグネチャ部分のみに効きます)
  • import 宣言(その import 文自体に対して出る警告のみに効きます)
  • freestanding declaration macro の呼び出し(展開結果のすべての宣言に効きます)

同じ宣言に付いた他の属性も、字句スコープの一部として @diagnose の効果を受けます。位置関係に関わらず、たとえば次の2つは等価で、どちらも @SomeWrapper の非推奨警告をエラーに昇格させます。

@diagnose(DeprecatedDeclaration, as: error)
@SomeWrapper func foo() { ... }

@SomeWrapper
@diagnose(DeprecatedDeclaration, as: error)
func foo() { ... }

enclosing scope に対する上書き

@diagnose の効果は、その宣言を囲むスコープでのそのグループの挙動を 上書き するものとして定義されます。一番外側のスコープはコマンドラインオプション(-warnings-as-errors / -Werror / -Wwarning)で決まるモジュール全体の挙動で、内側に向かって宣言ごとに順に上書きされていきます。

// -Werror UnsafeImportedAPI 下でも、この struct の中では warning に落ちる
@diagnose(UnsafeImportedAPI, as: warning)
struct LegacyFormatReader {
  func read(_ data: UnsafeRawPointer, _ count: Int) -> Header {
    c_read_bundle(data, count)
    // warning: call to imported function 'c_read_bundle' is unsafe API [#UnsafeImportedAPI]
  }

  // さらにこのメソッドの中だけは ignored
  @diagnose(UnsafeImportedAPI, as: ignored, reason: "input is validated upstream")
  func readTrustedInput(_ data: UnsafeRawPointer, _ count: Int) -> Header {
    c_read_bundle(data, count)
    // 診断は出ない
  }
}

サブグループにも効くので、親グループ(例: UnsafeImportedAPI)の挙動を全体として維持しつつ、サブグループ(例: UnsafeImportedOwnership)だけ別の扱いに落とす、といった組み合わせも表現できます。

複数指定時の「後勝ち」

同じ宣言に複数の @diagnose を付けた場合、SE-0443 のコマンドラインフラグと同じく 字句上あとに書かれたものが勝つ ルールで評価されます。Swift の属性は通常は順序に依存しませんが、@diagnose はこの方針から外れてコマンドラインの挙動と揃えられています。

@diagnose(DiagGroupID, as: error)
@diagnose(DiagGroupID, as: warning) // 上の error を上書きして warning になる
public func foo() { ... }

親グループと子グループを重ねれば、「親グループ全体は error にしつつ、一部のサブグループだけ ignored にする」といった指定もできます。

@diagnose(UnsafeImportedAPI, as: error)
@diagnose(UnsafeImportedOwnership, as: ignored) // サブグループのみ ignored に
public func foo() { ... }

-suppress-warnings との関係

-suppress-warnings はこのスコープ上書きモデルの外に置かれます。-suppress-warnings が指定されているコンパイルでは、@diagnose は(as: error も含めて)一切効かず、モジュール全体の警告抑制が優先されます。

これは SE-0480 との整合性のためでもあります。SwiftPM は他パッケージから依存として取り込まれたパッケージをビルドする際に、警告制御フラグをすべて剥がして -suppress-warnings を付けます。もしこの状況下で @diagnose(..., as: error) が効いてしまうと、遠い依存パッケージ内の @diagnose のせいで自分のビルドが落ちるという、コマンドラインの -Werror では起こらない現象が生じてしまいます。これを避けるため、-suppress-warnings 下では @diagnose は無視されます。

マクロ展開との関係

@diagnose が付いたスコープの中でマクロが展開された場合、展開されたコードにも @diagnose の効果が及びます。たとえば @diagnose(groupID, as: ignored) が付いたメソッド内でマクロ呼び出しがあり、その展開結果で groupID の警告が出る場合でも、その警告は抑制されます。

マクロ展開側が @diagnose を出力することも許されます。マクロ作者は、自身が生成するコードについて「この警告は本来出るはずがない」「これは生成パターン上避けられない警告」といった意図をコード生成時に埋め込めます。freestanding declaration macro の呼び出しに @diagnose を付ければ、展開されたすべての宣言にその属性が効きます。

@diagnose(DeprecatedDeclaration, as: ignored)
#generateLegacyBindings(for: OldFramework)

ただし、attached peer macro で生成される 兄弟 の宣言には、元の宣言に付いた @diagnose は波及しません。peer で生成される宣言は元の宣言の字句スコープの外に置かれるためです。その場合はマクロ展開側で @diagnose を出すか、両方を含む外側のスコープに @diagnose を付ける必要があります。

公開インターフェースへの影響

@diagnose 属性はテキスト形式のモジュールインターフェースには出力されず、バイナリモジュールの内容にも影響しません。警告の扱いはあくまで「そのモジュールをビルドしているとき」のローカルな関心事であり、利用側には伝播しません。

Future Directions

今回の提案は宣言単位での制御にとどまっており、さらに細かい粒度の制御はすべて今後の検討事項とされています。具体的には次のような方向性が挙げられています(いずれも speculative で、実現が約束されているわけではありません)。

  • do { ... } ブロックなど、宣言に対応しない字句スコープでの制御
  • クロージャ式の本体に対する制御
  • using @diagnose(...) のようなファイルスコープ宣言
  • 第三者ツール(リンタなど)向けに、from: パラメータでグループ識別子の名前空間を切る形での拡張