Como usar linters personalizados para impor convenções de arquitetura

1. Por que linters personalizados são essenciais para a arquitetura de software

Linters genéricos como ESLint, Pylint ou Checkstyle são excelentes para capturar erros de sintaxe, problemas de estilo e más práticas comuns de codificação. No entanto, eles são cegos para questões arquiteturais. Um linter genérico não sabe que sua camada de apresentação não deve importar diretamente classes da camada de infraestrutura, ou que módulos específicos não devem depender uns dos outros.

O custo de violações arquiteturais silenciosas é alto. Quando um desenvolvedor, por pressa ou desconhecimento, cria um acoplamento indevido entre camadas, essa decisão se propaga como dívida técnica invisível. Com o tempo, o sistema se torna rígido, difícil de testar e propenso a bugs em cascata. Linters personalizados transformam convenções de arquitetura em regras executáveis que são verificadas automaticamente durante o desenvolvimento, antes que o código chegue ao repositório.

2. Mapeando convenções arquiteturais para regras de lint

O primeiro passo é identificar quais padrões arquiteturais são violáveis e podem ser expressos como regras. As categorias mais comuns incluem:

  • Dependências proibidas: Exemplo — a camada de domínio não pode depender da camada de infraestrutura.
  • Imports incorretos: Exemplo — classes de um módulo "core" não podem importar classes de módulos "plugin".
  • Anotações ausentes: Exemplo — toda classe que expõe um endpoint REST deve ter @RestController.
  • Naming patterns: Exemplo — classes de serviço devem terminar com Service e repositórios com Repository.

A documentação da intenção é crucial. Cada regra deve vir acompanhada de uma explicação clara sobre qual decisão arquitetural ela protege e por que aquela convenção é importante. Isso facilita a revisão e a aceitação pelo time.

3. Ferramentas e frameworks para criar linters personalizados

A escolha da ferramenta depende da linguagem e da complexidade das regras. As principais abordagens são:

  • Linters baseados em AST (Abstract Syntax Tree): ESLint para JavaScript/TypeScript, Pylint para Python, Checkstyle para Java. Eles permitem criar regras personalizadas percorrendo a árvore sintática do código.
  • Ferramentas especializadas em arquitetura: ArchUnit para Java é um exemplo maduro, permitindo definir regras arquiteturais em testes unitários.
  • Motores de regras customizadas: Para cenários muito específicos, é possível construir um linter próprio usando parsers como ANTLR ou Bison.

Criar regras do zero oferece flexibilidade total, mas exige mais esforço de manutenção. Estender linters existentes é mais rápido e aproveita a infraestrutura de relatórios e integração já consolidada.

4. Exemplos práticos de regras arquiteturais personalizadas

Regra de camadas (Java com ArchUnit)

// Proibir que a camada de apresentação importe diretamente a camada de infraestrutura
@AnalyzeClasses(packages = "com.exemplo")
public class ArquiteturaTest {
    @Test
    public void camadaApresentacaoNaoDeveImportarInfraestrutura() {
        JavaClasses classes = new ClassFileImporter().importPackages("com.exemplo");
        ArchRule regra = classes()
            .that().resideInAPackage("..presentation..")
            .should().onlyDependOnClassesThat()
            .resideInAnyPackage("..presentation..", "..application..", "..domain..", "java..");
        regra.check(classes);
    }
}

Regra de dependência cíclica (TypeScript com ESLint)

// Impedir que módulos A e B referenciem um ao outro
module.exports = {
    rules: {
        "no-cross-import": {
            create(context) {
                return {
                    ImportDeclaration(node) {
                        if (node.source.value.includes("moduloA") &&
                            context.getFilename().includes("moduloB")) {
                            context.report({
                                node,
                                message: "Módulo B não pode importar módulo A (dependência cíclica proibida)"
                            });
                        }
                    }
                };
            }
        }
    }
};

Regra de anotação obrigatória (Java com Checkstyle personalizado)

// Exigir que toda classe em pacote "controller" tenha @RestController
public class RestControllerAnnotationCheck extends AbstractCheck {
    @Override
    public int[] getDefaultTokens() {
        return new int[]{TokenTypes.CLASS_DEF};
    }

    @Override
    public void visitToken(DetailAST ast) {
        if (ast.getText().endsWith("Controller")) {
            boolean temAnotacao = ast.findFirstToken(TokenTypes.ANNOTATION) != null;
            if (!temAnotacao) {
                log(ast, "Classes Controller devem ter @RestController");
            }
        }
    }
}

Regra de naming pattern (Python com Pylint)

# Garantir que classes de serviço terminem com Service
from pylint.checkers import BaseChecker

class ServiceNamingChecker(BaseChecker):
    name = 'service-naming'
    msgs = {
        'W9001': (
            'Nome da classe de serviço deve terminar com "Service"',
            'service-naming-error',
            'Classes no módulo services devem ter sufixo Service'
        )
    }

    def visit_classdef(self, node):
        if 'services' in node.file and not node.name.endswith('Service'):
            self.add_message('W9001', node=node)

5. Integrando linters personalizados no pipeline de CI/CD

Para que as regras sejam efetivas, elas precisam ser executadas automaticamente em momentos estratégicos:

  • Pré-commit hooks: Usando ferramentas como Husky (JavaScript) ou pre-commit (Python), as regras são verificadas antes do commit. Isso dá feedback imediato ao desenvolvedor.
  • Pull request checks: No CI/CD (GitHub Actions, GitLab CI, Jenkins), execute os linters personalizados como etapa obrigatória. Se uma regra falhar, o PR não pode ser mesclado.
  • Builds noturnos: Para regras mais pesadas ou análises completas do código, execute varreduras noturnas e gere relatórios.

A configuração de severidade é importante. Use erros para violações arquiteturais críticas (ex: dependência proibida entre camadas) e warnings para boas práticas (ex: nomenclatura recomendada). Mensagens de erro devem ser claras e acionáveis, apontando o arquivo, a linha e a convenção violada.

6. Estratégias para adoção e manutenção das regras

Adotar linters personalizados em um time exige cuidado para não gerar resistência:

  • Comece pequeno: Implemente de 3 a 5 regras críticas primeiro. Regras de dependência entre camadas e proibição de imports diretos são bons pontos de partida.
  • Governança clara: Defina quem cria, revisa e atualiza as regras. Um arquivo de configuração versionado (ex: .eslintrc, .pylintrc) com comentários explicativos ajuda na transparência.
  • Mitigação de falsos positivos: Quando uma regra dispara incorretamente, ajuste a lógica ou adicione exceções documentadas. Nunca desative a regra por completo sem análise.

7. Métricas e monitoramento do impacto

O verdadeiro valor dos linters personalizados aparece quando você mede seu impacto:

  • Rastreie violações ao longo do tempo: Use dashboards (Grafana, Kibana) para visualizar a quantidade de violações por sprint. Uma tendência de queda indica que a arquitetura está sendo preservada.
  • Correlacione com qualidade: Compare a redução de violações com métricas como taxa de bugs, tempo médio de revisão de PR e dívida técnica estimada.
  • Feedback loop: Reúna o time periodicamente para discutir os dados. Regras que geram muitos falsos positivos devem ser ajustadas. Regras que nunca disparam podem ser removidas.

Linters personalizados não são um fim em si mesmos, mas uma ferramenta para manter a arquitetura viva e saudável. Quando bem implementados, eles transformam convenções abstratas em guardiões automatizados que protegem o design do sistema no dia a dia do desenvolvimento.

Referências

  • ArchUnit User Guide — Documentação oficial do ArchUnit para Java, com exemplos de regras arquiteturais como dependências entre camadas e proibição de ciclos.
  • ESLint Custom Rules — Guia oficial da ESLint para criação de regras personalizadas usando AST, ideal para JavaScript/TypeScript.
  • Pylint Custom Checkers — Tutorial oficial do Pylint para criar checkers personalizados em Python, incluindo validação de naming patterns.
  • Checkstyle Writing Checks — Documentação do Checkstyle para criar verificações personalizadas em Java, com exemplos de validação de anotações e estrutura de classes.
  • Pre-commit Hooks with Husky — Guia de configuração de pré-commit hooks usando Husky, essencial para executar linters personalizados antes do commit.
  • GitHub Actions for Linting — Exemplo prático de como integrar linters em pipelines de CI/CD com GitHub Actions.
  • Dependency-Check OWASP — Ferramenta de análise de dependências que pode ser estendida para verificar convenções arquiteturais de segurança.