@codemod/matchers

中文文档
1.7.1发布于 2024/03/11Apache-2.0TypeScriptCommonJS近 30 天下载 7.9万

JavaScript & TypeScript 编码器的匹配器.

中文使用文档
文档目录11 节
  1. 安装
  2. 使用量
  3. 简单匹配
  4. 模糊匹配
  5. 配置匹配
  6. 反引用
  7. 在编码器中使用
  8. 深度匹配
  9. 自定义匹配符
  10. 捐款
  11. 许可证

@ codemod/ 匹配器

JavaScript & TypeScript 编码器的匹配器.

安装

从 无数字 :

$ npm install @codemod/matchers

使用量

本软件包主要用于编码器 @codemod/cli,但可以在任何 Babel 插件或 JavaScript/ TypeScript AST 处理器中使用. 请注意,以下例子均载于 类型脚本,但在大多数情况下,与它们的JavaScript对应的相同.

使用此软件包的最简单方法是使用 @codemod/cli 像这样:

// `t` is `@babel/types`
import { defineCodemod, t } from '@codemod/cli'

// `m` is the `@codemod/matchers` module
export default defineCodemod(({ m }) => ({
  visitors: {
    // …
  },
}))

其余的 README 显示如何使用 @codemod/matchers 无 @codemod/cli,但同样的模式适用.

简单匹配

正如您可以构建 AST 节点 @babel/types您可以构建 AST 节点 匹配一个精确的节点 @codemod/matchers数字 :

import * as m from '@codemod/matchers'
import * as t from '@babel/types'

// `matcher` only matches Identifier nodes named 'test'
const matcher = m.identifier('test')

matcher.match(t.identifier('test')) // true
matcher.match(t.identifier('test2')) // false

模糊匹配

然而,匹配精确的节点通常不是你想要的. @codemod/matchers 可以在仅指定部分数据的地方构建匹配器:

import * as m from '@codemod/matchers'
import * as t from '@babel/types'

// `matcher` matches any Identifier, regardless of name
const matcher = m.identifier()

matcher.match(t.identifier('test')) // true
matcher.match(t.identifier('test2')) // true
matcher.match(t.emptyStatement()) // false

这里有一个比较复杂的例子 匹配任何 console.log 打电话 假设 expr 将联署材料描述为:

import * as m from '@codemod/matchers'

// `matcher` matches any `console.log(…)` call
const matcher = m.callExpression(
  m.memberExpression(m.identifier('console'), m.identifier('log'), false)
  // `arguments` is omitted to match anything, or we could pass `m.anything()`
)

matcher.match(expr('console.log()')) // true
matcher.match(expr('console.log(1, 2)')) // true
matcher.match(expr('console.log')) // false

有各种各样的模糊的相配者 @codemod/matchers数字 :

import * as m from '@codemod/matchers'

m.anyString().match('a string') // true
m.anyString().match(1) // false

m.anyNumber().match(1) // true
m.anyNumber().match('a string') // false

m.anything().match(1) // true
m.anything().match('a string') // true
m.anything().match(expr('foo')) // true
m.anything().match(null) // true

m.anyNode().match(expr('a + b')) // true
m.anyNode().match(expr('!a')) // true
m.anyNode().match(1) // false
m.anyNode().match('a string') // false

配置匹配

通常你会想捕捉你所匹配的部分节点,以便从中提取信息或编辑信息.

import * as m from '@codemod/matchers'

// matches `console.<consoleMethod>(…)` calls
const consoleMethod = m.capture(m.identifier())
const matcher = m.callExpression(
  m.memberExpression(m.identifier('console'), consoleMethod, false)
  // `arguments` is omitted to match anything, or we could pass `m.anything()`
)

if (matcher.match(expr('console.log()'))) {
  console.log(`found console call: ${consoleMethod.current.name}`)
}

if (matcher.match(expr('console.group("hi!")'))) {
  console.log(`found console call: ${consoleMethod.current.name}`)
}

if (matcher.match(expr('notAConsoleCall()'))) {
  console.log(`found console call: ${consoleMethod.current.name}`)
}

// logs:
//   found console call: log
//   found console call: group

反引用

有时你会想在选手的后期部分提及一个被捕获的早期值. 例如,让我们假设您想要匹配一个函数表达式,该函数表达式返回它的参数:

import * as m from '@codemod/matchers'

const argumentMatcher = m.capture(m.identifier())
const matcher = m.functionExpression(
  m.anything(),
  [argumentMatcher],
  m.blockStatement([m.returnStatement(m.fromCapture(argumentMatcher))])
)

matcher.match(expr('function(a) { return a; })')) // true
matcher.match(expr('function id(a) { return a; })')) // true
matcher.match(expr('function(a) { return b; })')) // false
matcher.match(expr('function(a) { return 1; })')) // false
matcher.match(expr('function(a) { return a + a; })')) // false

在编码器中使用

所有之前的例子都有匹配者测试特定的AST节点. 这对插图很有用,但通常你不会用它。 编码器为 @codemod/cli 为: Babel 插件 因此,使用访问者模式处理AST。 下面是上面的例子,它可以识别那些只做不做而再次返回其参数的函数,这次是一个 Babel 插件,它用全局替换了这些函数 IDENTITY 参考文献 :

/**
 * Replaces identity functions with `IDENTITY`:
 *
 *   list.filter(function(a) { return a; });
 *
 * becomes:
 *
 *   list.filter(IDENTITY);
 */
import * as m from '@codemod/matchers'
import * as t from '@babel/types'
import { NodePath } from '@babel/traverse'

export default function () {
  return {
    visitor: {
      FunctionExpression(path: NodePath<t.FunctionExpression>): void {
        const argumentMatcher = m.capture(m.identifier())
        const matcher = m.functionExpression(
          m.anything(),
          [argumentMatcher],
          m.blockStatement([m.returnStatement(m.fromCapture(argumentMatcher))])
        )

        if (matcher.match(path.node)) {
          path.replaceWith(t.identifier('IDENTITY'))
        }
      },
    },
  }
}

这里又是一个没有使用过的插件 @codemod/matchers数字 :

/**
 * Replaces identity functions with `IDENTITY`:
 *
 *   list.filter(function(a) { return a; });
 *
 * becomes:
 *
 *   list.filter(IDENTITY);
 */
import * as t from '@babel/types'
import { NodePath } from '@babel/traverse'

export default function () {
  return {
    visitor: {
      /**
       * This version of the codemod, which does not use `@codemod/matchers`,
       * is more verbose and more likely to have subtle bugs. For example,
       * it's easy to forget to check that the `return` statement actually has a
       * return value before checking that it is an identifier, which would
       * result in a crash.
       */
      FunctionExpression(path: NodePath<t.FunctionExpression>): void {
        // ensure function has exactly one parameter
        if (path.node.params.length !== 1) {
          return
        }

        // ensure parameter is an identifier
        const param = path.node.params[0]
        if (!t.isIdentifier(param)) {
          return
        }

        // ensure function body has exactly one statement
        if (path.node.body.body.length !== 1) {
          return
        }

        // ensure that statement is a return statement
        const statement = path.node.body.body[0]
        if (!t.isReturnStatement(statement)) {
          return
        }

        // ensure the return actually returns something, an identifier
        if (!statement.argument || !t.isIdentifier(statement.argument)) {
          return
        }

        // ensure returned identifier has same name as the param
        if (statement.argument.name !== param.name) {
          return
        }

        // replace!
        path.replaceWith(t.identifier('IDENTITY'))
      },
    },
  }
}

深度匹配

有时候你知道你想匹配一个节点,但不知道它在树上深度,因此无法将整个匹配树进行硬码. 为了应付这种情况,你可以使用 containerOf 选手。 例如,这个匹配者会找到第一个 done 在摩卡测试中调用, 计算参数可能使用的名称 :

import * as m from '@codemod/matchers'

const doneParam = m.capture(m.identifier())
const matcher = m.callExpression(m.identifier('test'), [
  m.anyString(),
  m.function(
    [doneParam],
    m.containerOf(m.callExpression(m.fromCapture(doneParam)))
  ),
])

// matches because there's a `done()` call
matcher.match(
  expr(`
  test('setTimeout calls back around N ms later', function(done) {
    const now = Date.now();
    const duration = 5;

    setTimeout(function() {
      assert.ok(Date.now() - now < 10);
      done();
    }, duration);
  });
`)
)

// does not match because there's no `done()` call
matcher.match(
  expr(`
  test('adds things', function() {
    assert.strictEqual(3 + 4, 7);
  });
`)
)

自定义匹配符

构建自定义匹配器最简单的方法就是编组出现有的匹配器:

import * as m from '@codemod/matchers'
import * as t from '@babel/types'

function plusEqualOne() {
  return m.assignmentExpression(
    '+=',
    m.anything(), // or just `undefined` for the same effect
    m.numericLiteral(1)
  )
}

const matcher = plusEqualOne()

matcher.match(expr('a += 1')) // true
matcher.match(expr('a.b += 1')) // true
matcher.match(expr('a -= 1')) // false
matcher.match(expr('a += 2')) // false

您可以轻松使用上游构建简单的自定义匹配器 :

import * as m from '@codemod/matchers'

const oddNumberMatcher = m.matcher(
  (value) => typeof value === 'number' && Math.abs(number % 2) === 1
)

oddNumberMatcher.match(expr('-1')) // true
oddNumberMatcher.match(expr('0')) // false
oddNumberMatcher.match(expr('1')) // true
oddNumberMatcher.match(expr('2')) // true
oddNumberMatcher.match(expr('3')) // true
oddNumberMatcher.match(expr('Infinity')) // false
oddNumberMatcher.match(expr('NaN')) // false

这种匹配器很容易通过将它包入函数来参数化:

import * as m from '@codemod/matchers';

function stringMatching(pattern: RegExp) {
  return m.matcher(
    value => typeof value === 'string' && pattern.test(value)
  );
)

const startsWithRun = stringMatching(/^run/);

startsWithRun.match('run');     // true
startsWithRun.match('runner');  // true
startsWithRun.match('running'); // true
startsWithRun.match('ruining'); // false
startsWithRun.match(' run');    // false
startsWithRun.match('');        // false
startsWithRun.match(1);         // false

一个常见的情况是,你认为你需要一个自定义匹配者,就是当你想要一些可能的值之一的时候. 在这种情况下,您可以使用 or 匹配者 :

import * as m from '@codemod/matchers'

const matcher = m.or(m.anyString(), m.anyNumber())

matcher.match(1) // true
matcher.match('string') // true
matcher.match({}) // false
matcher.match(expr('1')) // false

在处理函数等事项时,匹配少数数值之一是常见的,这些函数可以是箭头函数,函数表达式,或函数声明. 这里有一个更普通的版本 IDENTITY 使用 or 匹配器,用于替换箭头函数 :

/**
 * Replaces identity functions with `IDENTITY`:
 *
 *   list.filter(function(a) { return a; });
 *   list2.filter(a => a);
 *
 * becomes:
 *
 *   list.filter(IDENTITY);
 *   list2.filter(IDENTITY);
 */
import * as m from '@codemod/matchers'
import * as t from '@babel/types'
import { NodePath } from '@babel/traverse'

export default function () {
  return {
    visitor: {
      FunctionExpression(path: NodePath<t.FunctionExpression>): void {
        const paramId = m.capture(m.identifier())
        const matcher = m.function(
          [paramId],
          m.or(
            m.blockStatement([m.returnStatement(m.fromCapture(paramId))]),
            m.fromCapture(paramId)
          )
        )

        if (matcher.match(path.node)) {
          path.replaceWith(t.identifier('IDENTITY'))
        }
      },
    },
  }
}

你可能不需要它, 但你可以自己建造 通过子分类 Matcher这里也是 stringMatching 但作为一个子类 Matcher数字 :

import * as m from '@codemod/matchers'
import * as t from '@babel/types'

// This is more ceremony than the simple predicate-based one above.
class StringMatching extends m.Matcher<string> {
  constructor(private readonly pattern: RegExp) {
    super()
  }

  matchValue(
    value: unknown,
    keys: ReadonlyArray<PropertyKey>
  ): value is string {
    return typeof value === 'string' && this.pattern.test(value)
  }
}

const startsWithRun = new StringMatching(/^run/)

startsWithRun.match('run') // true
startsWithRun.match('runner') // true
startsWithRun.match('running') // true
startsWithRun.match('ruining') // false
startsWithRun.match(' run') // false
startsWithRun.match('') // false
startsWithRun.match(1) // false

捐款

见 捐款额 (c) 提供关于设立发展项目和为项目作贡献的信息.

许可证

版权 2019 Brian Donovan

根据 Apache 许可证, 版本 2. 0 (“ License ”) 获得许可; 您不能使用此文件, 除非符合许可证 。 您可在下列地点获得许可证副本:

http://www.apache.org/licenses/LICENSE-2.0

除非适用的法律要求或书面同意,根据许可证分发的软件在 " AS IS " BASIS上分发,没有任何明示或默示的WARANTIES或CONDITION。 见《许可证》关于许可和许可证限制的具体语言.

版本与发布记录

@codemod/matchers 共发布 24 个版本,首次发布于 2019/02/09;过去 12 个月发布了 0 个版本。

版本发布日期距今
1.7.1latest2024/03/112 年前
1.7.02023/04/293 年前
1.6.12023/02/253 年前
1.6.02023/02/193 年前
1.5.02023/02/183 年前
1.4.02023/02/173 年前

关于 @codemod/matchers 的常见问题

@codemod/matchers 是什么?

JavaScript & TypeScript 编码器的匹配器.

如何安装 @codemod/matchers?

在项目目录运行 npm install @codemod/matchers,或使用 pnpm add / yarn add / bun add。

@codemod/matchers 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@codemod/matchers 支持 ESM 还是 CommonJS?

以 CommonJS 形式发布,可以直接 require();在 ES Module 项目中也可以用 import 引入(Node.js 会把 module.exports 作为默认导出)。

@codemod/matchers 可以商用吗?

@codemod/matchers 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@codemod/matchers 的下载量有多少?

根据 npm 官方下载统计,@codemod/matchers 在 2026/10/10 – 2026/11/08 这 30 天内被下载 79,213 次,日均约 2,640 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@codemod/matchers 还在维护吗?

最新版本 1.7.1 发布于 2024/03/11(2 年前);至今共发布 24 个版本,其中过去 12 个月发布了 0 个;首次发布于 2019/02/09。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。