mpsuesser/effect-oxlint

GitHub: mpsuesser/effect-oxlint

基于 Effect v4 函数式编程范式,为 oxlint 提供类型安全、可组合的自定义 lint 规则编写框架。

Stars: 3 | Forks: 0

# effect-oxlint [![npm](https://img.shields.io/npm/v/effect-oxlint)](https://www.npmjs.com/package/effect-oxlint) [![JSR](https://jsr.io/badges/@effect-oxlint/effect-oxlint)](https://jsr.io/@effect-oxlint/effect-oxlint) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./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, 代码规范, 函数式编程, 威胁情报, 安全插件, 开发者工具, 开源库, 抽象语法树, 插件开发, 搜索引擎爬虫, 测试, 组合子, 自动化攻击, 自定义规则, 错误基检测, 静态代码分析