独自の戒律を書く
出荷済みのルールが上限ではありません。コードレビューで繰り返し指摘する規約は、まだルールとして書き下ろしていないものです。一度書けば、それ以降すべてのファイルを自動で判断します。
ルールとは、エンジンが答える質問である
独自のルールは、バイナリが実行するデータです — .commandments/custom/ 内の .json ファイルです。このフォルダ、またはその下の任意のサブフォルダ(例: custom/core/, custom/viewer/)に配置することで、コードの各部分を保護するルールをグループ化できます。ルールはファイル名で識別され、移動しても設定内の参照はそのまま有効です。同名のファイルが2つある場合、どちらかが静かに優先されるのではなく、両方が拒否されます。ルールは、検査対象のエンジン、検出する違反(sin)、およびクエリを指定します。クエリは、対象を開くセレクタ、ノードを保持する where ステップ、ノードを除外する reject ステップで構成されます。各ステップは正確に1つのチェックを行います。ルールの品質は、各ステップが問う質問の質に依存します。
{
"engine": "php",
"sin": {
"name": "raw-sql",
"description": "コールサイトにインラインで書かれたSQL。",
"rule": "クエリはテーブルを所有するリポジトリを経由すること。",
"skill": "no-raw-sql"
},
"find": {
"select": "call",
"where": [
{"resolves": "Illuminate\\Support\\Facades\\DB", "of": "child:class"},
{"nameIn": ["select", "statement", "unprepared"]}
],
"reject": [
{"file": "*Repository.php"}
]
}
}
セレクタは言語に依存しない中立な種類です — call, construction, function, type-declaration, assignment, loop, branch, return, throw, catch, comparison, literal, member-access, null-safe, identifier, import, parameter, block, bail-out, expression-statement, self-reference などです。または kind:<Kind> で、言語固有のノード種類(kind:Expr_MethodCall)を指定することもできます。中立な種類で表現できない場合に使います。
ルールは他のルールから構築されます。 既に書いたチェックは名前で再利用できます。{"rule": "ActionClass"} は、その名前のルールがノードを検出した場合に通過します。また、find 内で "select" の代わりに "from": "ActionClass" と書くと、そのルールが検出したノードを絞り込みます。これはルールが別のルールの中で読み込まれる仕組みです。どちらも同じエンジンの .commandments/custom/ 内の任意のルールにアクセスでき、各部分は独立したルールとして、独自の条件でリストされ実行されます。存在しない名前、別のエンジンのルール、そして自分自身に戻ってくるチェーンは、ルール読み込み時に拒否されます(何も検査する前に行われます)。
either と not を別のルールなしで使う。 {"any": [{"is": "loop"}, {"is": "branch"}]} は、いずれかのステップが通過した場合に通過します。{"all": [...]} はすべてのステップが通過した場合に通過します。{"not": {...}} はそのステップが通過しなかった場合に通過します。これは where で reject だけができることを表現する方法です。
ステップの1つのチェック — ツール自体から生成されたすべてのチェック:
それが何であり、何と呼ばれるか
| チェック | ノードを保持する条件 |
|---|---|
{"is": "loop"} | 中立な種類に該当する場合 |
{"kind": "Expr_StaticCall"} | 言語固有の種類であり、言語の表記または語彙のスペルに一致する場合: Expr_StaticCall と static-call は同じ構成要素です。スペルは種類から派生するため、エディタの補完リストは言語が実際に出力するものを示し、2つの種類がスペルを共有する場合、それはどちらでもない |
{"name": "select"} | 名前が一致する場合 — 呼び出しやアクセスの名前は、その子の名前 |
{"nameIn": ["select", "statement"]} | 名前がそのいずれかに一致する場合 |
{"nameLike": "get*"} | 名前がグロブパターンに一致する場合(* は任意の文字列、? は1文字) |
| `{"nameMatches": "^(get | set)[A-Z]"}` |
{"nameMood": "question"} | 名前が特定の言語学的ムードである場合: bare-verb は動詞のみ(holds, ships)、third-person は動詞を叙述する(entersTestMode)、question は疑問詞で始まる(is, has, can, should)。ムードを読み取り、ルールが書く単語リストではありません。ここで唯一NAMEを判断するチェックであり、名前が特定の読み方をするかどうかのみを答えます:何を罪とするかは、ムードとそのものが何をするかの組み合わせです。出荷済みルールのように、メンバーが何を返すかや何かを変更するかと組み合わせてください |
{"nameCase": "snake"} | 名前(先頭の $ や _ を除く)が camel, pascal, snake, upper, kebab のいずれかのケースである場合 |
{"hasModifier": "static"} | 修飾子を持つ場合 |
{"hasFlag": "byRef"} | 言語のツリーが設定するフラグを持つ場合 |
{"operator": "*"} | 演算子が完全に一致する場合: * と ?? は二項演算子を区別し、+= は代入、! は単項演算子、extends は継承句 |
{"operatorIn": ["*", "/"]} | 演算子がいずれかに一致する場合 |
それが言うこと
| チェック | ノードを保持する条件 |
|---|---|
{"text": "select * from users"} | リテラルのテキストが完全に一致する場合 |
{"textLike": "*where id*"} | リテラルのテキストがグロブパターンに一致する場合 |
{"textMatches": "(?i)^select"} | リテラルのテキストが正規表現に一致する場合 |
{"commentIs": "history"} | コメントが校正済みの散文の読みの1つである場合: history はコードの動作ではなく変更理由を説明するもの、strawman は誰も犯さない間違いを防ぐもの、restates は文書化対象のコードが既に言っていることを繰り返すもの。読みは散文パッケージ自身のものであり、あらゆる言語でのテストで測定されています。セットは固定です:ルールは校正された判断を要求し、ルール自身が書いた単語と一致する散文を要求しません。コメントに対して使用された場合("select": "comment")はそのコメントを判断し、コードに対して使用された場合はそのコードに付けられたコメントを判断します |
{"docReference": {"resolves": false}} | ドキュメントコメントが存在しないコードを参照している場合: 参照(<see cref>, {@see}, @see, Sphinx :class:)が解決できず、プロジェクトが所有すべきものである場合。2つの参照は無視されます(これがリンク切れスキャナーとの違いです):1つはコンパイラがそもそも解決できないもの、もう1つはスキャン範囲外の所有者が宣言しているものです。このスキルではその所有者がまだそのメンバーを持っているかどうかは判断できないためです。解決はブリッジ自身のものです |
{"commentLike": "*TODO*"} | そのノードのコメント、またはその上の連続したコメントがグロブパターンに一致する場合 — コメント自体に使用された場合("select": "comment")は、そのコメントが言う内容が一致する場合。1つのキーで、ルールがどのように到達したかに関わらずコメントを読み取ります |
| `{"commentMatches": "(?i)\b(TODO | FIXME)\b"}` |
{"docTag": "deprecated"} | ドキュメントがそのタグを持つ場合: PHPDoc と JSDoc の @tag、C# の XML 要素、Python の docstring 内の Sphinx フィールド/ディレクティブ、Google/NumPy セクション |
{"documented": false} | ドキュメントコメントを持つ場合、または false で持たない場合 |
それが参照するもの
| チェック | ノードを保持する条件 |
|---|---|
{"resolves": "App\\Models\\User"} | 名前が解決する参照先がそれである場合 — または、値が何も名指さない場合(変数、プロパティ)は、それが保持するクラス:その正確なクラスであり、拡張したものではない |
{"resolvesLike": "App\\Models\\*"} | 名前が解決する参照先がグロブパターンに一致する場合、または値が保持するクラスが一致する場合: {"resolvesLike": "*Service", "of": "child:var"} は任意の *Service のレシーバ |
{"declaredBy": "App\\Models\\*"} | 読み取るメンバーが、グロブパターンが指定するクラスによって宣言されている場合: レシーバが保持するクラスから、その親をたどってそのメンバーを宣言するクラスまで。これは、そのフィールドが誰のものであるかを示し、名前だけでは判断できません。レシーバがスキャン内で型を持たない、またはそのメンバーを宣言する宣言がない場合、ステップは何も保持しません |
{"answeredBy": false} | 読み取るものを宣言する型が、既にその質問(同じものを返すメソッド全体)に名前を付けている場合 — または false でそうでない場合。これは「既存の名前を使う」と「名付ける」を区別し、そのメソッド自体が質問している名前は、質問する場所には含まれません |
{"sameAs": {"of": "closest:loop", "target": "child:iter"}} | 同じファイル内の他のノードと同じものを名指す場合。of でそのノードから出発し、target でそこから到達する — ルールが単一の質問で問えないもの: ループが歩いているコレクション自体に対する remove、読み取った直後のフィールドである引数。両者は同じように書かれている必要があり、そうでなければ同じツリーとして読み取られます。パスの終わりにノードがない場合は通過しません |
{"declaredElsewhere": true} | 名指すものが、そのファイルとは別のファイルで宣言されている場合。または false でそうでない場合。宣言ファイルの外から設定されるフィールドは、そのクラスが自身を変更するために書いたメソッドを迂回しており、これが型の内部処理に関するルールが問う質問です。スキャン内でそのものを宣言する宣言がない場合、何も保持しないため、ルールは所有者も指定します |
{"type": "number"} | 式の型が、言語が解釈する通りである場合: チェッカーが与えたもの、その形状が示すもの、宣言が書く型 — プリミティブのいずれか。resolves はクラスのみを扱います |
{"typeLike": "Money*"} | 式の型がグロブパターンに一致する場合 |
{"calls": {"name": "dd"}} | 自身の呼び出し(内部のネストされた関数内のものではない)がステップを通過する場合 |
{"argument": {"at": 0, "is": "literal"}} | その位置の引数がステップを通過する場合。負の at は末尾から数え、-1 が最後。存在しない場合は何も通過しません。at がない場合、任意の引数がステップを通過すれば十分です。これは、名前付き引数で渡されるものについて、位置が識別子でない場合にルールが問う方法です |
{"constructs": "Date*"} | コンストラクション、または他の何かが、グロブパターンが指定する型のインスタンスを作成する場合 — Python ではクラスの呼び出し |
{"unused": true} | 何もそれを参照しない場合: 関数や型は自身の外部から、スキャンが解決する名前と呼び出しと言語のコールグラフを通じて;パラメータはその関数内で名前が読み取られるかどうか;フィールドやプロパティは、レシーバが保持する型を通じてその宣言に到達するもの。言語自体が名前で到達するメンバーや、オーバーライドするものは該当しません:プロジェクトが書くものはそれらを名指す必要はありません |
{"referredFrom": {"is": "parameter"}} | それを参照するもの(呼び出し、名前、書かれた型)がステップを通過する場合。calledFrom が参照自体について問うのではなく、それが置かれているファイルについて問う点が異なります。これにより、ものの一方の側を他方から区別します: ディスパッチによって名前が付けられたが、どのリスナーのパラメータにも現れないイベントクラスは誰にも聞かれていません。{"not": {"referredFrom": …}} はまさにそれを示します |
{"calledFrom": "app/Http/**"} | それを参照するもの(ほとんどの場合、呼び出し)が、グロブパターンに一致するファイルにある場合。file と同じように読み取ります |
それがどこにあるか
| チェック | ノードを保持する条件 |
|---|---|
{"file": "*Repository.php"} | プロジェクトからのパス(リポジトリ内、または判定対象フォルダ内)の任意の末尾がグロブパターンに一致する場合: * はフォルダ内、** はフォルダをまたぎます (.gitignore と同様) |
| `{"fileMatches": "(^ | /)pages/.*(Create |
{"namespaceLike": "App\\Http\\*"} | 宣言された名前空間、パッケージ、モジュールがグロブパターンに一致する場合 |
{"layer": "App\\Domain"} | プロジェクトが宣言するスタックのそのレイヤー(バックエンド、Python、C#)にある場合 |
{"topLevel": true} | すべての関数(クロージャも含む)とすべての型の外にある場合 — ファイル読み込み時に実行されるコード |
{"withinLoop": true} | ループ内にある場合、または false でない場合 |
{"position": "first"} | 兄弟の中で first(最初), last(最後), only(唯一)である場合(唯一のものは最初かつ最後でもある) |
{"field": "cond"} | 木構造内の親のそのスロット(言語自身のスロット名、例: cond, body, else, args)を埋める場合。これは他のチェックでは決して問わない位置の軸です: 条件として立つ呼び出しと、引数として渡される呼び出しを区別します。名前は言語のものであるため、その名前で書かれたルールはその言語を判断します |
{"role": "member"} | すべてのブリッジが書く言葉で、その種類のものである場合: statement, expression, member(型、関数、プロパティ、enum ケースの任意の宣言), type(書かれた型), pattern, markup, other。これは言語ごとに種類を指定せずに「宣言」と言う方法であり、メンバーの順序や仲間に関するルールに必要です |
{"descendant": {"is": "return"}} | その内部のノードがステップを通過する場合。"within": "ownBody" を指定すると、自身が保持するノードのみが対象となり、ネストされた関数で停止します。これは、メソッドが保存する場合と、保存処理をクロージャとしてキューに渡す場合の違いです |
{"child": {"is": "return"}} | その1レベル内部のノードがステップを通過する場合。descendant は任意の深さを扱えるため、これでしか言えないことがあります: 自身の本体が return する関数と、if 内でのみ return する関数を区別します。"field" を指定すると、そのフィールドを埋める子のみが対象となり、カウントのフィールドがカウント対象を絞り込みます |
{"body": {"is": "branch"}} | そのBODYの文がステップを通過する場合。言語が本体をラップする方法に関わらず読み取ります: C# はブロック文、Python は裸の文のため、child では異なる結果になりますが、これは統一して扱います。これにより、ループや関数の本体に関するルールを1つの言語に依存せずに書くことができます |
{"inside": {"is": "catch"}} | そのノードの上(ファイルルートまで)のノードがステップを通過する場合 |
{"all": [{"name": "store"}, {"hasModifier": "pu… |
(以下同様に、元の内容の残りを翻訳して続けてください)