Escrevendo seus próprios mandamentos
As regras fornecidas não são o teto. Uma convenção que você repete em revisões é uma regra que você ainda não escreveu. Escreva-a uma vez, e ela julgará cada arquivo a partir de então.
Uma regra é uma pergunta que o motor responde
Uma regra própria são dados que o binário executa — um arquivo .json em .commandments/custom/, nessa pasta ou em qualquer pasta abaixo dela, para que um projeto com regras para várias partes do código as agrupe pelo que protegem (custom/core/, custom/viewer/) — e ela faz a mesma pergunta que toda regra fornecida faz. Uma regra é nomeada pelo seu ARQUIVO onde quer que esteja, então mover uma para uma pasta mantém todas as menções a ela na configuração intactas, e dois arquivos com o mesmo nome são ambos recusados em vez de um vencer silenciosamente. Ela nomeia o motor que julga, o pecado que encontra, e uma consulta: um seletor que a abre, então etapas where que mantêm um nó e etapas reject que descartam um. Cada etapa faz exatamente uma verificação; uma regra é tão boa quanto a pergunta que cada etapa faz.
{
"engine": "php",
"sin": {
"name": "raw-sql",
"description": "SQL escrito inline no local da chamada.",
"rule": "Consultas passam pelo repositório que possui a tabela.",
"skill": "no-raw-sql"
},
"find": {
"select": "call",
"where": [
{"resolves": "Illuminate\\Support\\Facades\\DB", "of": "child:class"},
{"nameIn": ["select", "statement", "unprepared"]}
],
"reject": [
{"file": "*Repository.php"}
]
}
}
O seletor é um tipo neutro, que se lê igual em toda linguagem — 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>, um tipo de nó próprio da linguagem (kind:Expr_MethodCall) quando nenhum tipo neutro o descreve.
Uma regra é construída a partir de outras regras. Uma verificação que você já escreveu uma vez é nomeada, nunca escrita novamente: {"rule": "ActionClass"} passa quando a regra com esse nome também encontra o nó, e um find que escreve "from": "ActionClass" no lugar de "select" restringe o que aquela regra encontrou, sendo uma regra lida dentro de outra. Ambas alcançam qualquer regra do mesmo motor em .commandments/custom/, e cada parte continua sendo uma regra própria, listada e executada em seus próprios termos. Um nome que nada declara, uma regra de outro motor e uma cadeia que volta a si mesma são recusados quando as regras são lidas, antes que qualquer julgamento aconteça.
Diga either e not sem uma segunda regra. {"any": [{"is": "loop"}, {"is": "branch"}]} passa quando uma de suas etapas passa, {"all": [...]} quando todas passam, e {"not": {...}} quando sua etapa não passa — que é como um where diz o que apenas um reject poderia.
A verificação única de uma etapa — todas elas, geradas a partir da própria ferramenta:
O que é e como é chamado
| Verificação | Mantém o nó quando |
|---|---|
{"is": "loop"} | ele responde ao tipo neutro |
{"kind": "Expr_StaticCall"} | é o tipo próprio da linguagem, escrito como a linguagem escreve ou como o vocabulário soletra: Expr_StaticCall e static-call são uma mesma construção. As grafias são derivadas dos tipos, então a própria lista de autocompletar do editor mostra o que uma linguagem realmente emite, e uma grafia que dois tipos compartilham é a grafia de nenhum deles |
{"name": "select"} | seu nome — o nome de uma chamada ou acesso é o filho que o nomeia — é igual a ele |
{"nameIn": ["select", "statement"]} | seu nome é um deles |
{"nameLike": "get*"} | seu nome corresponde ao glob, * qualquer sequência de caracteres e ? um caractere |
| `{"nameMatches": "^(get | set)[A-Z]"}` |
{"nameMood": "question"} | seu nome está nesse modo linguístico: bare-verb é o verbo e nada mais (holds, ships), third-person narra um verbo (entersTestMode), e question abre com uma palavra interrogativa (is, has, can, should). Ele lê o modo, nunca uma lista de palavras que uma regra escreve. É a única verificação aqui que julga um NOME, e responde apenas se o nome LÊ de uma certa maneira: o que torna isso um pecado é o modo contra o que a coisa FAZ, então combine-o com o que o membro responde ou se ele altera algo, como as regras fornecidas fazem |
{"nameCase": "snake"} | seu nome, sem o $ ou _ inicial, está escrito em camel, pascal, snake, upper ou kebab case |
{"hasModifier": "static"} | ele carrega o modificador |
{"hasFlag": "byRef"} | ele carrega a flag que a árvore da sua linguagem define |
{"operator": "*"} | seu operador é exatamente ele: * e ?? distinguem duas expressões binárias, += uma atribuição, ! uma unária, extends uma cláusula de herança |
{"operatorIn": ["*", "/"]} | seu operador é um deles |
O que ele diz
| Verificação | Mantém o nó quando |
|---|---|
{"text": "select * from users"} | o texto de um literal é exatamente ele |
{"textLike": "*where id*"} | o texto de um literal corresponde ao glob |
{"textMatches": "(?i)^select"} | o texto de um literal corresponde à expressão regular |
{"commentIs": "history"} | um comentário é uma das leituras calibradas de prosa: history narra por que o código foi ALTERADO em vez do que ele faz, strawman defende contra um erro que ninguém cometeria, e restates não diz nada que o código que documenta não diga. As leituras são do próprio pacote de prosa, medidas por seus testes em cada linguagem, e o conjunto é fechado: uma regra pede um julgamento que foi calibrado, nunca uma prosa que corresponda a palavras que ela mesma escreve. Perguntado de um comentário ("select": "comment") ele julga aquele comentário, e perguntado de código ele julga os comentários escritos para ele |
{"docReference": {"resolves": false}} | um comentário de documentação nomeia um código que FOI REMOVIDO: uma referência que faz (<see cref>, {@see}, @see, Sphinx :class:) não resolve para nada, e o projeto teria que ser o dono dela. Duas referências são deixadas de lado, que é toda a diferença entre isso e um scanner de links quebrados: uma que o compilador nunca poderia ter resolvido de qualquer maneira, e outra cujo dono é declarado fora da varredura, já que nada aqui pode dizer se aquele dono ainda possui o membro. A resolução é da própria ponte |
{"commentLike": "*TODO*"} | um comentário sobre ele, ou na sequência de comentários diretamente acima dele, corresponde ao glob — e, perguntado de um comentário em si ("select": "comment"), o que AQUELE comentário diz, para que uma chave leia um comentário independentemente de como uma regra o alcançou |
| `{"commentMatches": "(?i)\b(TODO | FIXME)\b"}` |
{"docTag": "deprecated"} | sua documentação carrega a tag: @tag em PHPDoc e JSDoc, um elemento XML em C#, um campo ou diretiva Sphinx, ou uma seção Google/NumPy em uma docstring Python |
{"documented": false} | ele carrega um comentário de documentação, ou não com false |
Ao que ele se refere
| Verificação | Mantém o nó quando |
|---|---|
{"resolves": "App\\Models\\User"} | ao que seu nome se refere, resolvido, é aquilo — ou, para um valor que não nomeia nada (uma variável, uma propriedade), a classe que ele contém: exatamente essa classe, não uma que a estende |
{"resolvesLike": "App\\Models\\*"} | ao que seu nome se refere, resolvido, corresponde ao glob, ou a classe que um valor contém: {"resolvesLike": "*Service", "of": "child:var"} é um receptor de qualquer *Service |
{"declaredBy": "App\\Models\\*"} | o membro que ele lê é declarado por uma classe que o glob nomeia: a classe que seu receptor contém, seguida por seus pais até que um declare aquele membro. É de quem este campo é, o que nenhum nome pode dizer, e está vazio — então a etapa não mantém nada — onde o receptor não tem tipo na varredura ou nenhuma declaração dele declara o membro |
{"answeredBy": false} | o tipo que declara o que ele lê já nomeia a pergunta que faz — um método desse tipo cujo corpo inteiro retorna a mesma coisa, lido de qualquer maneira — ou NÃO com false. É o que distingue use o nome que existe de dê um nome a ele, e uma pergunta feita exatamente pelo método que a nomeia nunca é um dos lugares que a fazem |
{"sameAs": {"of": "closest:loop", "target": "child:iter"}} | ele nomeia a mesma coisa que outro nó do mesmo arquivo, alcançado seguindo of a partir dele e depois target a partir dali — a única pergunta que uma regra não poderia fazer: um remove na mesma coleção que seu loop percorre, um argumento que é o campo do qual acabou de ser lido. Os dois devem ser escritos da mesma forma, senão lidos como a mesma árvore; nenhum nó no final do caminho não é aprovação |
{"declaredElsewhere": true} | o que ele nomeia é declarado em OUTRO ARQUIVO diferente daquele em que está, ou não é com false. Um campo definido de fora do arquivo que o declara contornou os métodos que sua classe escreveu para se modificar, que é a pergunta que uma regra sobre o próprio negócio de um tipo faz; não mantém nada onde nenhuma declaração na varredura declara o que ele nomeia, então uma regra também diz o dono |
{"type": "number"} | o tipo que a expressão tem, como sua linguagem o lê: o que um verificador lhe deu, o que sua forma diz, ou o tipo que sua declaração escreve — um primitivo entre eles, onde resolves responde apenas a uma classe |
{"typeLike": "Money*"} | o tipo que a expressão tem corresponde ao glob |
{"calls": {"name": "dd"}} | uma chamada própria — não uma dentro de uma função aninhada nela — passa na etapa |
{"argument": {"at": 0, "is": "literal"}} | o argumento naquela posição passa na etapa; um at negativo conta a partir do final, -1 o último, e nenhum lá não passa nada. Sem at nenhum, QUALQUER argumento que passe na etapa é suficiente, que é como uma regra pergunta sobre um passado por nome, cujo lugar não o identifica |
{"constructs": "Date*"} | uma construção, ou em qualquer outra coisa uma própria, cria uma instância de um tipo que o glob nomeia — em Python, uma chamada de uma classe |
{"unused": true} | nada se refere a ele: uma função ou tipo de fora de si mesmo, através dos nomes e chamadas que a varredura resolve e o grafo de chamadas de sua linguagem; um parâmetro, pelo seu nome lido em sua função; um campo ou propriedade, pelo que alcança sua declaração através do tipo que seu receptor contém, já que um campo não se refere a nenhum símbolo na maioria das linguagens. Um membro que a própria linguagem alcança pelo nome, e um que sobrescreve, não são isso: nada que o projeto escreve precisa nomeá-los |
{"referredFrom": {"is": "parameter"}} | algo que se refere a ele — uma chamada, um nome, um tipo escrito — passa na etapa. É calledFrom perguntando sobre a REFERÊNCIA em vez do arquivo em que está, que é o que distingue um lado de uma coisa do outro: uma classe de evento nomeada por um despacho e por nenhum parâmetro de ouvinte não é ouvida por ninguém, e {"not": {"referredFrom": ...}} diz exatamente isso |
{"calledFrom": "app/Http/**"} | algo que se refere a ele — uma chamada, na maioria das vezes — está em um arquivo que o glob corresponde, lido como file lê um |
Onde está
| Verificação | Mantém o nó quando |
|---|---|
{"file": "*Repository.php"} | seu caminho a partir do projeto (o repositório em que está, caso contrário a pasta julgada), ou qualquer parte final dele, corresponde ao glob: * permanece dentro de uma pasta e ** as cruza, como no .gitignore |
| `{"fileMatches": "(^ | /)pages/.*(Create |
{"namespaceLike": "App\\Http\\*"} | o namespace, pacote ou módulo em que é declarado corresponde ao glob |
{"layer": "App\\Domain"} | está naquela camada da pilha que o projeto declara (backend, Python, C#) |
{"topLevel": true} | está fora de toda função, closures também, e de todo tipo — código que executa quando seu arquivo é carregado |
{"withinLoop": true} | está dentro de um loop, ou não com false |
{"position": "first"} | é o first, last ou only entre seus irmãos (um único também é primeiro e último) |
{"field": "cond"} | preenche aquele slot de seu pai na árvore — o nome próprio da linguagem para o slot, como cond, body, else, args — que é o único eixo de lugar que as outras verificações nunca perguntam: uma chamada servindo COMO condição em vez de uma passada como argumento. Os nomes são os de sua linguagem, então uma regra escrita com um julga aquela linguagem |
{"role": "member"} | é aquele tipo de coisa, nas palavras que toda ponte escreve: statement, expression, member (qualquer declaração de um tipo, uma função, uma propriedade ou um caso de enum), type (um tipo como escrito), pattern, markup ou other. É como uma regra diz uma declaração sem nomear um tipo por linguagem, que é o que uma regra sobre a ordem ou a companhia de membros precisa |
{"descendant": {"is": "return"}} | algum nó dentro dele passa na etapa; com "within": "ownBody" apenas os nós que ele mesmo CONTÉM são percorridos, parando em uma função aninhada dentro dele, que é a diferença entre um método que salva e um que entrega um closure contendo a ação para uma fila |
{"child": {"is": "return"}} | algum nó um nível dentro dele passa na etapa, o que descendant em qualquer profundidade não pode dizer: uma função cujo próprio corpo retorna, e nada para uma que retorna apenas dentro de um if. Com "field" apenas os filhos que preenchem aquele campo da árvore são percorridos, assim como o campo de uma contagem restringe o que ela conta |
{"body": {"is": "branch"}} | alguma instrução do seu CORPO passa na etapa, lida através do que sua linguagem envolve em um corpo: C# escreve uma instrução de bloco e Python escreve instruções simples, então child as responde de forma diferente e isso as responde de forma igual. É como uma regra sobre um loop ou o corpo de uma função é escrita uma vez para cada linguagem |
{"inside": {"is": "catch"}} | algum nó acima dele, até a raiz do arquivo, passa na etapa |
| `{"all": [{"name": "store"}, {"hasModifier": "pu |
…
[truncado para tradução]