UNPKG

jsctags

Version:
290 lines (251 loc) 8.5 kB
// Based on defnode.js (https://github.com/sourcegraph/defnode.js) const walk = require('acorn/dist/walk'); const walkall = require('./walkall'); // FindDefinitionNode takes the start/end position of an Identifier node and // returns the definition node (in ast) corresponding to the definition that // the Identifier identifies. For a FunctionDeclaration name, this is the // FunctionDeclaration; for a VariableDeclaration variable name, this is the // VariableDeclarator init; for ObjectExpression keys, this is the // corresponding value; for function parameters, this is itself; and for all // other inputs, this is undefined. // // This mapping is intended to correspond to what tern considers the identifier // of a given definition. I.e., if you use tern to ask for the definition of // some symbol, and it gives you an Identifier, you can use findDefinitionNode // to find the definition value of the Identifier. exports.findDefinitionNode = function(ast, start, end) { const origin = exports.findOriginPseudonode(ast, start, end); if (!origin) { return; } if (!origin.type && origin.key && origin.value && origin.kind) { // ObjectExpression property return origin.value; } switch (origin.type) { case 'AssignmentExpression': return rightmostExprOfAssignment(origin); case 'Identifier': return origin; case 'VariableDeclarator': return origin.init && rightmostExprOfAssignment(origin.init); default: return origin; } }; // FindNameNodes finds the Identifier AST node(s) corresponding to a definition // node (in ast). // // This mapping is intended to correspond to the mapping between a tern defs // JSON file !span and the names/paths of keys pointing to that !span. exports.findNameNodes = function(ast, start, end) { let def = walk.findNodeAt(ast, start, end, null, walkall.traversers); if (!def) { throw new Error( 'No definition node found at position ' + start + '-' + end ); } def = def.node; // When we search for the enclosing node, we don't want to just end up with // the def node itself, so exclude it (node != def). const test = function(type, node) { return ( (def.type === 'FunctionDeclaration' && type === 'FunctionDeclaration') || (node !== def && [ 'AssignmentExpression', 'FunctionDeclaration', 'FunctionExpression', 'ObjectExpression', 'VariableDeclarator' ].indexOf(type) !== -1) ); }; let enc = walk.findNodeAround(ast, end, test, walkall.traversers); if (!enc) { throw new Error( 'No enclosing declaration node found for definition at position ' + end ); } enc = enc.node; switch (enc.type) { case 'AssignmentExpression': // We only want to consider Identifiers and MemberExpression properties as // name nodes for this definition. Anything else is not really a receiver // of this definition. return collectChainedAssignmentNames(ast, enc); case 'FunctionDeclaration': case 'FunctionExpression': if (enc === def) { return def.id && [def.id]; } if (enc.params.indexOf(def) !== -1) { // The def is a function param return [def]; } break; case 'ObjectExpression': const prop = findPropInObjectExpressionByValuePos(enc, start, end); if (!prop) { throw new Error( 'No property found for ObjectExpression value at position ' + start + '-' + end ); } return [prop.key]; case 'VariableDeclarator': return [enc.id]; } }; // FindOriginPseudonode finds the AST node or node-like object of the // declaration/definition that encloses the Identifier AST node with the // specified start/end. // // This function returns ObjectExpression property objects if the Identifier is // an ObjectExpression property key. These objects are not true AST nodes (thus // the "pseudonode" description). exports.findOriginPseudonode = function(ast, start, end) { let nameNode = walk.findNodeAt( ast, start, end, okNodeTypes(['Identifier', 'Literal']), walkall.traversers ); if (!nameNode) { throw new Error('No name node found at position ' + start + '-' + end); } nameNode = nameNode.node; // Find enclosing decl-like node let enc = walk.findNodeAround( ast, end, okNodeTypes([ 'AssignmentExpression', 'FunctionDeclaration', 'FunctionExpression', 'ObjectExpression', 'VariableDeclarator' ]), walkall.traversers ); if (!enc) { throw new Error( 'No enclosing declaration node found for Identifier at position ' + start + '-' + end ); } enc = enc.node; switch (enc.type) { case 'AssignmentExpression': // We only want to consider this assignment a definition if our name node // is the LHS of the AssignmentExpression, or the property of the // AssignmentExpression's LHS MemberExpression. Otherwise, we're not really // defining something with this ident. if ( enc.left === nameNode || (enc.left.type === 'MemberExpression' && identOrLiteralString(enc.left.property) === identOrLiteralString(nameNode)) ) { return enc; } break; case 'FunctionDeclaration': case 'FunctionExpression': if (enc.id === nameNode) { // The ident is the function name return enc; } if (enc.params.indexOf(nameNode) !== -1) { // The ident is a function param return nameNode; } break; case 'ObjectExpression': return findPropInObjectExpressionByKeyPos(enc, start, end); case 'VariableDeclarator': return enc; } }; function okNodeTypes(types) { return function(_t) { return types.indexOf(_t) !== -1; }; } function findPropInObjectExpressionByKeyPos(objectExpr, start, end) { for (let i = 0; i < objectExpr.properties.length; ++i) { const prop = objectExpr.properties[i]; if (prop.key.start === start && prop.key.end === end) { return prop; } } } function findPropInObjectExpressionByValuePos(objectExpr, start, end) { for (let i = 0; i < objectExpr.properties.length; ++i) { const prop = objectExpr.properties[i]; if (prop.value.start === start && prop.value.end === end) { return prop; } } } // RightmostExprOfAssignment follows chained AssignmentExpressions to the rightmost // expression. E.g., given the AssignmentExpression AST node of `a = b = c = // 7`, it returns the Literal value 7 on the far right. function rightmostExprOfAssignment(assignmentExpr) { while (assignmentExpr.type === 'AssignmentExpression') { assignmentExpr = assignmentExpr.right; } return assignmentExpr; } function collectChainedAssignmentNames(ast, expr, seen) { const names = []; if (expr.type === 'VariableDeclarator') { names.push(expr.id); } else if (expr.left.type === 'Identifier') { names.push(expr.left); } else if ( expr.left.type === 'MemberExpression' && identOrLiteralString(expr.left.property) ) { names.push(expr.left.property); } // Avoid infinite loop (AssignmentExpr and VariableDeclarator have same end pos). if (!seen) { seen = []; } seen.push(expr); // Traverse to parent AssignmentExpressions to return all names in chained assignments. const test = function(type, node) { return ( seen.indexOf(node) === -1 && ((type === 'AssignmentExpression' && node.right === expr) || (type === 'VariableDeclarator' && node.init === expr)) ); }; const outer = walk.findNodeAround(ast, expr.end, test, walkall.traversers); if (outer) { names.push.apply( names, collectChainedAssignmentNames(ast, outer.node, seen) ); } return names; } // IdentOrLiteralString takes an AST node whose type is either Identifier or // Literal and returns the Identifier name or Literal string value. It is // useful when you have a name node in an ObjectExpression property or // MemberExpression property, which could be either an Identifier or Literal, // and you just want to extract the string name. const identOrLiteralString = (exports.identOrLiteralString = function(n) { if (n.type === 'Identifier') { return n.name; } if (n.type === 'Literal' && typeof n.value === 'string') { return n.value; } });