Como criar e manter padrões de código em seus projetos Ruby com Rubocop
Se você desenvolve código Ruby, talvez esteja familiarizado com as boas práticas do Ruby Style Guide. Recomendo fortemente conferir caso não esteja. Existem softwares criados para nos ajudar a reforçar as regras desejadas dentro de nossos repositórios, estes são chamados de linters
.
Lint, ou um linter, é uma ferramenta de análise estática de código usada para sinalizar erros de programação, bugs, erros de estilo e construções suspeitas. O termo se origina de um utilitário Unix que examinou o código fonte da linguagem C.[1] - Traduzido do Wikipedia
Quando falamos de linters para Ruby, existe um que é amplamente usado e disseminado, o Rubocop. Daqui em diante eu irei contar um pouco sobre o ecossistema do Rubocop, como ele funciona, como utilizar em seus projetos e como contribuir.
Caso você já esteja familiarizado com o Rubocop
pode ir direto para "Como funciona?" ou "Como contribuir?"
Rubocop
O Rubocop
é um linter feito em Ruby
e para Ruby
.
Pontos interessantes do Rubocop
:
- Existe desde 2012.
- Documentação completa e de alta fidelidade.
- Comunidade ativa e receptiva.
- Possui pacotes de regras para o
Ruby on Rails
além das regras paraRuby
. - Expandiu a biblioteca padrão de
AST
para facilitar a criação de regras mais elaboradas.
Como utilizar o Rubocop?
Para utilizar o Rubocop
é bem simples e os comandos podem ser encontrados no repositório. Nada diferente para quem está acostumado a usar o Ruby Gems
e o Bundler
.
Config file
Você pode usar um arquivo .rubocop.yml
para personalizar as regras utilizadas ao fazer o lint do diretório onde o arquivos está localizado.
$ rubocop --init # gera um arquivo .rubocop.yml no diretório
Caso você queira adicionar o Rubocop
em um projeto existente que contenha muitas ofensas (violações do estilo do código) de acordo com as regras padrão do rubocop, utilize a opção abaixo:
$ rubocop --auto-gen-config
--auto-gen-config
irá gerar um arquivo de configuração contendo exceções para todas as ofensas contidas no projeto, assim você pode introduzir as regras ao projeto aos poucos sem precisar fazer grandes e demoradas mudanças na sua base de código.
$ rubocop [options] [file, file2, ...]
$ rubocop -l # executa apenas regras de lint (sem performance, segurança etc)
$ rubocop --safe # executa apenas regras seguras - considerada estáveis e determinísticas
$ rubocop -F # OU --fail-fast interrompe a execução na primeira ofensa
$ rubocop -a # OU --auto-correct corrige ofensas quando seguras
$ rubocop -A # OU --auto-correct-all corrige todas as ofensas mesmo não seguras
Como funciona?
O Rubocop
funciona através de regras chamadas de cops
. Cops
são representados por classes que funcionam de modo semelhante a um Service object. Cada classe é responsável por:
- Uma regra
- Parâmetros de configuração
- Mensagens de erros
AST & node matcher
AST
significa Abstract Syntax Tree
e é modo como o Rubocop
desconstrói o código para fazer buscas por padrões de sintaxe - de código Ruby
neste caso. Originalmente o Rubocop
utilizava diretamente a gem parser, mas com o tempo sentiram falta de várias ferramentas e criaram o Rubocop AST que extende a gem parser
.
Na documentação do parser você pode conferir os tipos de sintaxes que ele é capaz de identificar: de sintaxes básicas como atribuições e tipos até sintaxes mais elaboradas como argumentos ocultos de blocos. Já na documentação do Rubocop AST podemos ver como ele extende o parser e implementa ferramentas muito semelhantes às utilizadas em expressões regulares.
Com isso já podemos capturar grupos e identificar sintaxes independente da ordem e da quantidade de nódulos subsequentes. Essas ferramentas nos ajudam muito na hora de trabalharmos com regras que envolvem o uso de argumentos específicos nas chamadas de métodos - mais comum no Rubocop Rails
, mas também presente no Rubocop
.
De maneira bem objetiva, isso já cobre o funcionamento do Rubocop
que precisamos entender para podermos contribuir. Existem cops que procuram padrões de sintaxe Ruby
através do AST. Ao identificar estes padrões, os cops aplicam ofensas àquele pedaço de código. Não precisamos entender a arquitetura do projeto ou API do comando do terminal para podermos contribuir com novas regras que podem ser úteis para nós e para a comunidade.
Como contribuir?
3 dicas/avisos antes de começarmos a contribuir:
- Agora que sabemos como tudo funciona, vou deixar claro que existe uma curva de aprendizado entre entender o
AST
e saber de fato trabalhar com ele de maneira eficiente, assim como existe comRegex
. Então não se fruste, mas também não pense que será rápido criar um novocop
dependendo do que vocês está buscando fazer. - Cave o projeto e procure por exemplos de uso destas regras em outros cops para ter ideias de como utilizar. Não pare no primeiro cop, olhe o máximo de cops que puder. É um projeto open source e seu primeiro exemplo provavelmente não será o melhor exemplo.
- O projeto possui testes feitos em rspec para todos os cops e contribuir para ele quer dizer que você será responsável por testar seu código também. Esteja familiarizado ou leve em consideração o tempo de aprender a utilizar.
Contribuindo para os cops
Vamos tentar fazer uma mudança simples, mas tentando entender como as coisas funcionam no caminho.
Uma convenção do Ruby Style Guide
é a Nomeação de predicados. Essa regra nos diz que devemos evitar certos prefixos para métodos que retornam booleanas e força a utilização da interrogação (?) no final do nome, como vemos nos exemplos abaixo:
# bad
def is_even(value)
end
def is_even?(value)
end
# good
def even?(value)
end
# bad
def has_value
end
def has_value?
end
# good
def value?
end
# Tirado da documentação em Abril de 2022
Os prefixos proibidos por padrão são is_, has_, have_
e vamos supor que temos visto muitos casos de should_
e queremos propor essa mudança para o Rubocop
ao invés de configurar em todos os projetos que constribuímos.
A regra na doc oficial se chama Naming/PredicateName
e uma rápida busca no projeto nos mostra o seguinte cop existente:
rubocop/lib/rubocop/cop/naming/predicate_name.rb
# frozen_string_literal: true
module RuboCop
module Cop
module Naming
class PredicateName < Base
include AllowedMethods
# @!method dynamic_method_define(node)
def_node_matcher :dynamic_method_define, <<~PATTERN
(send nil? #method_definition_macros
(sym $_)
...)
PATTERN
def on_send(node)
dynamic_method_define(node) do |method_name|
predicate_prefixes.each do |prefix|
next if allowed_method_name?(method_name.to_s, prefix)
add_offense(
node.first_argument.loc.expression,
message: message(method_name, expected_name(method_name.to_s, prefix))
)
end
end
end
def on_def(node)
predicate_prefixes.each do |prefix|
method_name = node.method_name.to_s
next if allowed_method_name?(method_name, prefix)
add_offense(
node.loc.name,
message: message(method_name, expected_name(method_name, prefix))
)
end
end
alias on_defs on_def
private
def allowed_method_name?(method_name, prefix)
!(method_name.start_with?(prefix) && # cheap check to avoid allocating Regexp
method_name.match?(/^#{prefix}[^0-9]/)) ||
method_name == expected_name(method_name, prefix) ||
method_name.end_with?('=') ||
allowed_method?(method_name)
end
def expected_name(method_name, prefix)
new_name = if forbidden_prefixes.include?(prefix)
method_name.sub(prefix, '')
else
method_name.dup
end
new_name << '?' unless method_name.end_with?('?')
new_name
end
def message(method_name, new_name)
"Rename `#{method_name}` to `#{new_name}`."
end
def forbidden_prefixes
cop_config['ForbiddenPrefixes']
end
def predicate_prefixes
cop_config['NamePrefix']
end
def method_definition_macros(macro_name)
cop_config['MethodDefinitionMacros'].include?(macro_name.to_s)
end
end
end
end
end
Vamos quebrar o que está acontecendo aqui:
- A classe herda
Base
e incluiAllowedMethods
Base
possui diversos métodos compartilhados entre todos os cops. Poderia passar despercebido, mas olhe bem: a documentação no cabeçalho doBase
nos conta como usar essa classe para criar cops. Como eu disse antes, esse projeto é muito bem documentado 😉AllowedMethods
parace nos passar regras sobre métodos que são sempre permitidos e não serão analisados por esse cop
2. Chamada do método def_node_matcher
recebendo 2 parâmetros: um symbol :dynamic_method_define
e um padrão AST
declarado em um heredoc
- A documentação do método nos diz que este symbol é o nome do método
node_matcher
que está sendo definido. - Pelo nome
dynamic_method_define
podemos imaginar que ele é usado para encontrar métodos sendo definidos dinamicamente através dedefine_singleton_method
oudefine_method
no Ruby.
3. Se você leu a documentação do Base
sabe o motivo de termos os métodos on_def
e on_send
neste cop: executar as regras do cop durante os nós (nodes) com ocorrências de definição de métodos.
- Vamos analisar essas regras...
Agora sabemos que os prefixos que precisamos alterar estão no método predicate_prefixes
, mas ele possui apenas o seguinte código:
def predicate_prefixes
cop_config['NamePrefix']
end
Buscando NamePrefix
no projeto encontramos uma ocorrência em rubocop/config/default.yml
. Ou seja, configurações padrões! Então para adicionar um novo prefixo em:
ForbiddenPrefixes:
- is_
- has_
- have_
- should_
Agora falta só abrir o Pull Request
Existe um guia claro de como contribuir, que pode mudar a partir da data que escrevi este artigo, mas falando de maneira genérica, devemos sempre:
- Abrir PRs com um objetivo único e claro
- Garantir que adicionamos testes e os existentes continuem passando
- Rodar o
bundle exec rubocop
para fazer lint do próprio código - Adicionar as devidas alterações na documentação
- Adicionar seu nick do github entre os contribuídores e atualizar o changelog
- Fazer squad dos commits
Este é o template para novos PRs em Abril de 2022:
Before submitting the PR make sure the following are checked:
* [ ] The PR relates to *only* one subject with a clear title and description in grammatically correct, complete sentences.
* [ ] Wrote [good commit messages][1].
* [ ] Commit message starts with `[Fix #issue-number]` (if the related issue exists).
* [ ] Feature branch is up-to-date with `master` (if not - rebase it).
* [ ] Squashed related commits together.
* [ ] Added tests.
* [ ] Ran `bundle exec rake default`. It executes all tests and runs RuboCop on its own code.
* [ ] Added an entry (file) to the [changelog folder](https://github.com/rubocop/rubocop/blob/master/changelog/) named `{change_type}_{change_description}.md` if the new code introduces user-observable changes. See [changelog entry format](https://github.com/rubocop/rubocop/blob/master/CONTRIBUTING.md#changelog-entry-format) for details.
[1]: https://chris.beams.io/posts/git-commit/
Repassando
Aprendemos:
- Como encontrar cops e o funcionamento básico deles
- Como cops estão estruturados e o que herdam
- A documentação do projeto acontece dentro do próprio código
- A existência de arquvios de configuração
- Vimos um exemplo básico de padrão
AST
que podemos estudar mais a fundo 😉
Agora você pode usar o Rubocop
para garantir a saúde e fácil compreensão de seus projetos Ruby, além de propor novas regras ou alterações. Lembre-se que o Rubocop
é um projeto de código aberto e isso quer dizer que as decisões tomadas pelo projeto irão sempre favorecer mudanças que sejam positivas para a maioria dos usuários e não para cada caso, mas algo que afeta você pode ser exatamente algo que afeta muitos outros usuários também. Um bom jeito de começar pode ser ajudando a resolver alguma issue aberta.