メインコンテンツへスキップ

設計思想

非公式ベータ版翻訳

このページは PageTurner AI で翻訳されました(ベータ版)。プロジェクト公式の承認はありません。 エラーを見つけましたか? 問題を報告 →

Prettierはオピニオネイテッドなコードフォーマッタです。この文書ではその設計思想について説明します。

Prettierが重視する原則

正確性

Prettierの最優先要件は、フォーマット後も動作が完全に同一である有効なコードを出力することです。正確性ルールに違反するケースを発見した場合はバグ報告をお願いします。修正が必要な問題です!

文字列

ダブルクォートとシングルクォートのどちらを使うべきか?Prettierはエスケープが最小限で済む方を選択します。例えば'It\'s gettin\' better!'ではなく"It's gettin' better!"となります。同数または引用符が含まれない場合、デフォルトでダブルクォートを使用します(singleQuoteオプションで変更可能)。

JSXには専用のクォートオプションjsxSingleQuoteがあります。 JSXはHTMLを起源としており、属性ではダブルクォートが主流です。ブラウザの開発者ツールもこの慣例に従い、ソースがシングルクォートでも常にダブルクォートで表示します。このためJavaScriptではシングルクォートを、"HTML"(JSX)ではダブルクォートを使う分離オプションを提供しています。

Prettierは文字列のエスケープ方法を維持します。例えば"🙂""\uD83D\uDE42"に変換することはなく、逆も同様です。

空行

空行の自動生成は非常に複雑な問題です。Prettierのアプローチは、元のソースコードの空行を維持することに加え、次の2つのルールを適用します:

  • 複数の連続した空行を単一の空行に圧縮

  • ブロック(およびファイル全体)の先頭/末尾の空行を削除(ただしファイル末尾には常に単一の改行を保持)

複数行オブジェクト

デフォルトではPrettierの整形アルゴリズムは、収まる場合に式を1行で出力します。ただしJavaScriptではオブジェクトが多様な用途で使われ、複数行のままの方が可読性が向上するケースがあります([オブジェクトリスト]、[ネスト設定]、[スタイルシート]、[キー付きメソッド]など)。全てのケースに適用できる汎用ルールが見つからなかったため、元ソースで{と最初のキーの間に改行がある場合のみ複数行を維持します。これにより、長い1行オブジェクトは自動拡張されますが、短い複数行オブジェクトは決して圧縮されません。

この条件付き動作はobjectWrapオプションで無効化できます。

ヒント: 複数行オブジェクトを1行に結合したい場合:

const user = {
name: "John Doe",
age: 30,
};

{の直後の改行を削除するだけで:

const user = {  name: "John Doe",
age: 30
};

Prettierを実行すると:

const user = { name: "John Doe", age: 30 };

再び複数行に戻したい場合は、{の後に改行を追加:

const user = {
name: "John Doe", age: 30 };

Prettierを実行:

const user = {
name: "John Doe",
age: 30,
};
書式設定の可逆性に関する注記

オブジェクトリテラルの半手動書式設定は、実際には機能ではなく回避策です。当時は適切なヒューリスティックが見つからず、緊急の修正が必要だったために実装されました。しかし一般的な戦略として、Prettierはこのような非可逆的な書式設定を避けるため、チームはこの挙動を完全に除去するか、少なくとも適用場面を減らせるヒューリスティックを探し続けています。

可逆的とはどういう意味でしょうか?一度オブジェクトリテラルが複数行になると、Prettierはそれを折り畳みません。Prettierでフォーマットされたコードでプロパティを追加してフォーマットし、その後追加したプロパティを削除してもう一度フォーマットすると、元の書式と完全に同じにならない場合があります。この不要な変更がコミットに含まれる可能性があり、これはPrettierが防ぐために作られたまさにその種の状況です。

デコレータ

オブジェクトと同様に、デコレータも様々な用途で使用されます。デコレート対象の行のに記述するのが適切な場合もあれば、同じ行に配置するのが好ましい場合もあります。これに対する適切なルールが見つかっていないため、Prettierは元のデコレータ位置を維持します(行内に収まる場合)。理想的ではありませんが、困難な問題に対する現実的な解決策です。

@Component({
selector: "hero-button",
template: `<button>{{ label }}</button>`,
})
class HeroButtonComponent {
// These decorators were written inline and fit on the line so they stay
// inline.
@Output() change = new EventEmitter();
@Input() label: string;

// These were written multiline, so they stay multiline.
@readonly
@nonenumerable
NODE_TYPE: 2;
}

例外が1つあります:クラスです。クラスデコレータをインライン化することは適切ではないため、常に独立した行に移動されます。

// Before running Prettier:
@observer class OrderLine {
@observable price: number = 0;
}
// After running Prettier:
@observer
class OrderLine {
@observable price: number = 0;
}

注記:Prettier 1.14.x以前はデコレータの自動移動を試みていたため、古いバージョンでコードをフォーマットした場合、不整合を避けるために手動でデコレータを結合する必要が生じる可能性があります:

@observer
class OrderLine {
@observable price: number = 0;
@observable
amount: number = 0;
}

テンプレートリテラル

テンプレートリテラルには補間式を含めることができます。補間内で改行を挿入するのが適切かどうかの判断は、残念ながらテンプレートの意味的内容に依存します(例えば自然言語の文中で改行を挿入するのは通常望ましくありません)。Prettierにはこの判断を行う十分な情報がないため、オブジェクトと同様のヒューリスティックを使用します:補間式内に既に改行がある場合にのみ、複数行に分割します。

つまり、以下のようなリテラルは印刷幅を超えても複数行に分割されません:

`this is a long message which contains an interpolation: ${format(data)} <- like this`;

Prettierに補間式を分割させたい場合は、${...}内のどこかに改行が存在することを確認する必要があります。そうしないと、どんなに長くても全てを1行に保ちます。

チームは元の書式に依存しない方法を望んでいますが、現時点ではこれが最良のヒューリスティックです。

セミコロン

これはnoSemiオプションの使用に関する説明です。

以下のコードを考えてみてください:

if (shouldAddLines) {
[-1, 1].forEach(delta => addLine(delta * 20))
}

上記のコードはセミコロンなしでも問題なく動作しますが、Prettierは実際には次のように変換します:

if (shouldAddLines) {
;[-1, 1].forEach(delta => addLine(delta * 20))
}

これはミスを防ぐための措置です。Prettierがセミコロンを挿入せず、この行を追加した場合を想像してください:

 if (shouldAddLines) {
+ console.log('Do we even get here??')
[-1, 1].forEach(delta => addLine(delta * 20))
}

おっと!上記は実際にはこう解釈されます:

if (shouldAddLines) {
console.log('Do we even get here??')[-1, 1].forEach(delta => addLine(delta * 20))
}

[の前にセミコロンを置くことで、このような問題は決して発生しません。行を他の行から独立させることで、ASIルールを気にせずに行を移動・追加できます。

この手法はセミコロンフリースタイルを使用するstandardでも一般的です。

注意:現在プログラムにセミコロン関連のバグがある場合、Prettierはそのバグを自動修正しません。Prettierはコードを再フォーマットするだけで、コードの動作は変更しないことを覚えておいてください。開発者が(の前にセミコロンを置き忘れた次のバグのあるコードを例として見てみましょう:

console.log('Running a background task')
(async () => {
await doBackgroundWork()
})()

このコードをPrettierに通しても、コードの動作は変わりません。代わりに、実行時の実際の振る舞いが明確になるようにフォーマットされます。

console.log("Running a background task")(async () => {
await doBackgroundWork();
})();

プリント幅

printWidthオプションは、Prettierにとって絶対的なルールというよりガイドラインです。これは行の長さの上限ではなく、Prettierに「おおよそこの長さを目安にしてほしい」という指示です。Prettierは指定されたプリント幅を満たすよう努めますが、実際にはそれより短い行や長い行も生成されます。

非常に長い文字列リテラル、正規表現、コメント、変数名など、改行できない特殊なケースが存在します(Prettierが行わないコード変換が必要なため)。また、コードを50レベルもネストすれば、行の大部分はインデントで埋まるでしょう。

これら以外にも、Prettierが意図的にプリント幅を超えるケースがいくつかあります。

インポート

Prettierは長いimport文を複数行に分割できます:

import {
CollectionDashboard,
DashboardPlaceholder,
} from "../components/collections/collection-dashboard/main";

次の例はプリント幅に収まりませんが、Prettierはこれを1行で出力します:

import { CollectionDashboard } from "../components/collections/collection-dashboard/main";

これは意外に思えるかもしれませんが、単一要素のimportは1行に保つという要望が多かったためこの仕様です。require呼び出しも同様です。

テスト関数

長いテスト記述を1行に保ちたいという要望も多くありました。引数を改行しても可読性が向上しないケースがあるためです。

describe("NodeRegistry", () => {
it("makes no request if there are no nodes to prefetch, even if the cache is stale", async () => {
// The above line exceeds the print width but stayed on one line anyway.
});
});

Prettierはdescribeittestといった一般的なテストフレームワーク関数を特別扱いします。

JSX

JSXが関わる場合、Prettierは通常のJavaScriptとは異なるフォーマットを行います:

function greet(user) {
return user
? `Welcome back, ${user.name}!`
: "Greetings, traveler! Sign up today!";
}

function Greet({ user }) {
return (
<div>
{user ? (
<p>Welcome back, {user.name}!</p>
) : (
<p>Greetings, traveler! Sign up today!</p>
)}
</div>
);
}

これには2つの理由があります。

第一に、特にreturn文では、多くの開発者が既にJSXを括弧で囲んでいます。Prettierはこの一般的なスタイルに従います。

第二に、この代替フォーマットはJSXの編集を容易にします。セミコロンの残存ミスが発生しやすく、通常のJavaScriptとは異なり、JSX内に残ったセミコロンはページ上にプレーンテキストとして表示されてしまう可能性があるためです。

<div>
<p>Greetings, traveler! Sign up today!</p>; {/* <-- Oops! */}
</div>

コメント

コメントの内容については、Prettierができることはほとんどありません。コメントには散文からコメントアウトされたコード、ASCIIダイアグラムまであらゆるものが含まれ得るためです。Prettierはそれらのフォーマットや改行方法を知りえないため、コメントは原則として「そのまま」保持されます。唯一の例外はJSDoc形式のコメント(各行が*で始まるブロックコメント)で、Prettierはインデントを修正できます。

次に問題となるのはコメントの配置位置です。これは非常に難しい課題であり、Prettierは元の位置をほぼ維持しようとしますが、コメントはほぼどこにでも配置できるため容易ではありません。

一般的に、行末ではなく行全体をコメントとして確保する方が良い結果が得られます。// eslint-disable-lineよりも// eslint-disable-next-lineを推奨します。

eslint-disable-next-line$FlowFixMeといった「マジックコメント」は、Prettierが式を複数行に分割した場合に手動で移動が必要になる場合があることに留意してください。

次のコードを考えてみましょう:

// eslint-disable-next-line no-eval
const result = safeToEval ? eval(input) : fallback(input);

ここに別の条件を追加する必要が生じたとします:

// eslint-disable-next-line no-eval
const result = safeToEval && settings.allowNativeEval ? eval(input) : fallback(input);

Prettierはこれを次のように変換します:

// eslint-disable-next-line no-eval
const result =
safeToEval && settings.allowNativeEval ? eval(input) : fallback(input);

これによりeslint-disable-next-lineコメントが無効化されます。この場合、コメントを移動する必要があります:

const result =
// eslint-disable-next-line no-eval
safeToEval && settings.allowNativeEval ? eval(input) : fallback(input);

可能であれば、行範囲で動作するコメント(例: eslint-disableeslint-enable)やステートメントレベルで動作するコメント(例: /* istanbul ignore next */)を優先してください。これらはより安全です。eslint-disable-lineeslint-disable-next-line コメントの使用を禁止するには、eslint-plugin-eslint-comments が利用できます。

非標準構文に関する免責事項

Prettierは、ECMAScriptの早期提案段階の機能や、いかなる仕様でも定義されていないMarkdown構文拡張など、非標準の構文を認識してフォーマットできる場合がよくあります。このような構文のサポートはベストエフォートかつ実験的と見なされます。互換性のない変更がどのリリースでも導入される可能性があり、これは破壊的変更とは見なされません。

機械生成ファイルに関する免責事項

package.jsoncomposer.lock などの一部のファイルは、パッケージマネージャによって機械生成され、定期的に更新されます。Prettierが他のファイルと同じJSONフォーマットルールを適用すると、これらのツールと頻繁に競合が発生します。この不便を避けるため、Prettierは代わりに JSON.stringify ベースのフォーマッタをこれらのファイルに使用します。垂直方向の空白が削除されるなど差異が生じる場合がありますが、これは意図された動作です。

Prettierが_対象としない_こと

Prettierはコードを_出力_するだけで、変換は行いません。これはPrettierのスコープを制限するためです。出力に集中し、それを最高水準で実現しましょう!

以下はPrettierのスコープ外の例です:

  • 単一引用符/二重引用符の文字列とテンプレートリテラルの相互変換

  • 長い文字列リテラルをプリント幅に収めるための + による分割

  • 省略可能な箇所での {}return の追加/削除

  • ?: から if-else ステートメントへの変換

  • インポート、オブジェクトキー、クラスメンバー、JSXキー、CSSプロパティなどの並べ替え/移動(前述のように出力だけでなく_変換_となることに加え、副作用(例:インポート)による潜在的な安全性の問題や、最も重要な正しさ目標の検証困難化のため)