?
Quelle méthode de visiteur dois-je utiliser pour extraire les classes appels - intl.formatMessage (est-ce vraiment CallExpression)
Oui, il est un CallExpression
, il n'y a pas de noeud AST spécial pour un appel de méthode par rapport à? un appel de fonction, la seule chose qui change est le récepteur (appelé) .Quand vous vous demandez à quoi ressemble l'AST, vous pouvez utiliser le fantastique AST Explorer. En prime, vous pouvez même écrire le plugin Babel dans AST Explorer en sélectionnant Babel dans le menu Transform.
Comment puis-je détecter un appel à formatMessage?
Par souci de concision, je ne l'accent sur l'appel exact intl.formatMessage(arg)
, pour un vrai plug-in dont vous auriez besoin pour couvrir d'autres cas aussi bien (par exemple intl["formatMessage"](arg)
) qui ont une autre représentation AST.
La première chose est d'identifier que l'appelé est intl.formatMessage
. Comme vous le savez, il s'agit d'un simple accès aux propriétés d'un objet, et le noeud AST correspondant est appelé MemberExpression
. Le visiteur reçoit le nœud AST correspondant, CallExpression
dans ce cas, comme path.node
. Cela signifie que nous devons vérifier que path.node.callee
est un MemberExpression
. Heureusement, c'est assez simple, car babel.types
fournit des méthodes sous la forme isX
où X
est le type de nœud AST.
if (t.isMemberExpression(path.node.callee)) {}
Maintenant, nous savons que c'est un MemberExpression
, qui a une object
et un property
qui correspondent à object.property
. Nous pouvons donc vérifier si object
est l'identifiant intl
et property
l'identifiant formatMessage
. Pour cela, nous utilisons isIdentifier(node, opts)
, qui prend un second argument qui vous permet de vérifier qu'il a une propriété avec la valeur donnée. Toutes les méthodes isX
sont de cette forme pour fournir un raccourci, pour plus de détails, voir Check if a node is a certain type. Ils vérifient également que le nœud n'est pas null
ou undefined
, donc le isMemberExpression
n'était pas techniquement nécessaire, mais vous pourriez vouloir gérer un autre type différemment.
if (
t.isIdentifier(path.node.callee.object, { name: "intl" }) &&
t.isIdentifier(path.node.callee.property, { name: "formatMessage" })
) {}
Comment puis-je détecter le nombre de paramètres dans l'appel? (Le remplacement n'est pas censé se produire s'il y a la mise en forme)
Le CallExpression
a une propriété arguments
, qui est un tableau des nœuds AST des arguments. Encore une fois, par souci de brièveté, je ne considérerai les appels qu'avec exactement un argument, mais en réalité, vous pourriez aussi transformer quelque chose comme intl.formatMessage(arg, undefined)
. Dans ce cas, il vérifie simplement la longueur de path.node.arguments
. Nous voulons également que l'argument soit un objet, donc nous vérifions un ObjectExpression
.
if (
path.node.arguments.length === 1 &&
t.isObjectExpression(path.node.arguments[0])
) {}
Un ObjectExpression
a une propriété properties
, qui est un tableau de ObjectProperty
noeuds. Vous pouvez vérifier techniquement que id
est la seule propriété, mais je vais sauter cela ici et à la place seulement chercher une propriété id
. Le ObjectProperty
a un key
et value
, et nous pouvons utiliser Array.prototype.find()
pour rechercher la propriété avec la clé étant l'identificateur id
.
const idProp = path.node.arguments[0].properties.find(prop =>
t.isIdentifier(prop.key, { name: "id" })
);
idProp
sera le ObjectProperty
correspondant si elle existe, sinon il sera undefined
. Quand ce n'est pas undefined
, nous voulons remplacer le nœud.
Comment faire le remplacement? (Intl.formatMessage ({id: 'quelque chose'}?.) À intl.messages [ 'quelque chose']
Nous voulons remplacer l'ensemble CallExpression
et Babel fournit path.replaceWith(node)
La seule chose qui reste, crée l'AST Pour cela, nous devons d'abord comprendre comment intl.messages["section.someid"]
est représenté dans l'AST est un MemberExpression
comme était obj["property"]
est un accès aux objets de propriété calculée, qui est également représenté comme un MemberExpression
dans l'AST , mais avec la propriété computed
définie sur true
Cela signifie que intl.messages["section.someid"]
est un MemberExpression
avec un MemberExpression
comme objet.
Rappelez-vous que ces deux sont sémantiquement équivalentes:
intl.messages["section.someid"];
const msgs = intl.messages;
msgs["section.someid"];
Pour construire un MemberExpression
nous pouvons utiliser t.memberExpression(object, property, computed, optional)
. Pour créer intl.messages
nous pouvons réutiliser le intl
de path.node.callee.object
car nous voulons utiliser le même objet, mais changez la propriété. Pour la propriété, nous devons créer un Identifier
avec le nom messages
.
t.memberExpression(path.node.callee.object, t.identifier("messages"))
Seuls les deux premiers arguments sont nécessaires et pour le reste, nous utilisons les valeurs par défaut (pour false
computed
en option et null
). Maintenant, nous pouvons utiliser cet MemberExpression
comme objet et nous devons rechercher la propriété calculée (le troisième argument est true
) qui correspond à la valeur de la propriété id
, qui est disponible sur le idProp
que nous avons calculé précédemment. Et enfin, nous remplaçons le nœud CallExpression
par le nœud nouvellement créé.
if (idProp) {
path.replaceWith(
t.memberExpression(
t.memberExpression(
path.node.callee.object,
t.identifier("messages")
),
idProp.value,
// Is a computed property
true
)
);
}
code complet:
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
)
);
}
}
}
}
}
}
};
}
Le code complet et certains cas de test peuvent être trouvés dans this AST Explorer Gist.
Comme je l'ai mentionné à quelques reprises, c'est une version naïve et de nombreux cas ne sont pas couverts, qui sont admissibles à la transformation. Il n'est pas difficile de couvrir plus de cas, mais vous devez les identifier et les coller dans l'AST Explorer vous donnera toutes les informations dont vous avez besoin. Par exemple, si l'objet est { "id": "section.someid" }
au lieu de { id: "section.someid" }
il ne sera pas transformé, mais couvrant c'est aussi simple que de vérifier aussi un StringLiteral
en plus d'une Identifier
, comme ceci:
const idProp = path.node.arguments[0].properties.find(prop =>
t.isIdentifier(prop.key, { name: "id" }) ||
t.isStringLiteral(prop.key, { value: "id" })
);
Je n'ai pas présenté aussi abstractions exprès pour éviter la charge cognitive supplémentaire, donc les conditions semblent très longues.
Ressources utiles:
Michael, je vous remercie beaucoup pour la meilleure réponse. Je vais essayer aujourd'hui. :) –
Merci encore une fois. Vous n'avez pas non plus aidé à résoudre mon problème, appris à faire des plugins mais aussi probablement aidé d'autres utilisateurs de reac-intl. J'ai partagé votre réponse ici dans le fichier reac-intl github https://github.com/yahoo/react-intl/issues/1044 –