flutter formatとdart formatの違い

# はじめに

Flutter Advent Calendar 2021の記事です。

https://qiita.com/advent-calendar/2021/flutter

# 追記
### flutter formatコマンドは削除されました。現在はdart formatを使用する必要があります
 https://github.com/flutter/flutter/pull/129360

# 記事本編

flutter formatとdart formatの違いを調べてみました。

# TL;DR 
flutter formatはdart formatのラッパーです。flutter formatは指定VersionのFlutterで動くように良きように設定してくれているのでflutter formatを使っておきましょう。

# きっかけ
Effective Dartを読んでいる時に、Dartもformatあるよねなんでflutter formatコマンドがあるんだろうと思って調べました。

https://dart.dev/guides/language/effective-dart

# 何が違うのか
何が違うのか把握するために、それぞれを`-h`オプションで実行してみます。


flutter format -h

```

Idiomatically format Dart source code.

Usage: dart format [options...] <files or directories...>
-h, --help                     Print this usage information.
-v, --verbose                  Show all options and flags with --help.
-o, --output                   Set where to write formatted output.

          [json]               Print code and selection as JSON.
          [none]               Discard output.
          [show]               Print code to terminal.
          [write] (default)    Overwrite formatted files on disk.

    --set-exit-if-changed      Return exit code 1 if there are any formatting changes.
    --fix                      Apply all style fixes.
-l, --line-length              Wrap lines longer than this.
                               (defaults to "80")

Run "dart help" to see global options.
```


dart format -h

```
Idiomatically format Dart source code.

Usage: dart format [options...] <files or directories...>
-h, --help                     Print this usage information.
-v, --verbose                  Show all options and flags with --help.
-o, --output                   Set where to write formatted output.

          [json]               Print code and selection as JSON.
          [none]               Discard output.
          [show]               Print code to terminal.
          [write] (default)    Overwrite formatted files on disk.

    --set-exit-if-changed      Return exit code 1 if there are any formatting changes.
    --fix                      Apply all style fixes.
-l, --line-length              Wrap lines longer than this.
                               (defaults to "80")

Run "dart help" to see global options.
```

結果は一言一句全く同じでした。

どうやらCLIのインターフェース上からは回答を得られそうにないので、直接コードをみてみます。

```format.dart[]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:args/args.dart';

import '../artifacts.dart';
import '../base/common.dart';
import '../globals.dart' as globals;
import '../runner/flutter_command.dart';

class FormatCommand extends FlutterCommand {
  FormatCommand({required this.verboseHelp});

  @override
  ArgParser argParser = ArgParser.allowAnything();

  final bool verboseHelp;

  @override
  final String name = 'format';

  @override
  List<String> get aliases => const <String>['dartfmt'];

  @override
  final String description = 'Format one or more Dart files.';

  @override
  String get category => FlutterCommandCategory.project;

  @override
  String get invocation => '${runner?.executableName} $name <one or more paths>';

  @override
  Future<FlutterCommandResult> runCommand() async {
    final String dartBinary = globals.artifacts!.getHostArtifact(HostArtifact.engineDartBinary).path;
    final List<String> command = <String>[
      dartBinary,
      'format',
    ];
    final List<String> rest = argResults?.rest ?? <String>[];
    if (rest.isEmpty) {
      globals.printError(
        'No files specified to be formatted.'
      );
      command.add('-h');
    } else {
      command.addAll(<String>[
        for (String arg in rest)
          if (arg == '--dry-run' || arg == '-n')
            '--output=none'
          else
            arg
      ]);
    }

    final int result = await globals.processUtils.stream(command);
    if (result != 0) {
      throwToolExit('Formatting failed: $result', exitCode: result);
    }

    return FlutterCommandResult.success();
  }
}
```

https://github.com/flutter/flutter/blob/master/packages/flutter_tools/lib/src/commands/format.dart

実行部分のコードを見る限り、追加のオプションなども定義されていますが、formatを実行するという点においては同じもののように見えます。

それではなぜわざわざ処理をラップするためだけのコードが書かれたのでしょうか。

答えは過去のログを遡っていくことで見つけられました。

https://github.com/flutter/flutter/issues/5338

以下引用
> In order to reduce occasions where a user runs the wrong version of a utility, I think we can (and should) wrap utilities provided by the Dart SDK with our own flutter command.

> For example, I would like our users to try formatting. The instructions to ensure they are using the right version of the formatter are fairly complicated, complete with a bunch of "check this path" and "check this version".

> Instead, I would like to say, "Run flutter format foo.dart" and have the flutter command use dartfmt that we bundle.

> (we might want to do this for pub, too, but that's probably a different issue)

要はformatterの実行時に、ユーザ側がバージョニングなど余計なところで引っかからないように、最初から期待するVersionのformatterをバンドルしてくれているようです。

上記のような結果から、気にすることなく`flutter format`を使っていこうと思いました。


# 余談
以下にはコードを読んでみて知った余談を書いています。

## 隠しオプション
意図して隠しているわけではなさそうですが、`--output=none` のオプションは、`--dry-run`か`-n` を利用することでも実行できます。

経緯はここら辺に書かれています。後述しますが、内部で利用しているformatterを変更する際、既存の処理を壊さないように、workaroundとして実装されたようです。

https://github.com/flutter/flutter/issues/86140


## 隠しエイリアス
コマンドにはエイリアスとして`dartfmt`が定義されているため、`flutter dartfmt`でも同様の処理を実行できます。
上記のような実装になっている背景としては、実装当初は`dart format`を利用するのではなく、`dartfmt`を利用していたためです。

https://github.com/flutter/flutter/blob/e20ec2becb1bb7a45e54718768a8bc09f785ab74/packages/flutter_tools/lib/src/commands/format.dart

そして下記のように、`dartfmt`が廃止され、`dart format`が推奨になったことで、内部の実装も`dart format`にリプレイスされました。
前述の隠しオプションについてもこの対応の煽りを受けた結果の事象のようです。

ちなみにこのエイリアスが完全に利用されていないかまでは確認していないので、もしかしたらまだ利用されている箇所もあるかもしれません。

https://github.com/dart-lang/dart_style/issues/986

# 最後に
他にもアドベントカレンダーで色々記事書いたので興味あれば読んでみていただけるありがたいです。。


https://qiita.com/stnamco/items/d835c97e8cd57fa8f493

https://qiita.com/stnamco/items/bc70fa22c13efbdb8634

https://adventar.org/calendars/6859