Ir para o conteúdo

falsegreen-js (JavaScript / TypeScript)

CI npm Downloads License: MIT

O scanner determinístico para JS/TS. Uma varredura estática via TypeScript compiler API, sem execução de código, agnóstica ao runner entre Jest, Vitest, Mocha+Chai, Jasmine, AVA, node:test, Cypress, Playwright e Testing Library. Cobre .js, .jsx, .ts, .tsx, .mjs, .cjs, .mts, .cts.

Install

npm install --save-dev falsegreen-js

Use

npx falsegreen-js src                       # scan
npx falsegreen-js --format json|sarif|junit # output shape (matches the Python sibling)
npx falsegreen-js --baseline .falsegreen-baseline.json
npx falsegreen-js --config-audit            # jest/vitest config for project-level false-green

O relatório JSON carrega riskGroup e oracleRegistryVersion: os códigos são classificados em grupos de risco fechados, e o registro de oráculos (sync-fail / promise / runner-registered / value-only) é versionado, de modo que a detecção de assíncrono é baseada em princípios em vez de casada por string.

First finding

Salve um teste cuja asserção nunca roda:

// demo.test.ts
import { add } from "./add";

it("adds", () => {
  expect(add(2, 2));   // sem matcher - isso não asserta nada
});

Rode o scanner sobre ele:

npx falsegreen-js demo.test.ts

Ele reporta:

demo.test.ts:5  [JS2] expect() with no matcher - the assertion never runs
    level: unit   fix: add a matcher, e.g. .toBe(4)

Summary: 1 high, 0 low.

Reading a finding

Cada linha carrega os mesmos campos:

  • demo.test.ts:5 - o arquivo e a linha que disparou o achado.
  • [JS2] - o código do catálogo. JS2 é o expect() sem matcher. Cada código está explicado no catálogo JS/TS.
  • level: unit - em qual nível da pirâmide de testes o arquivo está, lido a partir dos imports do arquivo.
  • fix: - uma dica de uma linha. Aqui: adicione um matcher para a asserção realmente rodar.

--format json|sarif|junit dá um relatório legível por máquina; o SARIF sobe para o GitHub code scanning e os achados aparecem direto no pull request.

Complete usage and configuration

O guia inicial acima é o caminho de cinco minutos. Esta seção é a referência completa: cada canal de instalação, cada formato de saída, cada chave de configuração, o contrato de código de saída e a integração com CI. Espelha o que o README do projeto documenta.

Install channels

Node 18 ou mais novo. O scanner lê .js, .jsx, .ts, .tsx, .mjs, .cjs, .mts, .cts.

npm install -D falsegreen-js   # dependência de desenvolvimento do projeto
npm install -g falsegreen-js   # instalação global
npx falsegreen-js .            # roda a última versão do npm sem instalar
node dist/cli.js .             # roda de um checkout clonado

Agnóstico de runner: o vocabulário de asserção e teste cobre Jest, Vitest, Mocha + Chai, Jasmine, AVA, node:test, tap, Cypress, Playwright, Testing Library e Vue Test Utils, então um teste Mocha ou AVA não é confundido com um que nunca checa nada.

Invocation

npx falsegreen-js                 # escaneia o cwd
npx falsegreen-js src test        # escaneia caminhos
npx falsegreen-js --staged        # só os arquivos de teste no stage do git (pre-commit)

Cada achado é reportado com seu nível na pirâmide (unit / integration / e2e, lido dos imports do arquivo) e uma dica de correção de uma linha; o resumo separa os achados por nível e lista as correções mais comuns.

Output formats

--format text|json|sarif|junit seleciona o formato do relatório (padrão text). --json continua como alias de --format json. Eles casam com o irmão Python conceito a conceito, então um pipeline pode trocar um scanner pelo outro.

npx falsegreen-js . --json                # JSON legível por máquina
npx falsegreen-js . --format sarif        # SARIF 2.1.0
npx falsegreen-js . --format junit        # JUnit XML
npx falsegreen-js . --output report.json  # grava em arquivo
npx falsegreen-js . --output .falsegreen/ # grava report.<ext> num diretório
  • sarif emite SARIF 2.1.0: uma regra por código presente, um resultado por achado, error para ALTO, warning para BAIXO, note para desligado. As tags do resultado carregam o julgamento (J1-J6), o grupo de risco (risk:effectiveness...) e o nível. Suba para o GitHub code scanning para ver os achados inline no PR.
  • junit emite JUnit XML: achados ALTO viram <failure>, o resto vira <skipped>, então um reporter de CI os mostra como suíte falhando.

O relatório JSON ainda carrega riskGroup e oracleRegistryVersion: os códigos são classificados em grupos de risco fechados, e o registro de oráculos (sync-fail / promise / runner-registered / value-only) é versionado para que a detecção async seja principiada, não casada por string. --output aceita arquivo ou diretório; mantenha o diretório de saída no gitignore.

Configuration

Desabilitar / habilitar códigos (CLI).

npx falsegreen-js --disable C7,JS3   # desliga códigos específicos
npx falsegreen-js --enable D8,M2     # reativa códigos desligados/opcionais na severidade do catálogo
npx falsegreen-js --diagnostics      # inclui o grupo de manutenibilidade D*/M* como avisos

--enable liga um código desligado por padrão na severidade do catálogo; não pode subir um código acima do catálogo. Um código passado a --enable e --disable ao mesmo tempo continua desligado, --disable vence.

Grupo de diagnóstico (desligado por padrão). Estes não são false-green, então só avisam: D1 assertion roulette, D3 assert duplicado, D4 casos de each só por índice, D6 console.* num teste, D7 teste anônimo, D8 número mágico, M2 corpo de teste longo. Ligue todos com --diagnostics, ou por código via severity na config.

Supressão inline. Um comentário na linha do achado silencia um achado justificado:

expect(user.id).toBe(user.id); // falsegreen: ignore[C7]
expect(x);                     // falsegreen: ignore

Arquivo de config do projeto. falsegreen.json, .falsegreenrc.json, ou uma chave "falsegreen" no package.json:

{
  "disable": ["C8"],
  "exclude": ["**/legacy/**"],
  "severity": { "JS3": "off", "C16": "high" }
}

Precedência, da maior para a menor: --disable na CLI, --enable na CLI, disable / severity da config, padrão do catálogo.

Auditoria de config. --config-audit é um modo separado. Em vez de escanear arquivos de teste, lê a config de Jest / Vitest (campo jest do package.json, jest.config.*, vitest.config.*) e reporta as formas de nível de projeto pelas quais uma suíte fica verde por configuração:

  • PL10 - passWithNoTests passa numa execução vazia ou filtrada até o nada.
  • PL7 - sem coverageThreshold / coverage.thresholds.
  • PL8 - bail interrompe a execução cedo.

Baseline (catraca). Adote o scanner numa base grande sem corrigir todo achado legado de uma vez:

npx falsegreen-js --write-baseline   # registra os achados atuais em .falsegreen-baseline.json, exit 0
npx falsegreen-js --baseline         # reporta e falha só nos achados que não estão no baseline

--baseline [PATH] e --write-baseline [PATH] usam .falsegreen-baseline.json por padrão. A identidade de um achado é um fingerprint de conteúdo (sha1 de caminho relativo + código + detalhe, sem número de linha), então sobrevive a deslocamentos de linha não relacionados. Comite o baseline e deixe o CI bloquear só nos achados novos.

Exit codes

Código Significado
0 limpo, nenhum achado que afete o gate
10 só achados de baixa confiança
20 ao menos um achado de alta confiança

Bloqueie o commit no 20.

CI integration

GitHub Actions. Um job que falha no exit 20:

name: falsegreen-js
on: [push, pull_request]
jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: "20" }
      - run: npx falsegreen-js .   # exit 20 falha o job

Upload de SARIF para o GitHub code scanning. Emita SARIF e passe para a action do CodeQL para que os achados apareçam inline no pull request:

      - run: npx falsegreen-js . --format sarif --output falsegreen.sarif
        continue-on-error: true
      - uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: falsegreen.sarif

Hook de pre-commit. Rode o scan dos arquivos em stage como hook com --staged:

  - repo: local
    hooks:
      - id: falsegreen-js
        name: falsegreen-js
        entry: npx falsegreen-js --staged
        language: system
        pass_filenames: false
        types_or: [javascript, ts, tsx, jsx]

Para a camada que nenhum scan estático alcança (um teste verde falha quando o código está errado?), rode um teste de mutação como o Stryker. O falsegreen-js é o pré-filtro barato em todo commit; os casos semânticos (intenção, correção do oráculo) são a falsegreen-skill.

What it covers

Detalhe completo por código no catálogo JS/TS.

Grupo Códigos Efeito
Compartilhado com Python (F1-F6) C2, C2b, C5, C6, C7, C8, C9, C16, C18, C20, C21, C23, C37, C44, CC ALTO bloqueia, BAIXO avisa
Específico de JavaScript JS1-JS9, JS11, JS13, JS15, JS17, JS18, JS21, JS22 idem
Diagnóstico / acoplamento (F8) D1, D3, D4, D6, D7, D8, M2 opcional
Projeto / CI (--config-audit) PL10 (--passWithNoTests), PL7 (limiar de cobertura), PL8 (bail) lê a config do jest/vitest

Playwright (E2E e API)

Um projeto que mistura node:test/Jest/Vitest com specs de Playwright é escaneado sem ruído. O Playwright tem uma gramática de asserção própria, e lê-la pela lente do node:test/assert gerava falsos positivos; o scanner agora reconhece o framework e o julga pelos critérios dele.

Como o Playwright é reconhecido. Três sinais, e basta um para marcar o arquivo:

  1. o import do pacote (@playwright/test, playwright, playwright-core);
  2. uma chamada da API test.* com namespace (test.describe, test.step, test.beforeAll, test.afterAll) — isso pega as suítes que importam test/expect de um re-export de fixture (test.extend/mergeTests, ex.: import { test, expect } from "@company/e2e/fixtures"), o padrão que a própria doc do Playwright recomenda;
  3. um parâmetro de fixture do Playwright: browserName sozinho (inequívoco), ou page/context/request corroborado por um matcher exclusivo do Playwright como toBeOK/toHaveCount. Um request isolado nunca liga o sinal por conta própria, então um teste de supertest/Express que desestrutura { request } não é confundido com Playwright.

O que isso corrige.

  • Hooks de ciclo de vida não são testes. test.beforeEach/afterEach/beforeAll/afterAll, test.describe e test.step não são mais analisados como corpo de teste, então um hook de setup ou teardown que chama código sem asserção não reporta mais C2b (e os códigos irmãos de corpo param de disparar no hook). Hook não é teste; a asserção fica no test(...). test.only/test.serial/test.fail/test.failing são testes que rodam de verdade e mantêm a análise completa.
  • Skip condicional é guarda de runtime, não teste desligado. test.skip(condição[, motivo]), test.skip() e a forma de predicado test.skip(({ browserName }) => ...) não disparam mais JS4. Skips declarados incondicionais ainda disparam: test.skip("título", fn), test.skip(true, "...") (grupo desligado por constante), test.describe.skip, xit/xdescribe/it.todo. A supressão exige um sinal de Playwright e um primeiro argumento não-string, então um test.skip(fn) sem título de node:test/Mocha num arquivo que não é Playwright continua disparando JS4 — sem falso negativo.
  • Matchers de resposta de API e de UI contam como asserção. expect(locator).toBeVisible()/ toHaveText()/toHaveCount() e expect(apiResponse).toBeOK() já eram oráculos reais; este trabalho adiciona o tratamento de hook, skip e reconhecimento de arquivo ao redor deles, para a spec inteira — de UI ou de API — ser lida corretamente.

O sinal de reconhecimento de arquivo vale só para a decisão de skip; não amplia o nível da pirâmide, então um arquivo Jest/Vitest com um describe simples nunca vira arquivo de camada browser nem passa a perdoar uma checagem fraca. Veja C6 abaixo.

C6 (checagem fraca) seguro para Playwright

O C6 (checagem fraca) usa um modelo de oráculo único em todos os runners: só dispara quando uma checagem de presença/não-vazio (toBeTruthy/toBeDefined/.length > 0) é o ÚNICO oráculo do teste. Uma checagem fraca ao lado de uma asserção mais forte no mesmo teste (uma guarda toBeGreaterThan(0) antes de um toBe(...)) não é sinalizada — a asserção forte carrega o teste. Um teste cujas asserções são todas fracas continua reportando C6. Isso importa em E2E, onde uma guarda como expect(items.length).toBeGreaterThan(0) costuma vir antes da checagem real.

What it does not cover, and why

Fora de escopo (o eixo errado)

O mesmo limite do resto da família: fragilidade/falso-vermelho, higiene, lentidão, design, nomenclatura, duplicação, runtime. Veja cobertura vs a literatura.

Códigos JS deixados de fora de propósito

Código O que sinalizaria Por que não
JS10 qualquer condicional no corpo do teste amplo demais; jest/no-conditional-in-test (ESLint) já cobre. No máximo, diagnóstico
JS12 uma promise com expect não retornada/aguardada absorvido por JS7
JS14 um snapshot gigante higiene (F8), não false-green
JS16 teste assíncrono sem a guarda expect.assertions(n) a ausência de uma guarda não é um smell; sinalizá-la tem taxa de falso-positivo altíssima
JS19 toBe sobre um literal de objeto/array (era toEqual) isto é falso-VERMELHO (uma checagem de identidade que falha em código correto): o eixo oposto
JS20 uma Promise comparada sem resolves/rejects saber que o sujeito é uma Promise exige informação de tipo; falso positivo alto

Não aplicável a partir do Python

Códigos específicos do pytest não têm análogo em JS: as regras de coleta (família C4), os códigos de binding/escopo de pytest.raises, os códigos de capsys/os.environ. Estão ausentes de propósito, não esquecidos.

Além do scanner

A fatia semântica (intenção, correção do oráculo) é a falsegreen-skill; o runtime (mutação com Stryker) fica fora de banda. Veja escopo e honestidade.