Implementando uma regra de lint do Redocly CLI
Transforme uma verificação escrita em linguagem natural ("todo schema de array deve declarar items") em uma regra de lint do Redocly que funcione e seja testada.
A escada. O Redocly oferece três maneiras de realizar uma verificação. A primeira é a mais barata de manter.
- Regra nativa — uma das regras nativas realiza a verificação.
- Regra configurável — uma ou mais entradas
rule/*noredocly.yamlrealizam a verificação com asserções. - Plugin personalizado — código JavaScript que implementa sua própria regra com o padrão de visitante. Use este degrau por último.
Faça as verificações uma de cada vez. Comece cada verificação no degrau 1. Desça um degrau somente quando o degrau atual não puder realizar a verificação. Use toda a sintaxe da documentação oficial ou do livro de receitas. Confie na documentação, não na sua memória. Use sintaxe moderna, como ESM em vez de CommonJS. Se o projeto tiver seu próprio estilo, siga o projeto.
1. Triagem
Para cada verificação, anote três coisas: o nó que ela examina, o que deve ser verdadeiro e a aparência de uma violação. Faça a triagem de cada verificação individualmente. Verificações diferentes podem cair em degraus diferentes.
Obtenha os nomes dos nós do comando inspect-node-types, não da memória.
A árvore de tipos é diferente em cada especificação e cada versão.
Os nomes que ele imprime são o subject.type de uma regra configurável e as chaves do visitante de um plugin.
| para aprender | execute |
|---|---|
| o tipo em um local | --pointer='#/paths/~1orders' |
| cada nó, ou cada nó de um tipo | sem sinalizador, ou --type=Schema |
| quais tipos a descrição usa | --summary |
| onde um tipo pode ficar | --type=<Type> --parents |
| os ancestrais de um nó | --pointer=<pointer> --parents |
As duas últimas linhas dão à regra seu escopo.
Quando um tipo fica abaixo de mais de um pai, a regra precisa de uma cláusula where.
Sem essa cláusula, a regra também reporta os ramos que você não deseja.
O comando mostra a descrição diante de você. Ele não mostra tudo que a especificação permite.
Quando o comando não imprime um ramo, esse ramo é não comprovado. Não é ausente.
O comando não encurta o passo 3. Ele fornece nomes de tipos. Ele não diz se suas asserções expressam a verificação.
Percorra a escada para cada verificação:
- Pesquise a lista de regras nativas pelo conceito.
Uma barra final em um caminho dá
no-path-trailing-slash. Quando a configuração já estende um conjunto de regras que ativa a regra nativa, informe o usuário. Não adicione a regra novamente. Use a regra nativa quando sua descrição corresponder à verificação, mesmo que ela reporte mais do que a regra que você substitui. Uma regra nativa geralmente é uma versão melhor da mesma ideia, portanto suas descobertas extras são a verificação feita corretamente. Quando você escreve o comportamento mais restrito manualmente, troca uma regra mantida por código que você precisa manter. Desça um degrau somente quando a regra nativa verificar algo diferente. Informe qual regra nativa você usou e o que ela adiciona. - Certifique-se de que a regra nativa suporta as versões da especificação que o projeto usa para lint.
Algumas regras nativas suportam apenas uma versão.
no-invalid-media-type-examplessuporta OpenAPI 3.x, portanto deixa uma verificação para Swagger 2.0 sem ser aplicada. O Redocly imprimeUnused rules found in redocly.yamlquando uma regra ativada não pode ser aplicada a um documento. Corrija essa linha. Não é ruído. - Tente asserções e uma cláusula
where. Veja "Regras configuráveis" abaixo. Uma verificação pode precisar de várias entradasrule/*, uma para cada caso ou local. Isso ainda é melhor que um plugin. - Escreva um plugin somente quando as regras configuráveis não puderem expressar a verificação, mesmo em combinação. Primeiro trabalhe com os três dialetos abaixo. Um plugin pode fazer qualquer verificação, mas é código. Adiciona complexidade, manutenção e seus próprios bugs.
- Diga ao usuário para abrir uma issue somente quando nenhuma regra configurável puder preencher uma lacuna real.
A triagem está completa quando cada verificação tem um degrau. Forneça uma solução para cada verificação: o degrau mais alto que funciona. Informe apenas o que o usuário deve resolver: uma incompatibilidade entre uma descrição e uma implementação, uma regra não comprovada ou uma decisão que poderia ser diferente. Informe no chat. Nunca escreva um arquivo de resumo. A configuração registra o resto, portanto não narre o degrau de cada verificação.
Migrar de outro linter
- Migre o conjunto de regras efetivo, não apenas as regras escritas no arquivo.
Um
extends, ou uma base herdada, ativa mais regras com suas severidades padrão. Cada uma dessas regras também é uma verificação. Resolva essa base e conte as regras antes de começar. Assim você saberá o tamanho real do trabalho. - Considere cada verificação de dois lugares: a descrição da regra e sua implementação. Nunca considere a partir de sua memória do nome da regra. Quando os dois discordarem, use a descrição. A descrição diz o que o autor queria, e a implementação pode conter um bug. Informe o usuário sobre a diferença.
- Pule uma regra com severidade
off. Não a migre, nem mesmo como uma entrada desabilitada. Quando as regras ativadas estiverem prontas, liste cada regra pulada em uma linha. Então o usuário pode decidir. - Para Spectral, leia primeiro a tabela de mapeamento no guia de migração. Muitas regras principais do Spectral mapeiam para uma regra nativa. Uma célula vazia significa que você desce na escada.
- Quando você copia uma função da origem, também copia seus bugs para um hospedeiro mais restrito.
O Redocly interrompe o documento inteiro quando uma regra lança uma exceção, mas o linter de origem pode interromper apenas uma regra.
Uma correspondência sem proteção, como
v.match(/…/)[0]em um valor que pode falhar na correspondência, remove todas as descobertas nesse arquivo. Adicione uma proteção ao código que você copia. Em seguida, faça lint de um documento que contenha a forma problemática.
2. Configurar
Escreva no redocly.yaml do usuário. Use exatamente esse nome, sem ponto inicial.
Quando o arquivo existir, leia-o primeiro e mantenha o extends e as regras existentes.
Defina cada regra como error, a menos que o usuário solicite outro nível.
Ao migrar, mantenha a severidade da origem e escolha o nível do Redocly mais próximo.
Existem apenas error, que falha a execução, warn e off, portanto info e hint tornam-se warn.
Referencie todos os arquivos que você criar a partir da configuração e os teste na execução de lint. Não deixe arquivos mortos.
Uma configuração sem extends não herda nada — nem recommended, nem mesmo struct.
A base padrão se aplica somente quando nenhum arquivo de configuração existe.
Portanto, um redocly.yaml executa exatamente as regras que nomeia.
O degrau de regra nativa existe somente quando extends nomeia um conjunto de regras, ou quando a configuração lista cada regra nativa por conta própria.
Com nenhum dos dois, um documento que viola a especificação três vezes lê limpo.
Conecte todas as regras.
Um plugin em plugins: não faz nada quando suas regras estão ausentes de rules:, e o Redocly não dá aviso.
Depois de escrever a configuração, liste os ids de regra que devem estar ativos.
Confirme que cada id está em rules: ou em um bloco com escopo de especificação.
Quando todas as regras estiverem prontas, compare sua lista com os conjuntos de regras predefinidos: minimal, recommended, recommended-strict e spec.
- Quando suas regras estiverem próximas de um conjunto de regras, estenda-o. Mantenha apenas as diferenças como substituições: outras severidades, regras para desativar e verificações que o conjunto de regras não contém.
- Um conjunto de regras também ativa regras que o usuário não solicitou.
Leia o que ele adiciona e substitua cada regra que contradiz ou repete uma regra que você escreveu.
Um conjunto de regras que ativa uma verificação que a origem define como
offé um erro a ser corrigido, não um benefício. - Quando as substituições são tantas quanto as regras, ou mais, mantenha a lista simples e sem
extends.
3. Verificar
Uma regra é não comprovada até que uma execução ao vivo a faça reportar.
Comprove cada regra com npx redocly lint <arquivo-api> em duas direções:
- um fixture violador: a regra reporta, no local correto;
- um fixture conforme: a execução permanece silenciosa.
Um falso positivo é a falha comum, portanto uma regra que passou apenas no fixture violador é não comprovada. Escreva o fixture violador para separar esta regra da regra ao lado: a regra que você substitui, ou sua vizinha na configuração. Dois erros precisam dessa separação:
- Coloque um valor falso no fixture violador.
Uma regra que deve rejeitar uma
descriptionvazia deve reportar emdescription: ''. Quando você coloca''apenas no fixture conforme, você não comprova nada, porque uma asserção de presença o aceita. A regra então parece correta e a lacuna permanece. - Uma regra nativa que substitui duas verificações de origem precisa de um fixture que viole apenas a segunda verificação.
no-unsafe-markdownreporta<script>, mas nuncaeval(. Um fixture que contém ambas dá à regra nativa crédito por uma verificação que ela não faz.
Para manter a saída limpa, passe uma pequena configuração temporária com --config.
Exclua essa configuração e os fixtures quando terminar.
Todo trecho que você entregar deve estar correto como escrito: ele analisa como YAML e se comporta como você afirma.
Você termina quando cada verificação nomeia um fixture violador que reportou e um fixture conforme que permaneceu silencioso.
Nomeie esses fixtures em seu relatório.
Um número não é evidência.
Uma execução limpa nos próprios arquivos do projeto também não é evidência: mostra apenas que nada reporta lá, e isso é verdade para uma regra correta e para uma regra morta.
Quando você verifica as regras arriscadas e assume as outras, a entrega é não comprovada. Diga isso, e nomeie cada regra não comprovada.
Um fixture conforme comprova apenas as formas dentro dele.
Uma versão da especificação que você não tentou, ou um local que --parents mostrou e você não testou, permanece não testado. Informe isso.
Quando uma execução ao vivo é impossível, marque a regra como não verificada. Não afirme que ela funciona.
Você também termina quando cada verificação solicitada está na configuração ou na lista pulada, e o projeto contém apenas os arquivos de configuração e plugin que você criou.
Regras configuráveis
Documentação: regras configuráveis. Exemplos práticos: cookbook/configurable-rules.
Uma regra é uma entrada sob rules: com um nome que começa com rule/:
rules:
rule/minha-verificacao:
subject:
type: Schema # um tipo de nó da lista de tipos da documentação
property: title # opcional
assertions:
defined: true
where: [] # cláusula opcional
message: Texto de problema personalizado.
severity: error
rules simples se aplica a todo documento que o projeto usa para lint. Mantenha regras lá por padrão.
Mova uma regra para uma chave com escopo de especificação somente quando a verificação precisar de algo que existe em uma versão da especificação, como nullable no OAS 3.0, ou quando o usuário solicitar o escopo.
As chaves são oas2Rules, oas3_0Rules, oas3_1Rules, oas3_2Rules, async2Rules, async3Rules, arazzo1Rules e overlay1Rules.
Uma regra dentro de uma chave não é executada para as outras versões, portanto uma verificação que o projeto precisa em duas versões precisa de uma entrada que ambas as versões alcancem.
Escopar uma regra de plugin dentro do próprio plugin, em seu mapa rules.
O que uma asserção testa
Dois fatos causam a maioria dos falsos positivos e a maioria das ausências silenciosas.
Um valor ausente passa.
Quase toda asserção é válida quando o valor que ela verifica está ausente.
const: array em property: type corresponde a um schema tipado array, e também a todo schema sem type.
Portanto, quando uma entrada where testa um valor de propriedade, adicione defined: true ao lado da asserção de valor se a propriedade puder estar ausente.
Presença não é valor.
defined, required, requireAny, disallowed e mutuallyExclusive testam se uma chave existe.
Eles não dizem nada sobre o conteúdo, portanto defined: true em description passa para description: ''.
Afirme o valor em vez disso: nonEmpty: true rejeita uma string vazia, e const, enum e pattern testam o conteúdo.
Este é o defeito mais frequente na prática.
Parece correto em revisão, e apenas um fixture violador que contenha '' o mostra.
Dois resultados desses fatos:
const: <valor>é também a maneira de dizer "este campo deve ser falso", porque a asserção é válida para esse valor e para um campo ausente.requireAnyconta apenas chaves. "Pelo menos um destes campos, e não vazio" não tem forma de asserção. Essa verificação é um caso real de plugin.
O que a asserção inspeciona
property: nome— o valor desse campo no nó sujeito.property: [nome, url, email]— cada campo listado individualmente. Todo campo que falha se torna seu próprio problema com seu próprio ponteiro JSON, como#/info/contact/name.propertyausente — as chaves do nó sujeito. Por exemplo,subject: { type: Paths }comassertions: { notPattern: /.+\/$/ }é executado nas próprias strings de caminho.
Aponte o sujeito, depois use a cláusula
Três dialetos apontam uma regra. Resolva-os nesta ordem.
1. Sujeito: o nó que possui o que você afirma.
- Uma verificação sobre as chaves que um mapa contém tem como alvo o próprio tipo do mapa.
Os mapas de respostas, cabeçalhos, propriedades de schema e caminhos são cada um seu próprio tipo.
Portanto "este mapa deve conter essa chave" é um
requiredem um sujeito. Não procure o pai. - Uma verificação sobre um valor tem como alvo o nó que contém o campo, mais
property. type: anycom umpropertyverifica essa propriedade onde quer que ela apareça.- Confirme o nome do tipo com
inspect-node-types, em um documento da versão da especificação que a regra tem como alvo. Um tipo nomeado pode existir em uma versão e não em outra.HeadersMapé um tipo no OpenAPI 3. No OpenAPI 2 o mapa deheadersda resposta é anônimo e não tem nome de tipo, portanto um sujeitoHeadersMapsoboas2Rulesnunca reporta. O Redocly avisa sobre um id de regra não resolvido, mas não sobre umsubject.typenão resolvido, portanto um tipo errado dá a você uma regra que nunca reporta.
2. Escopo: onde esse tipo aparece.
- Use
wherepara limitar o sujeito a locais específicos. "Um campotypedeve serstringquando o pai é um parâmetro de consulta" precisa desubject: { type: Schema, property: type }ewhere: [ { subject: { type: Parameter, property: in }, assertions: { const: query } } ]. Isto é: "para cada local onde um Schema aparece sob um Parameter comin: query, a asserção é executada no campotype". - Sem
where, a asserção é executada em toda aparição do sujeito.
3. Assertividade (opcional): o que fazer com o resultado.
- Omitir
assertionstransforma a regra em uma regra de rastreamento: ela gera uma descoberta para cada aparição do sujeito, sem verificar nada. Útil para auditoria. assertionsvazio ({}) não faz nada. Verifique a digitação.
Plugins
Documentação: plugins. Exemplos: cookbook/plugins.
Um plugin é um arquivo JavaScript que exporta um objeto id e um objeto rules.
import { oas2, oas3_0 } from '@redocly/openapi-core';
const myPlugin = {
id: 'meu-plugin',
rules: {
oas2: {
'caminho-sem-barra': () => ({
PathItem: {
enter(pathItem, { report, location }) {
if (pathItem.path.endsWith('/')) {
report({
message: 'Os caminhos não devem ter barra final.',
location,
});
}
},
},
}),
},
oas3_0: {
'caminho-sem-barra': () => ({
PathItem: {
enter(pathItem, { report, location }) {
if (pathItem.path.endsWith('/')) {
report({
message: 'Os caminhos não devem ter barra final.',
location,
});
}
},
},
}),
},
},
};
export default myPlugin;
Use visitantes enter ou leave. Cada um recebe o objeto e um contexto com report, location e type.
Para configurar um plugin:
plugins:
- './meu-plugin.js'
rules:
meu-plugin/caminho-sem-barra: error
Não se esqueça de mapear a regra. O plugin por si só não adiciona nada a rules:.
Casos que pedem um plugin
- Uma verificação que precisa da forma de um valor que não é um campo, como o padrão de uma chave de mapa.
Uma regra de padrão em
subject: { type: Paths }verifica as strings de caminho, não os nomes de parâmetro. Para verificar nomes de parâmetro, você precisa do contexto: o pai Parameter e seu camponame. - Uma verificação que deve rejeitar uma string vazia entre várias opções, como
requireAnycom conteúdo não vazio. - Uma verificação que usa
matchesou outras APIs de nó de Redocly que as asserções não expõem. - Uma verificação que precisa de uma condição dependente de estado, como "se o pai tiver
format: url, então isso deve ser uma URL".
Use as mesmas regras para escrever um plugin que você usa para escrever uma regra configurável:
- Afirme o valor, não a presença.
- Teste
''no fixture violador. - Teste ambas as direções (conforme e violador).
- Confirme os nomes dos tipos com
inspect-node-types. - Coloque a regra no bloco de versão da especificação correto.
Regras de plugin não se beneficiam da proteção de tipo. Um sujeito de plugin recebe cada nó desse tipo, mesmo em versões da especificação onde esse tipo não está definido. Sua primeira linha deve verificar se o objeto tem a forma que você espera:
PathItem: {
enter(pathItem, { report, location }) {
if (typeof pathItem !== 'object' || pathItem === null) return;
// ...
},
},
Listas e exemplos práticos
Toda regra inicia com severidade error a menos que a origem ou o usuário peça outro nível.
Use oas2Rules, oas3_0Rules, etc., para regras específicas da versão.
Coloque regras que funcionam em todas as versões em rules:.
Pre-fixo de plugin: a regra deve ser id-do-plugin/nome-da-regra.
Teste sempre: uma regra configurável é mais rápida de escrever e mais barata de manter do que um plugin.
Lembrete: --parents mostra onde um tipo fica. Para uma regra que deve ser executada apenas em schemas que estão em parâmetros de consulta, use:
subject:
type: Schema
property: type
where:
- subject:
type: Parameter
property: in
assertions:
const: query
Lembrete: um valor ausente passa em quase todas as asserções. Use defined: true para verificar presença.
Migração passo a passo
- Liste todas as regras efetivas (incluindo de
extendsherdados). - Para cada regra:
- Comece no degrau 1 (regra nativa). Encontre uma correspondência? Se sim, use-a.
- Degrau 2 (regra configurável). Ela pode expressar a verificação? Se sim, escreva-a.
- Degrau 3 (plugin). Escreva um plugin.
- Para cada regra migrada, escreva um fixture violador e um fixture conforme.
- Execute o lint em ambos. Ajuste a regra até que o violador reporte e o conforme fique em silêncio.
- Quando todas as regras estiverem prontas, verifique se você tem arquivos mortos (config ou plugin não referenciados).
- Informe: quais regras você usou, quais regras você pulou (com severidade
off), quais regras permanecem não comprovadas. - Não escreva um resumo em arquivo. Use o chat.
Fim.