mpsuesser/effect-oxlint
GitHub: mpsuesser/effect-oxlint
基于 Effect v4 函数式编程范式,为 oxlint 提供类型安全、可组合的自定义 lint 规则编写框架。
Stars: 3 | Forks: 0
# effect-oxlint
[](https://www.npmjs.com/package/effect-oxlint)
[](https://jsr.io/@effect-oxlint/effect-oxlint)
[](./LICENSE)
使用 [Effect v4](https://effect.website) 编写 [oxlint](https://oxc.rs/docs/guide/usage/linter) 自定义 lint 规则。
`effect-oxlint` 将 `@oxlint/plugins` 封装在 Effect 惯用法中,以便规则作者可以使用类型化错误、可组合的 visitor、`Option` 安全的 AST 匹配以及基于 `Ref` 的状态,而无需使用任何可变变量。
## 功能
- **`Rule.define`** — 将 `create` 编写为 Effect 生成器;使用 `yield*` 处理状态、上下文和诊断信息
- **`Visitor.*`** — 可组合的 visitor 组合子:`on`、`onExit`、`merge`、`tracked`、`filter`、`accumulate`
- **`AST.*`** — 返回 `Option` 的匹配器,支持双 API(数据优先和数据置尾):`matchMember`、`matchCallOf`、`matchImport`、`narrow`、`memberPath`
- **`Diagnostic.*`** — 结构化诊断构建器,支持可组合的自动修复
- **`SourceCode.*` / `Scope.*`** — 针对 token、注释、作用域和变量的 Effectful 查询
- **`Testing.*`** — 适用于 `@effect/vitest` 的 mock 构建器、规则运行器和断言辅助工具
- **`Plugin.define`** — 将规则组装成 oxlint 可以加载的插件
## 安装
```
npm install effect-oxlint effect@4.0.0-beta.43
```
```
bun add effect-oxlint effect@4.0.0-beta.43
```
```
deno add jsr:@effect-oxlint/effect-oxlint
```
## 快速开始
### 1. 定义规则
```
import * as Effect from 'effect/Effect';
import * as Option from 'effect/Option';
import { AST, Diagnostic, Rule, RuleContext } from 'effect-oxlint';
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 {
// node is typed as ESTree.MemberExpression automatically
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'
})
)
}
)
};
}
});
```
### 2. 为常见模式使用便捷工厂
```
import { Rule } from 'effect-oxlint';
// Ban a member expression
const noMathRandom = Rule.banMember('Math', 'random', {
message: 'Use the Effect Random service instead'
});
// Ban an import
const noNodeFs = Rule.banImport('node:fs', {
message: 'Use the Effect FileSystem service instead'
});
// Ban a statement type
const noThrow = Rule.banStatement('ThrowStatement', {
message: 'Use Effect.fail instead of throw'
});
// Ban bare identifier calls (e.g. fetch(), useState())
const noFetch = Rule.banCallOf('fetch', {
message: 'Use Effect HTTP client instead'
});
// Ban new expressions (e.g. new Date(), new Error())
const noNewDate = Rule.banNewExpr('Date', {
message: 'Use Clock service instead'
});
// Ban multiple patterns with one rule
const noImperativeLoops = Rule.banMultiple(
{
statements: [
'ForStatement',
'ForInStatement',
'ForOfStatement',
'WhileStatement',
'DoWhileStatement'
]
},
{ message: 'Use Arr.map / Effect.forEach instead' }
);
// Combine different ban types in a single rule
const useClockService = Rule.banMultiple(
{
newExprs: 'Date',
members: [['Date', 'now']]
},
{ message: 'Use Clock service' }
);
```
### 3. 组装为插件
```
import { Plugin } from 'effect-oxlint';
export default Plugin.define({
name: 'my-effect-rules',
rules: {
'no-json-parse': noJsonParse,
'no-math-random': noMathRandom,
'no-node-fs': noNodeFs,
'no-throw': noThrow
}
});
```
## Visitor 组合子
Visitor 是 `Record Effect>` 映射。`Visitor` 模块提供了用于构建和组合它们的组合子。
### 合并多个 visitor
```
import { Visitor } from 'effect-oxlint';
const combined = Visitor.merge(importVisitor, memberVisitor, statementVisitor);
```
当两个 visitor 处理相同的节点类型时,这两个处理程序将按顺序运行。
### 使用 `Ref` 跟踪深度
用 `Visitor.tracked` 替换可变的 `let depth = 0` 计数器:
```
import * as Ref from 'effect/Ref';
import { AST, Visitor } from 'effect-oxlint';
const depthRef = yield * Ref.make(0);
const tracker = Visitor.tracked(
'CallExpression',
// node is typed as ESTree.CallExpression
(node) => AST.isCallOf(node, 'Effect', 'gen'),
depthRef
);
// depthRef increments on enter, decrements on exit
```
### 收集与分析
在遍历期间收集数据,然后在 `Program:exit` 时进行分析:
```
import { Visitor, AST } from 'effect-oxlint';
const visitor =
yield *
Visitor.accumulate(
'ExportNamedDeclaration',
(node) => AST.narrow(node, 'ExportNamedDeclaration'),
function* (exports) {
// all exports collected — analyze them here
}
);
```
### 按文件名过滤
将 visitor 限制在特定文件中:
```
import { Visitor } from 'effect-oxlint';
const visitor =
yield *
Visitor.filter((filename) => !filename.endsWith('.test.ts'), mainVisitor);
```
## AST 匹配
每个匹配器都返回 `Option`,以便与 `pipe`、`Option.map` 和 `Option.flatMap` 进行安全组合。所有公共匹配器均支持双 API(数据优先和数据置尾)。
```
import { pipe } from 'effect';
import * as Option from 'effect/Option';
import type { ESTree } from 'effect-oxlint';
import { AST } from 'effect-oxlint';
// Data-first (pass a MemberExpression directly)
declare const memberNode: ESTree.MemberExpression;
AST.matchMember(memberNode, 'JSON', ['parse', 'stringify']);
// Data-last (pipe-friendly)
pipe(memberNode, AST.matchMember('Effect', 'gen'));
// Chain: narrow an ESTree.Node, then match
declare const node: ESTree.Node;
pipe(
AST.narrow(node, 'CallExpression'),
Option.flatMap(AST.matchCallOf('Effect', 'gen'))
);
// Extract member path: a.b.c -> Some(['a', 'b', 'c'])
AST.memberPath(memberNode);
// Match imports by string or predicate
declare const importNode: ESTree.ImportDeclaration;
AST.matchImport(importNode, (src) => src.startsWith('node:'));
```
## 诊断与自动修复
```
import { Diagnostic } from 'effect-oxlint';
// Basic diagnostic
const diag = Diagnostic.make({ node, message: 'Avoid this pattern' });
// With autofix
const fixed = Diagnostic.withFix(
diag,
Diagnostic.replaceText(node, 'replacement')
);
// Compose multiple fixes
const multiFix = Diagnostic.composeFixes(
Diagnostic.insertBefore(node, 'prefix'),
Diagnostic.insertAfter(node, 'suffix')
);
```
## 类型
`effect-oxlint` 重新导出了所有 `@oxlint/plugins` 类型,因此使用者不需要直接依赖它来进行类型导入:
```
import type { ESTree, OxlintPlugin, CreateRule } from 'effect-oxlint';
// ESTree namespace includes all AST node types
const node: ESTree.CallExpression = /* ... */;
```
## 测试
`effect-oxlint` 附带了一个 `Testing` 模块,包含 mock AST 构建器、规则运行器和断言辅助工具。
```
import { describe, expect, test } from '@effect/vitest';
import * as Option from 'effect/Option';
import { Rule, Testing } from 'effect-oxlint';
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('ignores other member expressions', () => {
const result = Testing.runRule(
noJsonParse,
'MemberExpression',
Testing.memberExpr('console', 'log')
);
Testing.expectNoDiagnostics(result);
});
});
```
### Node 构建器接受符合人体工程学的简写
```
// newExpr accepts a string — auto-wrapped in id()
Testing.newExpr('Date'); // equivalent to Testing.newExpr(Testing.id('Date'))
// ifStmt params are all optional — useful for enter/exit tracking tests
Testing.ifStmt(); // minimal IfStatement node
// program() accepts a comments parameter for comment-based rules
Testing.program(
[Testing.exprStmt(Testing.callExpr('foo'))],
[Testing.comment('Line', ' eslint-disable')]
);
```
可用的构建器包括 `id`、`memberExpr`、`computedMemberExpr`、`chainedMemberExpr`、`callExpr`、`callOfMember`、`importDecl`、`newExpr`、`throwStmt`、`tryStmt`、`ifStmt`、`program`、`objectExpr` 等。
## 模块
| 模块 | 用途 |
| -------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `Rule` | 核心规则构建器(`define`、`meta`、`banMember`、`banImport`、`banStatement`、`banCallOf`、`banNewExpr`、`banMultiple`) |
| `Visitor` | 可组合的 visitor(`on`、`onExit`、`merge`、`tracked`、`filter`、`accumulate`) |
| `AST` | 返回 `Option` 的模式匹配器(`matchMember`、`matchCallOf`、`matchImport`、`narrow`、`memberPath`、`findAncestor`) |
| `Diagnostic` | 诊断构建和自动修复辅助工具 |
| `RuleContext` | 可访问文件信息、源代码和 `report` 的 Effect 服务 |
| `SourceCode` | Effectful 查询:文本、token、注释、作用域、节点位置 |
| `Scope` | 使用 `Option` 进行变量查找和引用分析 |
| `Plugin` | 用于插件组装的 `define` 和 `merge` |
| `Comment` | 注释类型谓词(`isLine`、`isBlock`、`isJSDoc`、`isDisableDirective`) |
| `Token` | Token 类型谓词(`isKeyword`、`isPunctuator`、`isIdentifier`、`isString`) |
| `Testing` | 用于测试工具的 mock 构建器、`runRule`、`expectDiagnostics`、`messages` |
## 开发
```
bun install # install dependencies
bun run check # lint + format + typecheck (auto-fix)
bun run test # run all tests
bun run typecheck # tsgo type-check only
# 单个测试文件
bunx vitest run test/Rule.test.ts
# 按测试名称
bunx vitest run -t "reports for matching"
```
## 贡献
参见 [CONTRIBUTING.md](./CONTRIBUTING.md)。
## 许可证
MIT
标签:CMS安全, Effect v4, JavaScript, JSR, Lint规则, npm包, odt, OxLint, TypeScript, Vitest, 代码规范, 函数式编程, 威胁情报, 安全插件, 开发者工具, 开源库, 抽象语法树, 插件开发, 搜索引擎爬虫, 测试, 组合子, 自动化攻击, 自定义规则, 错误基检测, 静态代码分析