CommunityRédaction et éditiongithub.com

jessegall/commandments-writing-detectors

Comment rédiger votre propre commandement — une règle locale au projet qui juge chaque fichier par la suite. À lire AVANT d'écrire ou de modifier une règle dans `.commandments/custom/`, lorsque l'utilisateur demande une nouvelle règle/vérification/détecteur, lorsque `commandments make` vous y renvoie, ou lorsque vous êtes sur le point d'utiliser une expression régulière pour inspecter du code.

Qu'est-ce que commandments-writing-detectors ?

Ce skill explique comment écrire des règles de jugement personnalisées (commandements) pour l'outil `commandments`, un moteur d'analyse statique basé sur des règles. Chaque règle est un fichier JSON placé dans `.commandments/custom/` qui définit un sélecteur (le type de nœud à inspecter), des étapes `where` (qui conservent un nœud si une condition est vraie) et des étapes `reject` (qui l'excluent). Les règles peuvent être composées : une règle peut en appeler une autre par son nom, évitant ainsi la duplication. Les vérifications disponibles (générées par l'outil) incluent des tests sur le nom, le type, la résolution de symbole, la position dans l'arbre syntaxique, les commentaires, les appels, et bien plus. Le skill insiste sur le fait qu'une règle est une question que le moteur répond : chaque étape fait exactement une vérification. Il prend en charge plusieurs langages (PHP, JavaScript, Python, C#, etc.) via des ponts et se concentre sur la création de règles locales au projet sans écrire de code externe.

Compatible avec✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/jessegall/code-commandments/tree/HEAD/skills/writing-detectors

Installed? Explore more Rédaction et édition skills: steipete/notion, langchain-ai/langchain, bytedance/podcast-generation · View all 6 →

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Écrire vos propres commandements

Les règles fournies ne sont pas le plafond. Une convention que vous répétez sans cesse dans vos relectures est une règle que vous n'avez pas encore écrite. Écrivez-la une fois, et elle jugera chaque fichier à partir de là.

Une règle est une question à laquelle le moteur répond

Une règle qui vous est propre est une donnée que le binaire exécute — un fichier .json dans .commandments/custom/, dans ce dossier ou tout sous-dossier, de sorte qu'un projet avec des règles pour plusieurs parties de son code les regroupe par ce qu'elles protègent (custom/core/, custom/viewer/) — et elle pose les mêmes questions que toute règle fournie. Une règle est nommée par son FICHIER, où qu'il se trouve, donc déplacer une règle dans un dossier laisse intacte toute mention de celle-ci dans la configuration, et deux fichiers du même nom sont tous deux refusés plutôt que l'un l'emporte silencieusement. Elle nomme le moteur qu'elle juge, le péché qu'elle trouve, et une requête : un sélecteur qui l'ouvre, puis des étapes where qui conservent un nœud et des étapes reject qui en retirent un. Chaque étape fait exactement une vérification ; une règle n'est aussi bonne que la question que chaque étape pose.

{
    "engine": "php",
    "sin": {
        "name": "raw-sql",
        "description": "SQL écrit en ligne sur le site d'appel.",
        "rule": "Les requêtes passent par le repository propriétaire de la table.",
        "skill": "no-raw-sql"
    },
    "find": {
        "select": "call",
        "where": [
            {"resolves": "Illuminate\\Support\\Facades\\DB", "of": "child:class"},
            {"nameIn": ["select", "statement", "unprepared"]}
        ],
        "reject": [
            {"file": "*Repository.php"}
        ]
    }
}

Le sélecteur est un type neutre, qui se lit de la même manière dans tous les langages — 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 — ou kind:<Kind>, un type de nœud propre au langage (kind:Expr_MethodCall) lorsqu'aucun type neutre ne le dit.

Une règle est construite à partir d'autres règles. Une vérification que vous avez déjà écrite une fois est nommée, jamais réécrite : {"rule": "ActionClass"} réussit lorsque la règle de ce nom trouve aussi le nœud, et un find qui écrit "from": "ActionClass" à la place de "select" réduit ce que cette règle a trouvé, ce qui est une règle lue à l'intérieur d'une autre. Les deux atteignent toute règle du même moteur dans .commandments/custom/, et chaque partie reste une règle en soi, listée et exécutée selon ses propres termes. Un nom que rien ne déclare, une règle d'un autre moteur et une chaîne qui revient sur elle-même sont refusés lorsque les règles sont lues, avant que quoi que ce soit ne soit jugé.

Dites either et not sans une seconde règle. {"any": [{"is": "loop"}, {"is": "branch"}]} réussit lorsque l'une de ses étapes le fait, {"all": [...]} lorsque chacune le fait, et {"not": {...}} lorsque son étape ne le fait pas — c'est ainsi qu'un where dit ce que seul un reject pourrait dire.

La vérification unique d'une étape — toutes celles qui existent, générées à partir de l'outil lui-même :

Ce que c'est et comment c'est appelé

VérificationConserve le nœud lorsque
{"is": "loop"}il répond au type neutre
{"kind": "Expr_StaticCall"}c'est le type propre au langage, écrit comme le langage l'écrit ou comme le vocabulaire l'orthographie : Expr_StaticCall et static-call sont une même construction. Les orthographes sont dérivées des types, donc la propre complétion de l'éditeur liste ce qu'un langage émet réellement, et une orthographe partagée par deux types est l'orthographe d'aucun des deux
{"name": "select"}son nom — celui d'un appel ou d'un accès est son enfant de nom — est celui-ci
{"nameIn": ["select", "statement"]}son nom est l'un d'eux
{"nameLike": "get*"}son nom correspond au glob, * toute séquence de caractères et ? un seul
`{"nameMatches": "^(getset)[A-Z]"}`
{"nameMood": "question"}son nom est dans ce mode linguistique : bare-verb est le verbe seul (holds, ships), third-person raconte un verbe (entersTestMode), et question commence par un mot interrogatif (is, has, can, should). Il lit le mode, jamais une liste de mots qu'une règle écrit. C'est la seule vérification ici qui juge un NOM, et elle répond seulement si ce nom a une certaine ALLURE : ce qui fait de ce mode un péché, c'est le mode par rapport à ce que la chose FAIT, alors associez-le à ce que le membre répond ou à savoir si cela change quelque chose, comme le font les règles fournies
{"nameCase": "snake"}son nom, moins un $ ou _ en tête, est écrit en camel, pascal, snake, upper ou kebab case
{"hasModifier": "static"}il porte le modificateur
{"hasFlag": "byRef"}il porte le drapeau que l'arbre de son langage définit
{"operator": "*"}son opérateur est exactement celui-ci : * et ?? distinguent deux expressions binaires, += une affectation, ! unaire, extends une clause d'héritage
{"operatorIn": ["*", "/"]}son opérateur est l'un d'eux

Ce que ça dit

VérificationConserve le nœud lorsque
{"text": "select * from users"}le texte d'un littéral est exactement celui-ci
{"textLike": "*where id*"}le texte d'un littéral correspond au glob
{"textMatches": "(?i)^select"}le texte d'un littéral correspond à l'expression régulière
{"commentIs": "history"}un commentaire est l'une des lectures calibrées de la prose : history raconte pourquoi le code a été MODIFIÉ plutôt que ce qu'il fait, strawman se défend contre une erreur que personne n'allait commettre, et restates ne dit rien que le code qu'il documente ne dit pas. Les lectures sont propres au package de prose, mesurées par ses tests dans chaque langage, et l'ensemble est fermé : une règle demande un jugement qui a été calibré, jamais une prose correspondant à des mots qu'elle écrit elle-même. Demandé d'un commentaire ("select": "comment"), elle juge ce commentaire, et demandé de code, elle juge les commentaires écrits pour lui
{"docReference": {"resolves": false}}un commentaire de documentation nomme du code qui a DISPARU : une référence qu'il fait (<see cref>, {@see}, @see, Sphinx :class:) ne résout à rien, et le projet aurait dû la posséder. Deux références sont laissées tranquilles, ce qui est toute la différence entre ceci et un scanner de liens brisés : une que le compilateur n'aurait jamais pu résoudre de toute façon, et une dont le propriétaire est déclaré en dehors du scan, car rien ici ne peut dire si ce propriétaire a encore le membre. La résolution est propre au pont
{"commentLike": "*TODO*"}un commentaire sur lui, ou dans la suite de commentaires directement au-dessus, correspond au glob — et, demandé d'un commentaire lui-même ("select": "comment"), ce que CE commentaire dit, donc une clé lit un commentaire quelle que soit la manière dont une règle l'atteint
`{"commentMatches": "(?i)\b(TODOFIXME)\b"}`
{"docTag": "deprecated"}sa documentation porte la balise : @tag en PHPDoc et JSDoc, un élément XML en C#, un champ ou une directive Sphinx ou une section Google/NumPy dans une docstring Python
{"documented": false}il porte un commentaire de documentation, ou non avec false

À quoi ça se réfère

VérificationConserve le nœud lorsque
{"resolves": "App\\Models\\User"}ce à quoi son nom se réfère, résolu, est cela — ou, pour une valeur qui ne nomme rien (une variable, une propriété), la classe qu'elle contient : cette classe exacte, pas une qui l'étend
{"resolvesLike": "App\\Models\\*"}ce à quoi son nom se réfère, résolu, correspond au glob, ou la classe qu'une valeur contient le fait : {"resolvesLike": "*Service", "of": "child:var"} est un récepteur de n'importe quel *Service
{"declaredBy": "App\\Models\\*"}le membre qu'il lit est déclaré par une classe nommée par le glob : la classe que son récepteur contient, remontée par ses parents jusqu'à ce que l'un déclare ce membre. C'est à qui appartient ce champ, ce qu'aucun nom ne peut dire, et il est vide — donc l'étape ne conserve rien — lorsque le récepteur n'a pas de type dans le scan ou qu'aucune déclaration de celui-ci ne déclare le membre
{"answeredBy": false}le type qui déclare ce qu'il lit nomme déjà la question qu'il pose — une méthode de ce type dont tout le corps retourne la même chose, lue dans les deux sens — ou NE le fait PAS avec false. C'est ce qui distingue utilise le nom qui existe de donne-lui un nom, et une question posée par la méthode même qui la nomme n'est jamais l'un des endroits qui la posent
{"sameAs": {"of": "closest:loop", "target": "child:iter"}}il nomme la même chose qu'un autre nœud du même fichier, atteint en suivant of à partir de lui puis target à partir de là — la seule question qu'une règle ne pourrait pas poser : un remove sur la collection même que sa boucle parcourt, un argument qui est le champ dont il vient d'être lu. Les deux doivent être écrits de la même manière, sinon lus comme le même arbre ; aucun nœud au bout du chemin n'est un passage
{"declaredElsewhere": true}ce qu'il nomme est déclaré dans un autre FICHIER que celui où il se trouve, ou ne l'est pas avec false. Un champ défini depuis l'extérieur du fichier qui le déclare a contourné les méthodes que sa classe a écrites pour se modifier, ce qui est la question que pose une règle sur les affaires propres d'un type ; il ne contient rien là où aucune déclaration dans le scan ne déclare ce qu'il nomme, donc une règle dit aussi le propriétaire
{"type": "number"}le type de l'expression, tel que son langage le lit : ce qu'un vérificateur lui a donné, ce que sa forme dit, ou le type que sa déclaration écrit — un primitif parmi eux, où resolves répond à une classe seule
{"typeLike": "Money*"}le type de l'expression correspond au glob
{"calls": {"name": "dd"}}un appel de son propre cru — pas un à l'intérieur d'une fonction imbriquée en lui — passe l'étape
{"argument": {"at": 0, "is": "literal"}}l'argument à cet endroit passe l'étape ; un at négatif compte depuis la fin, -1 le dernier, et aucun là ne passe rien. Sans at du tout, N'IMPORTE QUEL argument passant l'étape suffit, c'est ainsi qu'une règle pose une question sur un argument passé par nom, dont la place ne l'identifie pas
{"constructs": "Date*"}une construction, ou sur toute autre chose qui lui est propre, crée une instance d'un type nommé par le glob — en Python, un appel d'une classe
{"unused": true}rien ne se réfère à lui : une fonction ou un type depuis l'extérieur de lui-même, à travers les noms et les appels que le scan résout et le graphe d'appels de son langage ; un paramètre, par son nom lu dans sa fonction ; un champ ou une propriété, par ce qui atteint sa déclaration à travers le type que son récepteur contient, car un champ ne se réfère à aucun symbole dans la plupart des langages. Un membre que le langage lui-même atteint par nom, et un qui surcharge, ne sont pas cela : rien de ce que le projet écrit n'a à les nommer
{"referredFrom": {"is": "parameter"}}quelque chose qui se réfère à lui — un appel, un nom, un type écrit — passe l'étape. C'est calledFrom qui pose une question sur la RÉFÉRENCE plutôt que sur le fichier où elle se trouve, ce qui distingue un côté d'une chose d'un autre : une classe d'événement nommée par un envoi et par aucun paramètre d'écouteur n'est entendue par personne, et {"not": {"referredFrom": …}} dit exactement cela
{"calledFrom": "app/Http/**"}quelque chose qui se réfère à lui — un appel, le plus souvent — se trouve dans un fichier correspondant au glob, lu comme file lit un glob

Où ça se trouve

VérificationConserve le nœud lorsque
{"file": "*Repository.php"}son chemin depuis le projet (le référentiel dans lequel il se trouve, sinon le dossier jugé), ou toute queue de celui-ci, correspond au glob : * reste dans un dossier et ** les traverse, comme dans .gitignore
`{"fileMatches": "(^/)pages/.*(Create
{"namespaceLike": "App\\Http\\*"}l'espace de noms, le package ou le module dans lequel il est déclaré correspond au glob
{"layer": "App\\Domain"}il se trouve dans cette couche de la pile que le projet déclare (backend, Python, C#)
{"topLevel": true}il se trouve en dehors de toute fonction, y compris les fermetures, et de tout type — du code qui s'exécute lorsque son fichier se charge
{"withinLoop": true}il se trouve à l'intérieur d'une boucle, ou non avec false
{"position": "first"}il est le first, last ou only de ses frères (un seul est aussi premier et dernier)
{"field": "cond"}il remplit cet emplacement de son parent dans l'arbre — le nom propre au langage pour l'emplacement, comme cond, body, else, args — ce qui est le seul axe de lieu que les autres vérifications ne demandent jamais : un appel se tenant COMME une condition plutôt qu'un passé en argument. Les noms sont ceux de son langage, donc une règle écrite avec l'un d'eux juge ce langage
{"role": "member"}c'est ce genre de chose, dans les termes que chaque pont écrit : statement, expression, member (toute déclaration d'un type, d'une fonction, d'une propriété ou d'un cas d'énumération), type (un type tel qu'écrit), pattern, markup ou other. C'est ainsi qu'une règle dit une déclaration sans nommer un type par langage, ce dont une règle sur l'ordre ou la compagnie des membres a besoin
{"descendant": {"is": "return"}}un nœud à l'intérieur de lui passe l'étape ; avec "within": "ownBody" seuls les nœuds qu'il contient LUI-MÊME sont parcourus, en s'arrêtant à une fonction imbriquée en lui, ce qui est la différence entre une méthode qui sauvegarde et une qui remet une fermeture contenant la sauvegarde à une file d'attente
{"child": {"is": "return"}}un nœud un niveau à l'intérieur de lui passe l'étape, ce que descendant à n'importe quelle profondeur ne peut pas dire : une fonction dont le propre corps retourne, et rien pour une qui retourne seulement à l'intérieur d'un if. Avec "field" seuls les enfants remplissant ce champ de l'arbre sont parcourus, comme le champ d'une comptée réduit ce qu'elle compte
{"body": {"is": "branch"}}une instruction de son CORPS passe l'étape, lue à travers ce que son langage enveloppe un corps : C# écrit une instruction de bloc et Python écrit des instructions nues, donc child y répond différemment et ceci y répond de la même manière. C'est ainsi qu'une règle sur le corps d'une boucle ou d'une fonction est écrite une fois pour chaque langage
{"inside": {"is": "catch"}}un nœud au-dessus de lui, jusqu'à la racine du fichier, passe l'étape
{"all": [{"name": "store"}, {"hasModifier": "pu…

…

[Le contenu a été tronqué à des fins de traduction]

Skills associés