lib/visitIdentifierNodes.js
const KEYS_USED_FOR_ASSIGNMENT = new Set(['id', 'imported', 'local', 'params']);
const KEYS_USED_IN_REFERENCE_TO_OBJECTS = new Set(['property']);
function normalizeNode(node, context) {
const { key, parent } = context;
if (!parent) {
return undefined;
}
if (node.type === 'JSXIdentifier') {
if (key !== 'name' && key !== 'object') {
return undefined;
}
if (
parent.type === 'JSXOpeningElement' ||
(parent.type === 'JSXMemberExpression' &&
parent.parent.type === 'JSXOpeningElement')
) {
return {
name: node.name,
isJSX: true,
context,
};
}
}
if (parent.type === 'GenericTypeAnnotation') {
if (!node.name) {
return undefined;
}
// flow
return {
name: node.name,
context,
};
}
if (node.type !== 'Identifier') {
return undefined;
}
if (parent.type === 'ExportSpecifier') {
if (key === 'exported') {
// "bar" in `export { foo from bar }`
return undefined;
}
if (key === 'local') {
// "foo" in `export { foo from bar }`
return {
name: node.name,
context,
};
}
}
const isAssignment =
KEYS_USED_FOR_ASSIGNMENT.has(key) ||
(key === 'key' && parent.parent.type === 'ObjectPattern') ||
(key === 'left' && parent.type === 'AssignmentPattern') ||
(key === 'elements' && parent.type === 'ArrayPattern') ||
(key === 'argument' && parent.type === 'RestElement') ||
(key === 'value' &&
parent.parent.type === 'ObjectPattern' &&
parent.parent.parent.type === 'VariableDeclarator');
if (isAssignment) {
context.definedInScope.add(node.name);
}
const isReference =
KEYS_USED_IN_REFERENCE_TO_OBJECTS.has(key) ||
(key === 'key' &&
!parent.computed &&
parent.parent.type !== 'ObjectPattern');
return {
isReference,
isAssignment,
context,
name: node.name,
};
}
export default function visitIdentifierNodes(
rootAstNode,
visitor,
context = { definedInScope: new Set([]), key: 'root' },
) {
const queue = [{ node: rootAstNode, context }];
let current;
while (queue.length) {
current = queue.shift();
if (!current.node) {
continue;
}
if (Array.isArray(current.node)) {
if (current.context.key === 'body') {
// A new scope has started. Copy whatever we have from the parent scope
// into a new one.
current.context.definedInScope = new Set([
...current.context.definedInScope,
]);
}
const itemsToAdd = current.node.map((node) => ({
node,
context: current.context,
}));
queue.unshift(...itemsToAdd);
continue;
}
const normalizedNode = normalizeNode(current.node, current.context);
if (normalizedNode) {
visitor(normalizedNode, current.context);
}
const itemsToAdd = [];
Object.keys(current.node).forEach((key) => {
if (!current.node[key] || typeof current.node[key] !== 'object') {
return;
}
const newContext = {
...current.context,
key,
parent: {
type: current.node.type,
parent: current.context.parent,
computed: current.node.computed,
},
};
const itemToPush = {
node: current.node[key],
context: newContext,
};
if (key === 'body') {
// Delay traversing function bodies, so that we can finish finding all
// defined variables in scope first.
queue.push(itemToPush);
} else {
itemsToAdd.push(itemToPush);
}
});
queue.unshift(...itemsToAdd);
}
}