Escribiendo tus propios mandamientos
Las reglas incluidas no son el techo. Una convención que repites en revisiones es una regla que aún no has escrito. Escríbela una vez, y juzgará cada archivo a partir de ese momento.
Una regla es una pregunta que el motor responde
Una regla propia son datos que el binario ejecuta: un archivo .json en .commandments/custom/, dentro de esa carpeta o cualquier subcarpeta (así un proyecto con reglas para varias partes de su código las agrupa por lo que protegen: custom/core/, custom/viewer/), y pregunta lo mismo que pregunta cualquier regla incluida. Una regla se nombra por su ARCHIVO dondequiera que esté, por lo que moverlo a una carpeta deja en pie cada mención en la configuración, y dos archivos del mismo nombre se rechazan ambos en lugar de que uno gane en silencio. Nombra el motor que juzga, el pecado que encuentra y una consulta: un selector que la abre, luego pasos where que mantienen un nodo y pasos reject que eliminan uno. Cada paso realiza exactamente una comprobación; una regla es tan buena como la pregunta que cada paso hace.
{
"engine": "php",
"sin": {
"name": "raw-sql",
"description": "SQL escrito en línea en el punto de llamada.",
"rule": "Las consultas pasan por el repositorio propietario de la tabla.",
"skill": "no-raw-sql"
},
"find": {
"select": "call",
"where": [
{"resolves": "Illuminate\\Support\\Facades\\DB", "of": "child:class"},
{"nameIn": ["select", "statement", "unprepared"]}
],
"reject": [
{"file": "*Repository.php"}
]
}
}
El selector es un tipo neutral, que se lee igual en todos los lenguajes: 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 — o kind:<Kind>, un tipo de nodo propio del lenguaje (kind:Expr_MethodCall) cuando ningún tipo neutral lo expresa.
Una regla se construye a partir de otras reglas. Una comprobación que ya has escrito una vez se nombra, nunca se vuelve a escribir: {"rule": "ActionClass"} pasa cuando la regla con ese nombre también encuentra el nodo, y un find que escribe "from": "ActionClass" en lugar de "select" reduce lo que esa regla encontró, lo que es una regla leída dentro de otra. Ambas alcanzan cualquier regla del mismo motor en .commandments/custom/, y cada parte sigue siendo una regla por sí misma, listada y ejecutada en sus propios términos. Un nombre que nada declara, una regla de otro motor y una cadena que vuelve a sí misma se rechazan cuando se leen las reglas, antes de que se juzgue nada.
Di either y not sin una segunda regla. {"any": [{"is": "loop"}, {"is": "branch"}]} pasa cuando uno de sus pasos lo hace, {"all": [...]} cuando todos lo hacen, y {"not": {...}} cuando su paso no lo hace — que es cómo un where dice lo que solo un reject podría.
La única comprobación de un paso — todas las que existen, generadas desde la propia herramienta:
Qué es y cómo se llama
| Comprobación | Mantiene el nodo cuando |
|---|---|
{"is": "loop"} | responde al tipo neutral |
{"kind": "Expr_StaticCall"} | es el tipo propio del lenguaje, escrito como el lenguaje lo escribe o como el vocabulario lo deletrea: Expr_StaticCall y static-call son una misma construcción. Los deletreos se derivan de los tipos, por lo que el propio autocompletado del editor lista lo que un lenguaje realmente emite, y un deletreo que comparten dos tipos es el deletreo de ninguno |
{"name": "select"} | su nombre — el de una llamada o un acceso es el de su hijo nombre — es ese |
{"nameIn": ["select", "statement"]} | su nombre es uno de ellos |
{"nameLike": "get*"} | su nombre coincide con el glob, * cualquier secuencia de caracteres y ? uno |
| `{"nameMatches": "^(get | set)[A-Z]"}` |
{"nameMood": "question"} | su nombre está en ese modo lingüístico: bare-verb es el verbo y nada más (holds, ships), third-person narra un verbo (entersTestMode), y question comienza con una palabra interrogativa (is, has, can, should). Lee el modo, nunca una lista de palabras que escribe una regla. Es la única comprobación aquí que juzga un NOMBRE, y responde solo si el nombre SE LEE de cierta manera: lo que convierte eso en un pecado es el modo en contra de lo que la COSA HACE, así que combínalo con lo que responde el miembro o si cambia algo, como hacen las reglas incluidas |
{"nameCase": "snake"} | su nombre, menos un $ o _ inicial, está escrito en camel, pascal, snake, upper o kebab |
{"hasModifier": "static"} | lleva el modificador |
{"hasFlag": "byRef"} | lleva la bandera que establece el árbol de su lenguaje |
{"operator": "*"} | su operador es exactamente ese: * y ?? distinguen dos expresiones binarias, += una asignación, ! una unaria, extends una cláusula de herencia |
{"operatorIn": ["*", "/"]} | su operador es uno de ellos |
Qué dice
| Comprobación | Mantiene el nodo cuando |
|---|---|
{"text": "select * from users"} | el texto de un literal es exactamente ese |
{"textLike": "*where id*"} | el texto de un literal coincide con el glob |
{"textMatches": "(?i)^select"} | el texto de un literal coincide con la expresión regular |
{"commentIs": "history"} | un comentario es una de las lecturas calibradas de prosa: history narra por qué el código fue CAMBIADO en lugar de lo que hace, strawman defiende contra un error que nadie iba a cometer, y restates no dice nada que no diga el código que documenta. Las lecturas son propias del paquete de prosa, medidas por sus pruebas en cada lenguaje, y el conjunto es cerrado: una regla pide un juicio que fue calibrado, nunca una prosa que coincida con palabras que ella misma escribe. Preguntado a un comentario ("select": "comment") juzga ese comentario, y preguntado a un código juzga los comentarios escritos para él |
{"docReference": {"resolves": false}} | un comentario de documentación nombra código que HA DESAPARECIDO: una referencia que hace (<see cref>, {@see}, @see, Sphinx :class:) no resuelve a nada, y el proyecto habría tenido que poseerlo. Dos referencias se dejan intactas, que es toda la diferencia entre esto y un escáner de enlaces rotos: una que el compilador nunca podría haber resuelto de todos modos, y una cuyo dueño está declarado fuera del escaneo, ya que nada aquí puede decir si ese dueño aún tiene el miembro. La resolución es propia del puente |
{"commentLike": "*TODO*"} | un comentario sobre él, o en la serie de comentarios directamente encima, coincide con el glob — y, preguntado a un comentario en sí mismo ("select": "comment"), lo que DICE ese comentario, por lo que una clave lee un comentario sin importar cómo una regla lo alcanzó |
| `{"commentMatches": "(?i)\b(TODO | FIXME)\b"}` |
{"docTag": "deprecated"} | su documentación lleva la etiqueta: @tag en PHPDoc y JSDoc, un elemento XML en C#, un campo o directiva Sphinx o una sección Google/NumPy en un docstring de Python |
{"documented": false} | lleva un comentario de documentación, o no con false |
A qué se refiere
| Comprobación | Mantiene el nodo cuando |
|---|---|
{"resolves": "App\\Models\\User"} | a lo que su nombre se refiere, resuelto, es eso — o, para un valor que no nombra nada (una variable, una propiedad), la clase que contiene: esa clase exacta, no una que la extiende |
{"resolvesLike": "App\\Models\\*"} | a lo que su nombre se refiere, resuelto, coincide con el glob, o la clase que un valor contiene lo hace: {"resolvesLike": "*Service", "of": "child:var"} es un receptor de cualquier *Service |
{"declaredBy": "App\\Models\\*"} | el miembro que lee está declarado por una clase que nombra el glob: la clase que contiene su receptor, seguida por sus propios padres hasta que uno declara ese miembro. Es de quién es este campo, lo que ningún nombre puede decir, y está vacío — por lo que el paso no mantiene nada — donde el receptor no tiene tipo en el escaneo o ninguna declaración del mismo declara al miembro |
{"answeredBy": false} | el tipo que declara lo que lee ya nombra la pregunta que hace — un método de ese tipo cuyo cuerpo completo devuelve lo mismo, leído en cualquier dirección — o NO con false. Es lo que distingue usa el nombre que existe de dale un nombre, y una pregunta hecha por el mismo método que la nombra nunca es uno de los lugares que la hacen |
{"sameAs": {"of": "closest:loop", "target": "child:iter"}} | nombra lo mismo que otro nodo del mismo archivo, alcanzado siguiendo of desde él y luego target desde allí — la única pregunta que una regla no podría hacer: un remove en la misma colección que su bucle recorre, un argumento que es el campo del que se acaba de leer. Los dos deben estar escritos igual, de lo contrario leídos como el mismo árbol; ningún nodo al final de la ruta no es un pase |
{"declaredElsewhere": true} | lo que nombra está declarado en otro ARCHIVO que aquel en el que se encuentra, o no con false. Un campo establecido desde fuera del archivo que lo declara ha rodeado los métodos que su clase escribió para cambiarse a sí mismo, que es la pregunta que hace una regla sobre el negocio propio de un tipo; no contiene nada cuando ninguna declaración en el escaneo declara lo que nombra, por lo que una regla también dice el dueño |
{"type": "number"} | el tipo que tiene la expresión, como lo lee su lenguaje: lo que un verificador le dio, lo que dice su forma, o el tipo que escribe su declaración — un primitivo entre ellos, donde resolves responde solo a una clase |
{"typeLike": "Money*"} | el tipo que tiene la expresión coincide con el glob |
{"calls": {"name": "dd"}} | una llamada propia — no una dentro de una función anidada en ella — pasa el paso |
{"argument": {"at": 0, "is": "literal"}} | el argumento en ese lugar pasa el paso; un at negativo cuenta desde el final, -1 el último, y ninguno allí no pasa nada. Sin at en absoluto, CUALQUIER argumento que pase el paso es suficiente, que es cómo una regla pregunta sobre uno pasado por nombre, cuyo lugar no es lo que lo identifica |
{"constructs": "Date*"} | una construcción, o en cualquier otra cosa una propia, crea una instancia de un tipo que nombra el glob — en Python, una llamada a una clase |
{"unused": true} | nada se refiere a él: una función o tipo desde fuera de sí mismo, a través de los nombres y llamadas que el escaneo resuelve y el grafo de llamadas de su lenguaje; un parámetro, por su nombre leído en su función; un campo o propiedad, por lo que alcanza su declaración a través del tipo que contiene su receptor, ya que un campo no se refiere a ningún símbolo en la mayoría de los lenguajes. Un miembro al que el propio lenguaje accede por nombre, y uno que sobrescribe, no son esto: nada de lo que el proyecto escribe tiene que nombrarlos |
{"referredFrom": {"is": "parameter"}} | algo que se refiere a él — una llamada, un nombre, un tipo escrito — pasa el paso. Es calledFrom preguntando sobre la REFERENCIA en lugar del archivo en el que se encuentra, que es lo que distingue un lado de una cosa de otro: una clase de evento nombrada por un envío y por ningún parámetro de oyente no es escuchada por nadie, y {"not": {"referredFrom": …}} dice exactamente eso |
{"calledFrom": "app/Http/**"} | algo que se refiere a él — una llamada, más a menudo — se encuentra en un archivo que coincide con el glob, leído como file lee uno |
Dónde se encuentra
| Comprobación | Mantiene el nodo cuando |
|---|---|
{"file": "*Repository.php"} | su ruta desde el proyecto (el repositorio en el que se encuentra, si no, la carpeta juzgada), o cualquier parte de ella, coincide con el glob: * permanece dentro de una carpeta y ** las cruza, como en .gitignore |
| `{"fileMatches": "(^ | /)pages/.*(Create |
{"namespaceLike": "App\\Http\\*"} | el espacio de nombres, paquete o módulo en el que está declarado coincide con el glob |
{"layer": "App\\Domain"} | se encuentra en esa capa de la pila que el proyecto declara (backend, Python, C#) |
{"topLevel": true} | se encuentra fuera de toda función, clausuras también, y de todo tipo — código que se ejecuta cuando su archivo se carga |
{"withinLoop": true} | se encuentra dentro de un bucle, o no con false |
{"position": "first"} | es el first, last o only de sus hermanos (uno único también es primero y último) |
{"field": "cond"} | llena esa ranura de su padre en el árbol — el nombre propio del lenguaje para la ranura, como cond, body, else, args — que es el único eje de lugar que las otras comprobaciones nunca preguntan: una llamada que actúa COMO condición en lugar de una pasada como argumento. Los nombres son los de su lenguaje, por lo que una regla escrita con uno juzga ese lenguaje |
{"role": "member"} | es ese tipo de cosa, en las palabras que escribe cada puente: statement, expression, member (cualquier declaración de un tipo, una función, una propiedad o un caso de enum), type (un tipo como se escribe), pattern, markup u other. Es cómo una regla dice una declaración sin nombrar un tipo por lenguaje, que es lo que necesita una regla sobre el orden o la compañía de los miembros |
{"descendant": {"is": "return"}} | algún nodo dentro de él pasa el paso; con "within": "ownBody" solo se recorren los nodos que él MISMO contiene, deteniéndose en una función anidada dentro de él, que es la diferencia entre un método que guarda y uno que entrega una clausura que contiene la guardada a una cola |
{"child": {"is": "return"}} | algún nodo un nivel dentro de él pasa el paso, lo que descendant a cualquier profundidad no puede decir: una función cuyo propio cuerpo retorna, y nada para una que retorna solo dentro de un if. Con "field" solo se recorren los hijos que llenan ese campo del árbol, como el campo de un recuento reduce lo que cuenta |
{"body": {"is": "branch"}} | alguna declaración de su CUERPO pasa el paso, leída a través de lo que su lenguaje envuelve un cuerpo: C# escribe una declaración de bloque y Python escribe declaraciones simples, por lo que child las responde de manera diferente y esto las responde igual. Es cómo una regla sobre el cuerpo de un bucle o una función se escribe una vez para cada lenguaje |
{"inside": {"is": "catch"}} | algún nodo por encima de él, hasta la raíz del archivo, pasa el paso |
{"all": [{"name": "store"}, {"hasModifier": "pu |
…
[truncado para traducción]