我使用哪种访问者方法来提取类调用 - intl.formatMessage(它真的是 CallExpression)吗?
是的,它是一个CallExpression
,与函数调用相比,方法调用没有特殊的 AST 节点,唯一改变的是接收者(被调用者)。每当您想知道 AST 是什么样子时,您都可以使用神奇的AST Explorer。作为奖励,您甚至可以通过在 Transform 菜单中选择 Babel 在 AST Explorer 中编写 Babel 插件。
如何检测对 formatMessage 的调用?
为简洁起见,我将只关注对 的确切调用intl.formatMessage(arg)
,对于真正的插件,您还需要涵盖其他intl["formatMessage"](arg)
具有不同 AST 表示的情况(例如)。
首先要确定被调用者是intl.formatMessage
. 如您所知,那是一个简单的对象属性访问,对应的 AST 节点称为MemberExpression
. 访问者接收到匹配的 AST 节点,CallExpression
在本例中为path.node
。这意味着我们需要验证它path.node.callee
是一个MemberExpression
. 值得庆幸的是,这很简单,因为以AST 节点类型babel.types
的形式提供了方法。isX
X
if (t.isMemberExpression(path.node.callee)) {}
现在我们知道它是一个MemberExpression
,它有一个object
和property
对应于object.property
。所以我们可以检查是否object
是标识符intl
和property
标识符formatMessage
。为此,我们使用isIdentifier(node, opts)
,它接受第二个参数,允许您检查它是否具有具有给定值的属性。所有isX
方法都采用这种形式来提供快捷方式,有关详细信息,请参阅检查节点是否为某种类型。他们还会检查节点是否不是null
or undefined
,因此从isMemberExpression
技术上讲这不是必需的,但您可能希望以不同的方式处理另一种类型。
if (
t.isIdentifier(path.node.callee.object, { name: "intl" }) &&
t.isIdentifier(path.node.callee.property, { name: "formatMessage" })
) {}
如何检测调用中的参数数量?(如果有格式化,则不应该发生替换)
有CallExpression
一个arguments
属性,它是参数的 AST 节点的数组。同样,为简洁起见,我只会考虑只有一个参数的调用,但实际上你也可以转换类似intl.formatMessage(arg, undefined)
. 在这种情况下,它只是检查path.node.arguments
. 我们还希望参数是一个对象,所以我们检查一个ObjectExpression
.
if (
path.node.arguments.length === 1 &&
t.isObjectExpression(path.node.arguments[0])
) {}
AnObjectExpression
有一个properties
属性,它是一个ObjectProperty
节点数组。您可以从技术上检查这id
是唯一的属性,但我将在这里跳过它,而只查找一个id
属性。有ObjectProperty
一个key
and value
,我们可以使用它Array.prototype.find()
来搜索以标识符为键的属性id
。
const idProp = path.node.arguments[0].properties.find(prop =>
t.isIdentifier(prop.key, { name: "id" })
);
idProp
如果存在则为对应ObjectProperty
,否则为undefined
. 如果不是undefined
,我们要替换节点。
我如何更换?(intl.formatMessage({ id: 'something' }) 到 intl.messages['something'] ?
我们要替换整个CallExpression
和 Babel 提供的path.replaceWith(node)
. 剩下的唯一一件事就是创建应该替换它的 AST 节点。为此,我们首先需要了解intl.messages["section.someid"]
AST 中的表示方式。intl.messages
是一个MemberExpression
就像intl.formatMessage
是。是一个计算属性对象访问,在 ASTobj["property"]
中也表示为 a ,但属性设置为. 这意味着以a为对象的 a。MemberExpression
computed
true
intl.messages["section.someid"]
MemberExpression
MemberExpression
请记住,这两个在语义上是等价的:
intl.messages["section.someid"];
const msgs = intl.messages;
msgs["section.someid"];
要构造一个MemberExpression
我们可以使用t.memberExpression(object, property, computed, optional)
. 对于创建intl.messages
,我们可以重用intl
from path.node.callee.object
,因为我们想使用相同的对象,但要更改属性。对于属性,我们需要创建一个Identifier
名称为messages
。
t.memberExpression(path.node.callee.object, t.identifier("messages"))
只有前两个参数是必需的,其余的我们使用默认值(false
forcomputed
和null
for 可选)。现在我们可以使用它MemberExpression
作为对象,我们需要查找与属性true
值相对应的计算属性(第三个参数设置为 ),该id
属性在idProp
我们之前计算的值中可用。最后我们用CallExpression
新创建的节点替换节点。
if (idProp) {
path.replaceWith(
t.memberExpression(
t.memberExpression(
path.node.callee.object,
t.identifier("messages")
),
idProp.value,
// Is a computed property
true
)
);
}
完整代码:
export default function({ types: t }) {
return {
visitor: {
CallExpression(path) {
// Make sure it's a method call (obj.method)
if (t.isMemberExpression(path.node.callee)) {
// The object should be an identifier with the name intl and the
// method name should be an identifier with the name formatMessage
if (
t.isIdentifier(path.node.callee.object, { name: "intl" }) &&
t.isIdentifier(path.node.callee.property, { name: "formatMessage" })
) {
// Exactly 1 argument which is an object
if (
path.node.arguments.length === 1 &&
t.isObjectExpression(path.node.arguments[0])
) {
// Find the property id on the object
const idProp = path.node.arguments[0].properties.find(prop =>
t.isIdentifier(prop.key, { name: "id" })
);
if (idProp) {
// When all of the above was true, the node can be replaced
// with an array access. An array access is a member
// expression with a computed value.
path.replaceWith(
t.memberExpression(
t.memberExpression(
path.node.callee.object,
t.identifier("messages")
),
idProp.value,
// Is a computed property
true
)
);
}
}
}
}
}
}
};
}
完整的代码和一些测试用例可以在这个 AST Explorer Gist中找到。
正如我多次提到的,这是一个幼稚的版本,很多情况都没有涵盖,可以进行转换。覆盖更多案例并不难,但您必须识别它们并将它们粘贴到 AST Explorer 中,这将为您提供所需的所有信息。例如,如果对象 is{ "id": "section.someid" }
而不是{ id: "section.someid" }
它不会被转换,但覆盖它就像检查 aStringLiteral
除了a 一样简单Identifier
,如下所示:
const idProp = path.node.arguments[0].properties.find(prop =>
t.isIdentifier(prop.key, { name: "id" }) ||
t.isStringLiteral(prop.key, { value: "id" })
);
我也没有故意引入任何抽象来避免额外的认知负担,因此条件看起来很长。
有用的资源: