Convert close from positional to kwargs

If the close() call has 0 args, or a single unlabeled arg, the parser
interprets it as a CallExpression (positional) not a CallExpressionKw.

But then if a codemod wants to add a tag to it, it tries adding a kwarg
called 'tag', which fails because the CallExpression doesn't need
kwargs inserted into it.

The fix is: change the node from CallExpression to CallExpressionKw, and
update getNodeFromPath to take a 'replacement' arg, so we can replace
the old node with the new node in the AST.
This commit is contained in:
Adam Chalmers
2025-01-15 13:59:57 -06:00
committed by Nick Cameron
parent cd022fb087
commit 99866b5f9a
2 changed files with 42 additions and 5 deletions

View File

@ -51,7 +51,8 @@ export function getNodeFromPath<T>(
node: Program,
path: PathToNode,
stopAt?: SyntaxType | SyntaxType[],
returnEarly = false
returnEarly = false,
replacement?: any
):
| {
node: T
@ -63,9 +64,14 @@ export function getNodeFromPath<T>(
let stopAtNode = null
let successfulPaths: PathToNode = []
let pathsExplored: PathToNode = []
let parent = null as any
let parentEdge = null
for (const pathItem of path) {
if (typeof currentNode[pathItem[0]] !== 'object') {
if (stopAtNode) {
if (replacement && parent && parentEdge) {
parent[parentEdge] = replacement
}
return {
node: stopAtNode,
shallowPath: pathsExplored,
@ -74,6 +80,8 @@ export function getNodeFromPath<T>(
}
return new Error('not an object')
}
parent = currentNode
parentEdge = pathItem[0]
currentNode = currentNode?.[pathItem[0]]
successfulPaths.push(pathItem)
if (!stopAtNode) {
@ -97,6 +105,9 @@ export function getNodeFromPath<T>(
}
}
}
if (replacement && parent && parentEdge) {
parent[parentEdge] = replacement
}
return {
node: stopAtNode || currentNode,
shallowPath: pathsExplored,

View File

@ -2466,15 +2466,30 @@ function addTag(tagIndex = 2): addTagFn {
function addTagKw(): addTagFn {
return ({ node, pathToNode }) => {
const _node = { ...node }
const callExpr = getNodeFromPath<Node<CallExpressionKw>>(
// We have to allow for the possibility that the path is actually to a call expression.
// That's because if the parser reads something like `close()`, it doesn't know if this
// is a keyword or positional call.
// In fact, even something like `close(%)` could be either (because we allow 1 unlabeled
// starting param).
const callExpr = getNodeFromPath<Node<CallExpressionKw | CallExpression>>(
_node,
pathToNode,
'CallExpressionKw'
['CallExpressionKw', 'CallExpression']
)
if (err(callExpr)) return callExpr
const { node: primaryCallExp } = callExpr
// If the original node is a call expression, we'll need to change it to a call with keyword args.
const primaryCallExp: CallExpressionKw =
callExpr.node.type === 'CallExpressionKw'
? callExpr.node
: {
type: 'CallExpressionKw',
callee: callExpr.node.callee,
unlabeled: callExpr.node.arguments.length
? callExpr.node.arguments[0]
: null,
arguments: [],
}
const tagArg = findKwArg(ARG_TAG, primaryCallExp)
const tagDeclarator =
tagArg || createTagDeclarator(findUniqueName(_node, 'seg', 2))
@ -2483,6 +2498,17 @@ function addTagKw(): addTagFn {
const labeledArg = createLabeledArg(ARG_TAG, tagDeclarator)
primaryCallExp.arguments.push(labeledArg)
}
// If we changed the node, we must replace the old node with the new node in the AST.
const mustReplaceNode = primaryCallExp.type !== callExpr.node.type
if (mustReplaceNode) {
getNodeFromPath(_node, pathToNode, ['CallExpression'], false, {
...primaryCallExp,
start: callExpr.node.start,
end: callExpr.node.end,
})
}
if ('value' in tagDeclarator) {
// Now TypeScript knows tagDeclarator has a value property
return {