@ 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。 见《许可证》关于许可和许可证限制的具体语言.