484 lines
14 KiB
JavaScript
484 lines
14 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
"use strict";
|
|
// These are all the current lezer node types used in the source editor,
|
|
// Add more here as are needed
|
|
const nodeTypes = {
|
|
AssignmentExpression: "AssignmentExpression",
|
|
FunctionExpression: "FunctionExpression",
|
|
FunctionDeclaration: "FunctionDeclaration",
|
|
ArrowFunction: "ArrowFunction",
|
|
MethodDeclaration: "MethodDeclaration",
|
|
ClassDeclaration: "ClassDeclaration",
|
|
ClassExpression: "ClassExpression",
|
|
Property: "Property",
|
|
PropertyDeclaration: "PropertyDeclaration",
|
|
PropertyDefinition: "PropertyDefinition",
|
|
PrivatePropertyDefinition: "PrivatePropertyDefinition",
|
|
MemberExpression: "MemberExpression",
|
|
VariableDeclaration: "VariableDeclaration",
|
|
VariableDefinition: "VariableDefinition",
|
|
VariableName: "VariableName",
|
|
this: "this",
|
|
PropertyName: "PropertyName",
|
|
Equals: "Equals",
|
|
ParamList: "ParamList",
|
|
Spread: "Spread",
|
|
Number: "Number",
|
|
};
|
|
|
|
const functionsSet = new Set([
|
|
nodeTypes.FunctionExpression,
|
|
nodeTypes.FunctionDeclaration,
|
|
nodeTypes.ArrowFunction,
|
|
nodeTypes.MethodDeclaration,
|
|
]);
|
|
|
|
const nodeTypeSets = {
|
|
functions: functionsSet,
|
|
expressions: new Set([
|
|
nodeTypes.MemberExpression,
|
|
nodeTypes.VariableDefinition,
|
|
nodeTypes.VariableName,
|
|
nodeTypes.this,
|
|
nodeTypes.PropertyName,
|
|
]),
|
|
functionExpressions: new Set([
|
|
nodeTypes.ArrowFunction,
|
|
nodeTypes.FunctionExpression,
|
|
nodeTypes.ParamList,
|
|
]),
|
|
declarations: new Set([
|
|
nodeTypes.MethodDeclaration,
|
|
nodeTypes.PropertyDeclaration,
|
|
]),
|
|
functionsDeclAndExpr: new Set([
|
|
...functionsSet,
|
|
nodeTypes.Property,
|
|
nodeTypes.PropertyDeclaration,
|
|
nodeTypes.VariableDeclaration,
|
|
nodeTypes.AssignmentExpression,
|
|
]),
|
|
functionsVarDecl: new Set([
|
|
...functionsSet,
|
|
// For anonymous functions we are using the variable name where the function is stored. See `getFunctionName`.
|
|
nodeTypes.VariableDeclaration,
|
|
]),
|
|
paramList: new Set([nodeTypes.ParamList]),
|
|
variableDefinition: new Set([nodeTypes.VariableDefinition]),
|
|
numberAndProperty: new Set([nodeTypes.PropertyDefinition, nodeTypes.Number]),
|
|
memberExpression: new Set([nodeTypes.MemberExpression]),
|
|
classes: new Set([nodeTypes.ClassDeclaration, nodeTypes.ClassExpression]),
|
|
bindingReferences: new Set([
|
|
nodeTypes.VariableDefinition,
|
|
nodeTypes.VariableName,
|
|
]),
|
|
expressionProperty: new Set([nodeTypes.PropertyName]),
|
|
};
|
|
|
|
const ast = new Map();
|
|
|
|
/**
|
|
* Checks if a node has children with any of the node types specified
|
|
*
|
|
* @param {Object} node
|
|
* @param {Set} types
|
|
* @returns
|
|
*/
|
|
function hasChildNodeOfType(node, types) {
|
|
let childNode = node.firstChild;
|
|
while (childNode !== null) {
|
|
if (types.has(childNode.name)) {
|
|
return true;
|
|
}
|
|
childNode = childNode.nextSibling;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Checks if a node has children with any of the node types specified
|
|
*
|
|
* @param {Object} node
|
|
* @param {Set} types
|
|
* @returns
|
|
*/
|
|
function findChildNodeOfType(node, types) {
|
|
let childNode = node.firstChild;
|
|
while (childNode !== null) {
|
|
if (types.has(childNode.name)) {
|
|
return childNode;
|
|
}
|
|
childNode = childNode.nextSibling;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Gets a cached tree or parses the the source content
|
|
*
|
|
* @param {Object} parserLanguage - The language parser used to parse the source
|
|
* @param {String} id - A unique identifier for the source
|
|
* @param {String} content - The source text
|
|
* @returns {Tree} - https://lezer.codemirror.net/docs/ref/#common.Tree
|
|
*/
|
|
function getTree(parserLanguage, id, content) {
|
|
if (ast.has(id)) {
|
|
return ast.get(id);
|
|
}
|
|
const tree = parserLanguage.parser.parse(content);
|
|
ast.set(id, tree);
|
|
return tree;
|
|
}
|
|
|
|
function clear() {
|
|
ast.clear();
|
|
}
|
|
|
|
/**
|
|
* Gets the node and the function name which immediately encloses the node (representing a location)
|
|
*
|
|
* @param {Object} doc - The codemirror document used to retrive the part of content
|
|
* @param {Object} node - The parser syntax node https://lezer.codemirror.net/docs/ref/#common.SyntaxNode
|
|
* @params {Object} options
|
|
* options.includeAnonymousFunctions - if true, allow matching anonymous functions
|
|
* @returns
|
|
*/
|
|
function getEnclosingFunction(
|
|
doc,
|
|
node,
|
|
options = { includeAnonymousFunctions: false }
|
|
) {
|
|
let parentNode = node.parent;
|
|
while (parentNode !== null) {
|
|
if (nodeTypeSets.functionsVarDecl.has(parentNode.name)) {
|
|
// For anonymous functions, we use variable declarations, but we only care about variable declarations which are part of function expressions
|
|
if (
|
|
parentNode.name == nodeTypes.VariableDeclaration &&
|
|
!hasChildNodeOfType(parentNode.node, nodeTypeSets.functionExpressions)
|
|
) {
|
|
parentNode = parentNode.parent;
|
|
continue;
|
|
}
|
|
const funcName = getFunctionName(doc, parentNode);
|
|
if (funcName || options.includeAnonymousFunctions) {
|
|
return {
|
|
node: parentNode,
|
|
funcName,
|
|
};
|
|
}
|
|
}
|
|
parentNode = parentNode.parent;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Gets the node at the specified location
|
|
*
|
|
* @param {Object} doc - https://codemirror.net/docs/ref/#state.EditorState.doc
|
|
* @param {Object} tree - https://lezer.codemirror.net/docs/ref/#common.Tree
|
|
* @param {Object} location
|
|
* @returns {Object} node - https://lezer.codemirror.net/docs/ref/#common.SyntaxNodeRef
|
|
*/
|
|
function getTreeNodeAtLocation(doc, tree, location) {
|
|
try {
|
|
const line = doc.line(location.line);
|
|
const pos = line.from + location.column;
|
|
return tree.resolve(pos, 1);
|
|
} catch (e) {
|
|
// if the line is not found in the document doc.line() will throw
|
|
console.warn(e.message);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Converts Codemirror position to valid source location. Used only for CM6
|
|
*
|
|
* @param {Object} doc - The Codemirror document used to retrive the part of content
|
|
* @param {Number} pos - Codemirror offset
|
|
* @returns
|
|
*/
|
|
function positionToLocation(doc, pos) {
|
|
if (pos == null) {
|
|
return {
|
|
line: null,
|
|
column: null,
|
|
};
|
|
}
|
|
const line = doc.lineAt(pos);
|
|
return {
|
|
line: line.number,
|
|
column: pos - line.from,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Gets the name of the function if any exists, returns null
|
|
* for anonymous functions.
|
|
*
|
|
* @param {Object} doc - The codemirror document used to retrive the part of content
|
|
* @param {Object} node - The parser syntax node https://lezer.codemirror.net/docs/ref/#common.SyntaxNode
|
|
* @returns {String|null}
|
|
*/
|
|
function getFunctionName(doc, node) {
|
|
/**
|
|
* Examples:
|
|
* - Gets `foo` in `class ESClass { foo(a, b) {}}`
|
|
* - Gets `bar` in `class ESClass { bar = function () {}}`
|
|
* - Gets `boo` in `class ESClass { boo = () => {}}`
|
|
* - Gets `#pfoo` in `class ESClass { #pfoo() => {}}`
|
|
*/
|
|
if (
|
|
node.name == nodeTypes.MethodDeclaration ||
|
|
(node.name == nodeTypes.PropertyDeclaration &&
|
|
hasChildNodeOfType(node, nodeTypeSets.functionExpressions))
|
|
) {
|
|
const propDefNode = findChildNodeOfType(
|
|
node,
|
|
new Set([
|
|
nodeTypes.PropertyDefinition,
|
|
nodeTypes.PrivatePropertyDefinition,
|
|
])
|
|
);
|
|
|
|
if (!propDefNode) {
|
|
return null;
|
|
}
|
|
return doc.sliceString(propDefNode.from, propDefNode.to);
|
|
} else if (
|
|
/**
|
|
* Examples:
|
|
* - Gets `foo` in `let foo = function () {};`
|
|
* - Gets `bar` in `const bar = () => {}`
|
|
*/
|
|
node.name == nodeTypes.VariableDeclaration &&
|
|
hasChildNodeOfType(node, nodeTypeSets.functionExpressions)
|
|
) {
|
|
const varDefNode = findChildNodeOfType(
|
|
node,
|
|
nodeTypeSets.variableDefinition
|
|
);
|
|
|
|
if (!varDefNode) {
|
|
return null;
|
|
}
|
|
return doc.sliceString(varDefNode.from, varDefNode.to);
|
|
} else if (
|
|
/**
|
|
* Examples:
|
|
* - Gets `Foo` in `function Foo() {} - FunctionDeclaration`
|
|
* - Gets `bar` in `function bar(a) {} - Functionexpression`
|
|
*/
|
|
node.name == nodeTypes.FunctionDeclaration ||
|
|
node.name == nodeTypes.FunctionExpression
|
|
) {
|
|
const varDefNode = findChildNodeOfType(
|
|
node,
|
|
nodeTypeSets.variableDefinition
|
|
);
|
|
|
|
if (!varDefNode) {
|
|
return null;
|
|
}
|
|
return doc.sliceString(varDefNode.from, varDefNode.to);
|
|
} else if (
|
|
/**
|
|
* Examples:
|
|
* - Gets `foo` in `const a = { foo(a, ...b) {} }`
|
|
* - Gets `bar` in `const a = { bar: function () {} }`
|
|
* - Gets `bla` in `const a = { bla: () => {} }`
|
|
* - Gets `1234` in `const a = { 1234: () => {} }`
|
|
*/
|
|
node.name == nodeTypes.Property &&
|
|
hasChildNodeOfType(node, nodeTypeSets.functionExpressions)
|
|
) {
|
|
const propDefNode = findChildNodeOfType(
|
|
node,
|
|
nodeTypeSets.numberAndProperty
|
|
);
|
|
|
|
if (!propDefNode) {
|
|
return null;
|
|
}
|
|
return doc.sliceString(propDefNode.from, propDefNode.to);
|
|
} else if (
|
|
/**
|
|
* Examples:
|
|
* - Gets `bar` in `const foo = {}; foo.bar = function() {}`
|
|
* - Gets `bla` in `const foo = {}; foo.bla = () => {}`
|
|
*/
|
|
node.name == nodeTypes.AssignmentExpression &&
|
|
hasChildNodeOfType(node, nodeTypeSets.functionExpressions)
|
|
) {
|
|
const memExprDefNode = findChildNodeOfType(
|
|
node,
|
|
nodeTypeSets.memberExpression
|
|
);
|
|
|
|
if (!memExprDefNode) {
|
|
return null;
|
|
}
|
|
// Get the rightmost part of the member expression i.e for a.b.c get c
|
|
const exprParts = doc
|
|
.sliceString(memExprDefNode.from, memExprDefNode.to)
|
|
.split(".");
|
|
return exprParts.at(-1);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Gets the parameter names of the function as an array
|
|
*
|
|
* @param {Object} doc - The codemirror document used to retrieve the part of content
|
|
* @param {Object} node - The parser syntax node https://lezer.codemirror.net/docs/ref/#common.SyntaxNode
|
|
* @returns {Array}
|
|
*/
|
|
function getFunctionParameterNames(doc, node) {
|
|
// Find the parameter list node
|
|
|
|
let exprNode = node;
|
|
|
|
if (
|
|
// Example: Gets `(a)` in `const foo = {}; foo.bar = function(a) {}`
|
|
node.name == nodeTypes.AssignmentExpression ||
|
|
// Example: Gets `(a, b)` in `let foo = function (a, b) {};`
|
|
node.name == nodeTypes.VariableDeclaration ||
|
|
// Example: Gets `(x, y)` in `class ESClass { bar = function (x, y) {}}`
|
|
node.name == nodeTypes.PropertyDeclaration ||
|
|
// Example: Gets `(foo, ...bar)` in `const a = { foo: (foo, ...bar) {}}`
|
|
(node.name == nodeTypes.Property &&
|
|
!hasChildNodeOfType(node, nodeTypeSets.paramList))
|
|
) {
|
|
exprNode = findChildNodeOfType(node, nodeTypeSets.functionExpressions);
|
|
}
|
|
|
|
/**
|
|
* Others
|
|
* Function Declarations - Gets `(x, y)` in `function Foo(x, y) {}`
|
|
* Method Declarations - Gets `(a, b)` in `class ESClass { foo(a, b) {}}`
|
|
*/
|
|
const paramListNode = findChildNodeOfType(exprNode, nodeTypeSets.paramList);
|
|
if (paramListNode == null) {
|
|
return [];
|
|
}
|
|
|
|
const names = [];
|
|
let currNode = paramListNode.firstChild; // "("
|
|
// Get all the parameter names
|
|
while (currNode !== null && currNode.name !== ")") {
|
|
if (currNode.name == nodeTypes.VariableDefinition) {
|
|
// ignore spread operators i.e foo(...x)
|
|
if (currNode.prevSibling?.name !== nodeTypes.Spread) {
|
|
names.push(doc.sliceString(currNode.from, currNode.to));
|
|
}
|
|
}
|
|
currNode = currNode.nextSibling;
|
|
}
|
|
return names;
|
|
}
|
|
|
|
function getFunctionClass(doc, node) {
|
|
/**
|
|
* Examples (Class Methods and Properties):
|
|
* Gets `ESClass` in `class ESClass { foo(a, b) {}}`
|
|
* Gets `ESClass` in `class ESClass { bar = function () {}}`
|
|
*/
|
|
if (!nodeTypeSets.declarations.has(node.name)) {
|
|
return null;
|
|
}
|
|
return doc.sliceString(
|
|
node.parent.prevSibling.from,
|
|
node.parent.prevSibling.to
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Gets the meta data for member expression nodes
|
|
*
|
|
* @param {Object} doc - The codemirror document used to retrieve the part of content
|
|
* @param {Object} node - The parser syntax node https://lezer.codemirror.net/docs/ref/#common.SyntaxNode
|
|
* @returns
|
|
*/
|
|
function getMetaBindings(doc, node) {
|
|
if (!node || node.name !== nodeTypes.MemberExpression) {
|
|
return null;
|
|
}
|
|
|
|
const memExpr = doc.sliceString(node.from, node.to).split(".");
|
|
return {
|
|
type: "member",
|
|
start: positionToLocation(doc, node.from),
|
|
end: positionToLocation(doc, node.to),
|
|
property: memExpr.at(-1),
|
|
parent: getMetaBindings(doc, node.parent),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Walk the syntax tree of the langauge provided
|
|
*
|
|
* @param {Object} view - Codemirror view (https://codemirror.net/docs/ref/#view)
|
|
* @param {Object} language - Codemirror Language (https://codemirror.net/docs/ref/#language)
|
|
* @param {Object} options
|
|
* {Boolean} options.forceParseTo - Force parsing the document up to a certain point
|
|
* {Function} options.enterVisitor - A function that is called when a node is entered
|
|
* {Set} options.filterSet - A set of node types which should be visited, all others should be ignored
|
|
* {Number} options.walkFrom - Determine the location in the AST where the iteration of the syntax tree should start
|
|
* {Number} options.walkTo - Determine the location in the AST where the iteration of the syntax tree should end
|
|
*/
|
|
async function walkTree(view, language, options) {
|
|
const { forceParsing, syntaxTree } = language;
|
|
if (options.forceParseTo) {
|
|
// Force parsing the source, up to the end of the current viewport,
|
|
// Also increasing the timeout threshold so we make sure
|
|
// all required content is parsed (this is mostly needed for larger sources).
|
|
await forceParsing(view, options.forceParseTo, 10000);
|
|
}
|
|
await syntaxTree(view.state).iterate({
|
|
enter: node => {
|
|
if (options.filterSet?.has(node.name)) {
|
|
options.enterVisitor(node);
|
|
}
|
|
},
|
|
from: options.walkFrom,
|
|
to: options.walkTo,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* This enables walking a specific part of the syntax tree using the cursor
|
|
* provided by the node (which is the parent)
|
|
* @param {Object} cursor - https://lezer.codemirror.net/docs/ref/#common.TreeCursor
|
|
* @param {Object} options
|
|
* {Function} options.enterVisitor - A function that is called when a node is entered
|
|
* {Set} options.filterSet - A set of node types which should be visited, all others should be ignored
|
|
*/
|
|
async function walkCursor(cursor, options) {
|
|
await cursor.iterate(node => {
|
|
if (options.filterSet?.has(node.name)) {
|
|
options.enterVisitor(node);
|
|
}
|
|
});
|
|
}
|
|
|
|
module.exports = {
|
|
getFunctionName,
|
|
getFunctionParameterNames,
|
|
getFunctionClass,
|
|
getEnclosingFunction,
|
|
getTreeNodeAtLocation,
|
|
getMetaBindings,
|
|
nodeTypes,
|
|
nodeTypeSets,
|
|
walkTree,
|
|
getTree,
|
|
clear,
|
|
walkCursor,
|
|
positionToLocation,
|
|
};
|