Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/mpsuesser/effect-oxlint/llms.txt

Use this file to discover all available pages before exploring further.

effect-oxlint ships a dedicated Testing module with everything you need to exercise rules without running the full oxlint binary. It is exposed as a subpath export so test-only code never enters production bundles. Import it with import * as Testing from 'effect-oxlint/testing'.

Subpath import

import { describe, expect, test } from '@effect/vitest';
import * as Option from 'effect/Option';
import { Rule } from 'effect-oxlint';
import * as Testing from 'effect-oxlint/testing';
Always import from 'effect-oxlint/testing', not 'effect-oxlint'. The testing subpath is excluded from production builds and carries @effect/vitest as a dev-only peer.

Testing.runRule — run against a single visitor event

Testing.runRule(rule, visitorKey, node, opts?) creates a mock oxlint context, runs the rule’s create generator once, then fires the named visitor handler with the provided node. Returns ReadonlyArray<ReportedDiagnostic>.
const result = Testing.runRule(
  noJsonParse,
  'MemberExpression',
  Testing.memberExpr('JSON', 'parse')
);
The optional opts parameter is a MockContextOptions object for controlling filename, source text, and rule options passed to the rule.

Testing.runRuleMulti — multiple events with shared state

Testing.runRuleMulti(rule, pairs, opts?) fires multiple visitor events against the same context. Crucially, Ref state created in create is shared across all calls — this is how you test rules that track enter/exit depth or accumulate data across nodes.
const result = Testing.runRuleMulti(noThrowInGen, [
  ['CallExpression',  Testing.callOfMember('Effect', 'gen')],
  ['ThrowStatement',  Testing.throwStmt()],
  ['CallExpression:exit', Testing.callOfMember('Effect', 'gen')]
]);

Testing.expectDiagnostics — partial matching by message

Testing.expectDiagnostics(result, expected) asserts that the result array matches the expected patterns. Each matcher object is partial — only the fields you provide are compared. Missing fields are ignored.
Testing.expectDiagnostics(result, [
  { message: 'Use Schema for JSON' }
]);

// Match by messageId
Testing.expectDiagnostics(result, [
  { messageId: 'noThrow' },
  { messageId: 'noTryCatch' }
]);
The assertion throws a descriptive error (including the actual messages received) when the count or content does not match.

Testing.expectNoDiagnostics — assert clean

Testing.expectNoDiagnostics(result) throws when any diagnostic was reported. Use this on the “should not flag” cases.
Testing.expectNoDiagnostics(
  Testing.runRule(noJsonParse, 'MemberExpression', Testing.memberExpr('console', 'log'))
);

Testing.messages — ReadonlyArray<Option<string>>

Testing.messages(result) maps each ReportedDiagnostic to Option<string>Option.some(message) when the diagnostic uses an inline message, Option.none() when it uses messageId.
expect(Testing.messages(result)).toEqual([
  Option.some('Use Schema for JSON')
]);

Testing.messageIds — ReadonlyArray<Option<string>>

Testing.messageIds(result) is the counterpart for messageId-based diagnostics.
expect(Testing.messageIds(result)).toEqual([
  Option.some('noThrow')
]);

Complete test example

This is the full test suite for the no-json-parse rule from the README:
import { describe, expect, test } from '@effect/vitest';
import * as Option from 'effect/Option';
import { AST, Diagnostic, Rule, RuleContext } from 'effect-oxlint';
import * as Testing from 'effect-oxlint/testing';
import * as Effect from 'effect/Effect';

const noJsonParse = Rule.define({
  name: 'no-json-parse',
  meta: Rule.meta({
    type: 'suggestion',
    description: 'Use Schema for JSON decoding instead of JSON.parse'
  }),
  create: function* () {
    const ctx = yield* RuleContext;
    return {
      MemberExpression: (node) =>
        Option.match(
          AST.matchMember(node, 'JSON', ['parse', 'stringify']),
          {
            onNone: () => Effect.void,
            onSome: (matched) =>
              ctx.report(
                Diagnostic.make({
                  node: matched,
                  message: 'Use Schema for JSON'
                })
              )
          }
        )
    };
  }
});

describe('no-json-parse', () => {
  test('reports JSON.parse', () => {
    const result = Testing.runRule(
      noJsonParse,
      'MemberExpression',
      Testing.memberExpr('JSON', 'parse')
    );
    Testing.expectDiagnostics(result, [{ message: 'Use Schema for JSON' }]);
    // Or use the messages() helper — returns Option per diagnostic
    expect(Testing.messages(result)).toEqual([
      Option.some('Use Schema for JSON')
    ]);
  });

  test('reports JSON.stringify', () => {
    const result = Testing.runRule(
      noJsonParse,
      'MemberExpression',
      Testing.memberExpr('JSON', 'stringify')
    );
    Testing.expectDiagnostics(result, [{ message: 'Use Schema for JSON' }]);
  });

  test('ignores other member expressions', () => {
    const result = Testing.runRule(
      noJsonParse,
      'MemberExpression',
      Testing.memberExpr('console', 'log')
    );
    Testing.expectNoDiagnostics(result);
  });
});

Node builders overview

The Testing module provides a comprehensive set of AST node constructors. Each builder produces a minimal mock object satisfying the type shape expected by the rule and visitor machinery.

Identifiers

Testing.id('fetch')
// { type: 'Identifier', name: 'fetch' }

Member expressions

// obj.prop (non-computed)
Testing.memberExpr('JSON', 'parse')

// obj[prop] (computed)
Testing.computedMemberExpr('obj', 'key')

// a.b.c chained member (at least 2 names required)
Testing.chainedMemberExpr('Effect', 'gen')
Testing.chainedMemberExpr('a', 'b', 'c', 'd')

Call expressions

// fetch(args)
Testing.callExpr('fetch', [Testing.strLiteral('/api')])

// Effect.gen(args) — MemberExpression callee
Testing.callOfMember('Effect', 'gen', [])

Imports

// import ... from 'effect'
Testing.importDecl('effect')

// import { map, filter } from 'effect/Array'
Testing.importDeclWithSpecifiers('effect/Array', [
  Testing.importSpecifier('map'),
  Testing.importSpecifier('filter')
])

Statements

Testing.throwStmt()           // ThrowStatement
Testing.tryStmt()             // TryStatement
Testing.ifStmt()              // IfStatement (all params optional)
Testing.forStmt()             // ForStatement
Testing.forInStmt()           // ForInStatement
Testing.forOfStmt()           // ForOfStatement
Testing.whileStmt()           // WhileStatement
Testing.doWhileStmt()         // DoWhileStatement
Testing.switchStmt()          // SwitchStatement

Declarations

// const foo = <init>
Testing.varDecl('const', 'foo', Testing.callExpr('bar'))

// class Foo extends Bar { x; static y() {} }
Testing.classDecl('Foo', {
  superClass: Testing.id('Bar'),
  members: [Testing.propertyDef('x'), Testing.methodDef('y', true)]
})

// export { ... }
Testing.exportNamedDecl(Testing.varDecl('const', 'foo'))

New expressions

newExpr accepts either a string (auto-wrapped in id()) or an existing node:
Testing.newExpr('Date')                     // new Date()
Testing.newExpr(Testing.id('Date'))         // equivalent
Testing.newExpr('Error', [Testing.strLiteral('oops')])  // new Error('oops')

Program

Testing.program(
  [Testing.exprStmt(Testing.callExpr('foo'))],
  [Testing.comment('Line', ' eslint-disable')]
)

Ancestor helpers

// Generic node with optional parent pointer
Testing.astNode('ThrowStatement', parentNode)

// Build a chain from outermost to innermost — returns the innermost node
// Creates: FunctionDeclaration → BlockStatement → ThrowStatement
Testing.withParentChain('FunctionDeclaration', 'BlockStatement', 'ThrowStatement')

Mock context

// Create a mock context directly (returns { context, diagnostics })
const { context, diagnostics } = Testing.createMockContext({
  filename: '/src/myFile.ts',
  sourceText: 'const x = 1;',
  options: [{ allowedGlobals: [] }]
});

// Create a Layer for it.effect tests
const layer = Testing.mockRuleContextLayer({ filename: '/src/myFile.ts' });

// Provide mock context inline inside an Effect
const result = await Effect.runPromise(
  Testing.withMockRuleContext(myEffect, { filename: '/src/myFile.ts' })
);

Pure test vs it.effect test patterns

import { test, expect } from '@effect/vitest';
import * as Testing from 'effect-oxlint/testing';

// Pure test — no Effect.gen, no yield*
test('reports JSON.parse', () => {
  const result = Testing.runRule(
    noJsonParse,
    'MemberExpression',
    Testing.memberExpr('JSON', 'parse')
  );
  Testing.expectDiagnostics(result, [{ message: 'Use Schema for JSON' }]);
});
Use test (not it) for pure synchronous tests and it.effect for tests that need to yield* Effects. This is the convention enforced in the effect-oxlint test suite itself.

Build docs developers (and LLMs) love