Gabriel / Ramos

⟨ pintor de pixel ⟩

  • Blog
  • Escrevendo suas próprias regras lint

Escrevendo suas próprias regras lint

O que são Árvores de Sintaxe Abstrata e como utilizá-las para criar regras de validações específicas para o seu projeto usando ESLint

8 min. de leitura

Foto por amirali mirhashemian

Já parou pra pensar como ferramentas como o ESLint funcionam? Como a análise estática do seu código é realizada. Já teve vontade de criar seu plugin e não sabe como? Então, bora lá, ao fim do processo teremos um exemplo de plugin pronto para ser utilizado.

Antes de começar a desenvolver, precisamos entender um conceito bem importante e vai servir de base pra criar uma regra customizada de lint (como está nesse exemplo).

#Árvore de Sintaxe Abstrata (AST, ou abstract syntax tree)

É praticamente a fundação de como as ferramentas como o ESLint (ou o Babel) avaliam e processam um código.

Como o próprio nome entrega, é uma estrutura de dado de árvore e é o resultado do "parseamento" do código escrito estaticamente, contendo todas as declarações, funções, imports e estruturas presentes em um trecho de código.

Para nos ajudar a entender e criar nossas regras de Lint, vamos utilizar o AST Explorer. É uma ferramenta totalmente gratuita (é só acessar e usar) que gera pra gente uma AST baseado em qualquer código que a gente escreva.

Para entendermos um pouco melhor e termos ideia de como funciona, podemos imaginar que, esse trecho de código em JS:

const name = 'gabriel';

Gera a seguinte AST (convertida para JSON):

{
  "type": "Program",
  "start": 0,
  "end": 23,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 23,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 6,
          "end": 22,
          "id": {
            "type": "Identifier",
            "start": 6,
            "end": 10,
            "name": "name"
          },
          "init": {
            "type": "Literal",
            "start": 13,
            "end": 22,
            "value": "gabriel",
            "raw": "'gabriel'"
          }
        }
      ],
      "kind": "const"
    }
  ],
  "sourceType": "module"
}

Pode assustar à primeira vista. Mas, olhando com um pouco mais de calma podemos analisar como essa árvore foi criada e que existem várias informações nessa estrutura que servirão como base para criar as regras do nosso plugin. Vamos dar uma lida nelas:

  • nosso exemplo possui um tipo (type Program) e cada declaração interna também: a declaração da nossa variável (type VariableDeclarator), com um nome (type Identifier) e um valor inserido (type Literal);
  • possuímos o corpo do nosso programa (body) como um array de instruções, cada um dos objetos dentro é uma das instruções de nosso sistema;
  • temos o início e o fim das declarações (start e end);
  • podemos verificar o nome (name) e os valores (value) de cada declaração;
  • temos, inclusive, o tipo (kind) da nossa variável (que foi declarada como "const").

Com isso, podemos prosseguir pra criação e instalação dos pacotes necessários do nosso plugin.

#Preparando o ambiente e definição do módulo

Não tem muito segredo na hora de preparar o ambiente pra criar um plugin ou uma regra de ESLint. Entretanto, se você quiser (como a própria documentação sugere), você pode instalar o Yeoman e o gerador do ESLint, da seguinte forma:

npm i -g yo generator-eslint

Ao executar isso no seu terminal ambos os pacotes já estarão instalados e, para criar um plugin seguindo o padrão definido pelo ESLint, basta executar:

yo eslint:plugin

Ao executar esse comando, algumas perguntas aparecerão no seu terminal e, ao fim do processo, você terá uma estrutura base para iniciar o desenvolvimento das regras do seu plugin.

Nessa hora vale ter em mente algumas pequenas restrições necessárias para criar seu plugin. De acordo com a documentação do ESLint, os plugins devem seguir um padrão:

  • Devem ser um pacote do NPM;
  • Devem começar o nome com eslint-plugin-<nome>;
  • Podem conter um prefixo de escopo como @<escopo>/eslint-plugin-<nome>.

Caso precise realizar algum juste de nomenclatura (como adicionar um escopo ao seu plugin), a melhor hora é agora.

Podemos configurar o AST Explorer para simular um ambiente com ESLint em execução. Ao acessar o site, basta clicar na aba transform e selecionar a opção ESLint V4.

Feito isso, algumas abas novas aparecerão na parte de baixo da tela. Uma para declararmos nossa regras customizadas e a outra com a saída de erros do ESLint.

#Criando as regras

Existem diversas opções para criar uma regra. Vamos imaginar que criaremos uma regra de lint que irá disparar um erro ao encontrar uma declaração de variável com var.

Ou seja, esse trecho de código:

var name = 'gabriel';

Deveria receber uma mensagem como Você não deveria declarar variáveis com var. Utilize const ou let.

Precisaremos exportar, do nosso pacote, um objeto com uma chave rules contendo a definição de todas as nossas regras. Algo como:

module.exports = {
  rules: {
    // aqui vão nossas regras
  }
};

Podemos dar qualquer nome pra nossa regra (e é esse nome que será usado ao inserir o plugin nas configurações em um eslintrc). Vamos dar o nome de sem-var.

module.exports = {
  rules: {
    'sem-var': {
      // código da regra
    }
  }
};

Para que as regras sejam executadas corretamente, elas precisam possuir um método create, que será executado pelo ESLint ao carregar nosso plugin. Esse método recebe como parâmetro um objeto chamado context. Com isso, uma regra deve ficar mais ou menos da seguinte forma:

module.exports = {
  rules: {
    'sem-var': {
      create: context => {}
    }
  }
};

Daqui a pouco veremos como utilizar esse context. Agora, precisamos definir como retorno dessa função create um objeto que irá conter as chaves dos tipos de cada um dos nós da AST de um código JS.

Em outras palavras, como queremos verificar as declarações de variáveis, podemos atrelar uma função a uma chave VariableDeclaration (que é o type que encontramos na nossa AST).

Para fazer isso, basta retornarmos essa chave em um objeto do método create. Algo como:

module.exports = {
  rules: {
    'sem-var': {
      // agora retornamos um objeto
      create: context => ({
        // atribuímos uma função
        // para a chave VariableDeclaration
        // que receberá como parâmetro cada um dos nós desse tipo
        VariableDeclaration: node => {
          // definição da regra
        }
      })
    }
  }
};

Com isso, já temos uma função que será disparada para cada nó de uma AST. Agora chegou a hora de entendermos a utilização do parâmetro context da função atribuída ao create.

Esse objeto context possui uma série de funcionalidades, a que nos interessa, nesse momento, é a funcionalidade responsável por disparar as mensagens de erro da nossa regra de lint.

Essa funcionalidade é a report. Ou seja, para indicarmos uma erro em nossa regra, basta executarmos a função report e informar o nó que deve ser corrigido e uma mensagem de erro. Mais ou menos assim:

context.report({
  node,
  message: 'Você não deveria declarar variáveis com var. Utilize const ou let.'
});

Com isso em mente, podemos criar a validação dentro de nossa regra e já disparar o método report ao encontrarmos alguma declaração que use var. Para validarmos o tipo de uma variável, podemos verificar se o nó possui a propriedade kind e se essa propriedade é igual a var. Dessa forma:

module.exports = {
  rules: {
    'sem-var': {
      create: context => ({
        VariableDeclaration: node => {
          // verificamos se existe "kind"
          // e se é igual a "var"
          const estaDeclarandoComVar = node.kind && node.kind === 'var';

          // caso seja, reportamos o erro
          if (estaDeclarandoComVar) {
            context.report({
              node,
              message:
                'Você não deveria declarar variáveis com var. Utilize const ou let.'
            });
          }
        }
      })
    }
  }
};

Da parte da regra, é isso! Agora, precisamos também garantir que nossa regra está disparando os erros como o esperado.

#Testando nossa regra

O ESLint disponibiliza um utilitário chamado RuleTester, justamente para auxiliar na tarefa de testar os cenários válidos e inválidos das nossas regras.

Para iniciarmos, vamos criar um arquivo e criar uma instância desse RuleTester:

// arquivo teste.js
const { RuleTester } = require('eslint');
const tester = new RuleTester();

Caso seja necessário, você pode passar configurações extras ao instanciar o RuleTester para realizar qualquer configuração extra do ESLint.

Para executar algum deste, basta executarmos método run dessa instância criada, passando 3 parâmetros:

  • uma string nome do teste/regra;
  • a referência regra em si;
  • um objeto contendo as chaves valid e invalid que são arrays com amostras de código válido e inválido (com as mensagens de erro esperadas).
const { RuleTester } = require('eslint');
const tester = new RuleTester();

const regra = require('../rules/sem-var');

tester.run('Sem var', regra, {
  valid: [
    {
      code: `const nome = 'gabriel';`
    }
  ],
  invalid: [
    {
      code: `var nome = 'gabriel';`,
      errors: [
        {
          message:
            'Você não deveria declarar variáveis com var. Utilize const ou let.'
        }
      ]
    }
  ]
});

Com isso, basta executarmos esse arquivo:

node ./teste.js

E nosso teste passará com uma mensagem de sucesso no terminal! ✨

Brinque um pouco com os testes para ver as mensagens funcionando corretamente! Vale a pena se acostumar com essa ferramenta.

Caso você não queira ficar escrevendo trechos de código diretamente em uma string, você também pode utilizar outras ferramentas do Node (como o módulo fs), para criar alguma abstração que leia o conteúdo de algum arquivo JS e evitar ficar manipulando uma string diretamente.

#Aplicando o plugin no seu projeto

Agora que terminou o desenvolvimento, é só inserir o plugin no seu projeto. Você pode publicar o pacote e instalar onde quiser ou utilizar o npm link para testar localmente se tudo está funcionando como o esperado.

Para isso, lembre-se de ajustar seu arquivo de configuração do eslint, inserindo seu plugin:

{
  "plugins": [
    "meu-plugin
  ]
}

E aplicando a nova regra ao projeto:

{
  "rules": {
    "meu-plugin/sem-var": "error"
  }
}

#É isso aí!

Já tinha criado seu plugin ou regra customizada do ESLint? Já tinha estudado ou ouvido falar sobre o termo AST?

Espero que tenha curti e entendido um pouco como essas ferramentas funcionam. Acho que que criar uma regra de Lint customizada agora será uma tarefa mais fácil para você!


Compartilhe