Skip to content

Prévia dos plugins JS do Oxlint

Esta publicação anuncia a prévia dos plugins JS do Oxlint. Os plugins JS já chegaram à fase alpha! Veja o anúncio Oxlint JS Plugins Alpha para os recursos e melhorias mais recentes.


No início deste ano pedimos contribuições da comunidade para orientar o design do suporte do Oxlint a plugins JS personalizados. Hoje temos o prazer de anunciar o resultado de vários meses de pesquisa, prototipagem e, por fim, implementação:

O Oxlint passa a aceitar plugins escritos em JS!

Principais recursos

  • API de plugin compatível com o ESLint. O Oxlint poderá executar muitos plugins existentes do ESLint sem alterações.
  • Uma API alternativa, ligeiramente diferente, que permite um desempenho melhor.

O que é e o que não é

Esta versão de prévia é só o começo. É importante destacar que:

  • Esta primeira versão não implementa toda a API de plugins do ESLint.
  • O desempenho já é bom, e vai ficar muito melhor — temos várias otimizações na fila.

As APIs mais usadas para regras de verificação de código já estão implementadas, então muitas regras do ESLint já funcionam. Mas as APIs baseadas em tokens não estão presentes; regras estilísticas (formatação) não funcionam.

Convidamos a experimentar, dar feedback e ajudar a definir as prioridades da próxima fase.

O que este artigo cobre

  1. Como usar.
  2. O que vem em seguida.
  3. Alguns detalhes técnicos que possibilitam nossa abordagem de “ter o bolo e comer o bolo”: compatibilidade com o ESLint e excelente desempenho.

Início rápido

Instale o Oxlint no projeto:

sh
pnpm add -D oxlint

Escreva um plugin JS personalizado:

js
// plugin.js

// A regra mais simples — sem debugger
const rule = {
  create(context) {
    return {
      DebuggerStatement(node) {
        context.report({
          message: "No debugger!",
          node,
        });
      },
    };
  },
};

const plugin = {
  meta: {
    name: "best-plugin-ever",
  },
  rules: {
    "no-debugger": rule,
  },
};

export default plugin;

Crie o arquivo de configuração habilitando o plugin:

json
// .oxlintrc.json
{
  "jsPlugins": ["./plugin.js"],
  "rules": {
    "best-plugin-ever/no-debugger": "error"
  }
}

Adicione um arquivo para ser lintado:

js
// foo.js
debugger;

Execute o Oxlint:

sh
pnpm oxlint

A saída esperada:

 x best-plugin-ever(no-debugger): No debugger!
  ,-[foo.js:1:1]
1 | debugger;
  : ^^^^^^^^^
  `----

Para mais detalhes sobre como criar plugins, veja a documentação.

API alternativa

O Oxlint também oferece uma API um pouco diferente, que permite melhor desempenho.

Essa API alternativa produz plugins compatíveis com o ESLint e com o Oxlint.

Exemplo de regra que sinaliza arquivos com mais de 5 declarações de classe:

Versão ESLint

js
const rule = {
  create(context) {
    let classCount = 0;

    return {
      ClassDeclaration(node) {
        classCount++;
        if (classCount === 6) {
          context.report({ message: "Too many classes", node });
        }
      },
    };
  },
};

Versão com a API alternativa

js
import { defineRule } from "oxlint";

const rule = defineRule({
  createOnce(context) {
    // Define counter variable
    let classCount;

    return {
      before() {
        // Reset counter before traversing AST of each file
        classCount = 0;
      },
      // Same as before
      ClassDeclaration(node) {
        classCount++;
        if (classCount === 6) {
          context.report({ message: "Too many classes", node });
        }
      },
    };
  },
});

As diferenças

  1. Envolva o objeto da regra em defineRule(...).
diff
- const rule = {
+ const rule = defineRule({
  1. Use createOnce em vez de create.
diff
-   create(context) {
+   createOnce(context) {
  1. Mova qualquer preparação por arquivo do corpo de create para o hook before.
diff
-     let classCount = 0;
+     let classCount;

      return {
+       before() {
+         classCount = 0; // Reset counter
+       },
        ClassDeclaration(node) {
          classCount++;
          if (classCount === 6) {
            context.report({ message: "Too many classes", node });
          }
        },
      };
    },
  });

Esta é a única diferença importante — create (método do ESLint) é chamado repetidamente para cada arquivo, enquanto createOnce é chamado apenas uma vez.

Todas as outras APIs se comportam exatamente como no ESLint.

Os motivos pelos quais essa API alternativa pode melhorar bastante o desempenho estão explicados na documentação.

Desempenho

Como mencionado acima, o desempenho não foi o foco desta primeira prévia dos plugins JS do Oxlint. O objetivo principal foi cobrir API suficiente para os plugins JS serem úteis em projetos reais e reunir feedback de quem adota cedo.

O desempenho hoje é razoável, mas longe do ideal.

No entanto — e este é o ponto que consideramos importante — nosso protótipo da próxima versão mostra que o desenho arquitetural que adotamos é capaz de um desempenho excepcional, quando várias otimizações forem incorporadas (veja Por baixo dos panos).

Aplicaremos essas otimizações nos próximos meses, e quem usa a ferramenta verá ganhos de várias vezes em relação à versão atual.

Mesmo sem essas otimizações, o desempenho do Oxlint continua competitivo.

Oxlint versus ESLint ao lintar o projeto TypeScript de médio porte vuejs/core:

LinterTempo
ESLint4.116 ms
ESLint multithread3.710 ms
Oxlint48 ms
Oxlint com plugin JS próprio236 ms
Detalhes

INFO

sh
hyperfine -i --warmup 3 \
  './node_modules/.bin/oxlint --silent' \
  './node_modules/.bin/oxlint -c .oxlintrc-with-custom-plugin.json --silent' \
  'USE_CUSTOM_PLUGIN=true ./node_modules/.bin/eslint .' \
  'USE_CUSTOM_PLUGIN=true ./node_modules/.bin/eslint . --concurrency=auto'

Nota: a versão do Oxlint no NPM no momento da redação (1.23.0) tem um bug que afeta este benchmark e subestima muito o custo dos plugins JS. Os resultados acima foram obtidos com o branch main mais recente, após a correção, neste commit. Veja também abaixo.

Neste exemplo, adicionar um plugin JS simples ao Oxlint tem custo perceptível, mas o Oxlint ainda é cerca de 15× mais rápido que o ESLint, inclusive com o executor multithread novo do ESLint.

Óbvio: plugins JS mais pesados, ou muitos deles, aumentam o custo de desempenho.

Recursos

O Oxlint cobre a maior parte das APIs do ESLint normalmente usadas em plugins/regras que dependem só da AST. Isso inclui a maioria das regras do tipo “corrigir código”.

Ainda não há suporte a APIs baseadas em tokens; regras estilísticas (formatação) ainda não funcionam.

Suportado

  • Travessia da AST
  • Exploração da AST (node.parent, context.sourceCode.getAncestors)
  • Fixes
  • Seletores (documentação do ESLint)
  • APIs de SourceCode (por exemplo context.sourceCode.getText(node))

Ainda não suportado

  • Suporte no language server (IDE)
  • Opções de regras
  • Suggestions
  • Análise de escopo (implementada desde v1.25.0)
  • APIs de SourceCode relacionadas a tokens e comentários (por exemplo context.sourceCode.getTokens(node))
  • Análise de fluxo de controle

O que vem em seguida

Nos próximos meses, vamos:

1. Completar a superfície da API de plugins

O alvo é cobrir 100% da API de plugins do ESLint, para que o Oxlint possa, no fim, executar qualquer plugin do ESLint sem modificação.

2. Melhorar o desempenho

O desempenho já é aceitável, mas o prototipado mostrou vários ganhos relevantes com novas otimizações. Vamos aplicá-las e aproximar a velocidade dos plugins JS no Oxlint da velocidade em Rust.

Por baixo dos panos

O restante deste post não é necessário para usar plugins JS com o Oxlint. Se você se interessa pelos detalhes mais “nerds” de como a implementação funciona, continue lendo…

A grande questão: compatibilizar com o ESLint ou não?

A questão que levantamos à comunidade no início do ano foi se o Oxlint deveria mirar uma API de plugins compatível com o ESLint ou não.

Obviamente, uma interface compatível com o ESLint é ideal em termos de familiaridade e facilidade de migração.

Por outro lado, o Oxlint é conhecido pelo excelente desempenho, e sacrificar isso em demasia não seria desejável.

O foco principal do prototipado nos últimos meses foi quantificar o trade-off entre desempenho e compatibilidade com o ESLint, e investigar se existe uma solução “ter o bolo e comer o bolo” que atenda aos dois — API compatível com o ESLint e desempenho aceitável (“aceitável” aqui quer dizer muito rápido mesmo).

Acreditamos que, combinando várias abordagens, encontramos um caminho que satisfaz as duas demandas.

API alternativa

Veja a explicação na documentação sobre por que essa API abre espaço para maior desempenho.

Raw transfer

Ferramentas como o Oxc representam o código de um arquivo JS/TS como uma “AST” (árvore sintática abstrata). ASTs são enormes — muito maiores que o código-fonte que representam.

Em geral, a maior barreira para interoperabilidade performática entre JS e linguagens nativas como Rust é a serialização e a desserialização ao transferir estruturas tão grandes entre os “dois mundos”.

A forma mais simples e comum de mover uma AST entre JS e Rust é: serializar a AST em JSON, enviar para o JS como string e “reidratar” com JSON.parse. Isso é extremamente lento. Muitas vezes o custo dessas conversões anula o ganho de usar código nativo. Outros formatos são mais eficientes que JSON, mas ainda têm overhead considerável.

Desenvolvemos um esquema de “raw transfer” que elimina a serialização, usando o layout de memória nativo do Rust como formato (mais detalhes aqui).

“Raw transfer” é a base da implementação atual dos plugins JS.

Desserialização preguiçosa

O segundo grande inimigo do bom desempenho, principalmente ao rodar JS em vários núcleos em worker threads, é o coletor de lixo. Cada objeto criado precisa ser destruído para recuperar memória. Em JS, isso é papel do GC. Motores como o V8 são muito otimizados, mas a coleta de lixo ainda é cara, e o GC “rouba” CPU do trabalho útil.

Prototipamos um visitor de AST que desserializa a AST de forma preguiçosa, apenas nas partes que realmente precisam existir.

Por exemplo, se a regra trata de declarações de classe, o visitor percorre a maior parte da AST sem criar muita coisa, e só instancia objetos JS para nós ClassDeclaration, passados ao código da regra. No resto da AST (declarações de variável, if, funções etc.) não é necessário criar objetos de nó.

Isso tem dois efeitos:

  1. Raw transfer zera o custo de serialização. A preguiça reduz drasticamente o outro lado, a desserialização.
  2. Pressão muito menor sobre o coletor de lixo.

O Deno adotou abordagem semelhante, explicada com clareza no post do Marvin Hagemeister, e o lint do Deno tem implementação extremamente eficiente.

Para nós, a combinação de desserialização preguiçosa com “raw transfer” é o que entrega desempenho realmente bom. Nos testes, com esses dois custos mitigados, os plugins JS rodam bem mais rápido.

Essa otimização ainda não está na versão atual dos plugins JS. Será implementada numa versão futura.

Experimente!

Teste os plugins JS e conte como foi. Todo feedback — positivo ou negativo — é bem-vindo.

Em especial, se faltar alguma API que seus plugins precisam, avise. Vamos fechar lacunas da API nos próximos meses, priorizando o que tiver mais demanda.

Bons lints!


Edição: 18 de outubro de 2025

A versão original deste artigo, publicada em 9 de outubro, continha resultados de benchmark que mostravam o desempenho dos plugins JS do Oxlint muito melhor do que na prática. Isso vinha de um bug no Oxlint que fazia os plugins JS serem ignorados em muitos arquivos em certas circunstâncias quando a config tinha overrides. Esse bug levou a superestimar o desempenho dos plugins JS nos números que citamos.

Pedimos desculpas pelo erro e agradecemos ao Herrington Darkholme por apontar o problema.