lezer-utils.js (16987B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 // These are all the current lezer node types used in the source editor, 7 // Add more here as are needed 8 const nodeTypes = { 9 AssignmentExpression: "AssignmentExpression", 10 FunctionExpression: "FunctionExpression", 11 FunctionDeclaration: "FunctionDeclaration", 12 ArrowFunction: "ArrowFunction", 13 MethodDeclaration: "MethodDeclaration", 14 ClassDeclaration: "ClassDeclaration", 15 ClassExpression: "ClassExpression", 16 Property: "Property", 17 PropertyDeclaration: "PropertyDeclaration", 18 PropertyDefinition: "PropertyDefinition", 19 PrivatePropertyDefinition: "PrivatePropertyDefinition", 20 MemberExpression: "MemberExpression", 21 VariableDeclaration: "VariableDeclaration", 22 VariableDefinition: "VariableDefinition", 23 VariableName: "VariableName", 24 this: "this", 25 PropertyName: "PropertyName", 26 Equals: "Equals", 27 ParamList: "ParamList", 28 Spread: "Spread", 29 Number: "Number", 30 Script: "Script", 31 Block: "Block", 32 }; 33 34 const functionsSet = new Set([ 35 nodeTypes.FunctionExpression, 36 nodeTypes.FunctionDeclaration, 37 nodeTypes.ArrowFunction, 38 nodeTypes.MethodDeclaration, 39 ]); 40 41 const nodeTypeSets = { 42 functions: functionsSet, 43 expressions: new Set([ 44 nodeTypes.MemberExpression, 45 nodeTypes.VariableDefinition, 46 nodeTypes.VariableName, 47 nodeTypes.this, 48 nodeTypes.PropertyName, 49 ]), 50 functionExpressions: new Set([ 51 nodeTypes.ArrowFunction, 52 nodeTypes.FunctionExpression, 53 nodeTypes.ParamList, 54 ]), 55 declarations: new Set([ 56 nodeTypes.MethodDeclaration, 57 nodeTypes.PropertyDeclaration, 58 ]), 59 functionsDeclAndExpr: new Set([ 60 ...functionsSet, 61 nodeTypes.Property, 62 nodeTypes.PropertyDeclaration, 63 nodeTypes.VariableDeclaration, 64 nodeTypes.AssignmentExpression, 65 ]), 66 functionsVarDecl: new Set([ 67 ...functionsSet, 68 // For anonymous functions we are using the variable name where the function is stored. See `getFunctionName`. 69 nodeTypes.VariableDeclaration, 70 ]), 71 paramList: new Set([nodeTypes.ParamList]), 72 variableDefinition: new Set([nodeTypes.VariableDefinition]), 73 numberAndProperty: new Set([nodeTypes.PropertyDefinition, nodeTypes.Number]), 74 memberExpression: new Set([nodeTypes.MemberExpression]), 75 classes: new Set([nodeTypes.ClassDeclaration, nodeTypes.ClassExpression]), 76 bindingReferences: new Set([ 77 nodeTypes.VariableDefinition, 78 nodeTypes.VariableName, 79 nodeTypes.PropertyName, 80 ]), 81 expressionProperty: new Set([nodeTypes.PropertyName]), 82 }; 83 84 const ast = new Map(); 85 86 /** 87 * Checks if a node has children with any of the node types specified 88 * 89 * @param {object} node 90 * @param {Set} types 91 * @returns 92 */ 93 function hasChildNodeOfType(node, types) { 94 let childNode = node.firstChild; 95 while (childNode !== null) { 96 if (types.has(childNode.name)) { 97 return true; 98 } 99 childNode = childNode.nextSibling; 100 } 101 return false; 102 } 103 104 /** 105 * Checks if a node has children with any of the node types specified 106 * 107 * @param {object} node 108 * @param {Set} types 109 * @returns 110 */ 111 function findChildNodeOfType(node, types) { 112 let childNode = node.firstChild; 113 while (childNode !== null) { 114 if (types.has(childNode.name)) { 115 return childNode; 116 } 117 childNode = childNode.nextSibling; 118 } 119 return null; 120 } 121 122 /** 123 * Gets a cached tree or parses the the source content 124 * 125 * @param {object} parserLanguage - The language parser used to parse the source 126 * @param {string} id - A unique identifier for the source 127 * @param {string} content - The source text 128 * @returns {Tree} - https://lezer.codemirror.net/docs/ref/#common.Tree 129 */ 130 function getTree(parserLanguage, id, content) { 131 if (ast.has(id)) { 132 return ast.get(id); 133 } 134 const tree = parserLanguage.parser.parse(content); 135 ast.set(id, tree); 136 return tree; 137 } 138 139 function clear() { 140 ast.clear(); 141 } 142 143 /** 144 * Gets the node and the function name which immediately encloses the node (representing a location) 145 * 146 * @param {object} doc - The codemirror document used to retrive the part of content 147 * @param {object} node - The parser syntax node https://lezer.codemirror.net/docs/ref/#common.SyntaxNode 148 * @param {object} options 149 * @param {boolean} options.includeAnonymousFunctions - if true, allow matching anonymous functions 150 * @returns 151 */ 152 function getEnclosingFunction( 153 doc, 154 node, 155 options = { includeAnonymousFunctions: false } 156 ) { 157 let parentNode = node.parent; 158 while (parentNode !== null) { 159 if (nodeTypeSets.functionsVarDecl.has(parentNode.name)) { 160 // For anonymous functions, we use variable declarations, but we only care about variable declarations which are part of function expressions 161 if ( 162 parentNode.name == nodeTypes.VariableDeclaration && 163 !hasChildNodeOfType(parentNode.node, nodeTypeSets.functionExpressions) 164 ) { 165 parentNode = parentNode.parent; 166 continue; 167 } 168 const funcName = getFunctionName(doc, parentNode); 169 if (funcName || options.includeAnonymousFunctions) { 170 return { 171 node: parentNode, 172 funcName, 173 }; 174 } 175 } 176 parentNode = parentNode.parent; 177 } 178 return null; 179 } 180 181 /** 182 * Gets the parent scope node for the specified node. 183 * If neither is found then we fallback to the script node for the source. 184 * 185 * @param {object} node 186 * @param {string} scopeType - The scope type specifies what kind of scope node to look for. 187 * The types are defined by the platform. See https://firefox-source-docs.mozilla.org/js/Debugger/Debugger.Environment.html#type 188 * @returns {object | null} scope node or null if none is found 189 */ 190 function getParentScopeOfType(node, scopeType) { 191 let parentNode = node.parent; 192 let lastParentNode = parentNode; 193 while (parentNode !== null) { 194 if (scopeType == "block" || scopeType == "object") { 195 if (parentNode.name == nodeTypes.Block) { 196 return parentNode; 197 } 198 } else if (nodeTypeSets.functionsVarDecl.has(parentNode.name)) { 199 if ( 200 parentNode.name == nodeTypes.VariableDeclaration && 201 !hasChildNodeOfType(parentNode.node, nodeTypeSets.functionExpressions) 202 ) { 203 parentNode = parentNode.parent; 204 continue; 205 } 206 return parentNode; 207 } 208 lastParentNode = parentNode; 209 parentNode = parentNode.parent; 210 } 211 // If no function node was found up to the root node 212 if (lastParentNode?.name == nodeTypes.Script) { 213 return lastParentNode; 214 } 215 return null; 216 } 217 218 /** 219 * Gets the node at the specified location 220 * 221 * @param {object} doc - https://codemirror.net/docs/ref/#state.EditorState.doc 222 * @param {object} tree - https://lezer.codemirror.net/docs/ref/#common.Tree 223 * @param {object} location 224 * @returns {object} node - https://lezer.codemirror.net/docs/ref/#common.SyntaxNodeRef 225 */ 226 function getTreeNodeAtLocation(doc, tree, location) { 227 try { 228 const line = doc.line(location.line); 229 const pos = line.from + location.column; 230 return tree.resolve(pos, 1); 231 } catch (e) { 232 // if the line is not found in the document doc.line() will throw 233 console.warn(e.message); 234 } 235 return null; 236 } 237 238 /** 239 * Converts Codemirror position to valid source location. Used only for CM6 240 * 241 * @param {object} doc - The Codemirror document used to retrive the part of content 242 * @param {number} pos - Codemirror offset 243 * @returns 244 */ 245 function positionToLocation(doc, pos) { 246 if (pos == null) { 247 return { 248 line: null, 249 column: null, 250 }; 251 } 252 const line = doc.lineAt(pos); 253 return { 254 line: line.number, 255 column: pos - line.from, 256 }; 257 } 258 259 /** 260 * Gets the name of the function if any exists, returns null 261 * for anonymous functions. 262 * 263 * @param {object} doc - The codemirror document used to retrive the part of content 264 * @param {object} node - The parser syntax node https://lezer.codemirror.net/docs/ref/#common.SyntaxNode 265 * @returns {string | null} 266 */ 267 function getFunctionName(doc, node) { 268 /** 269 * Examples: 270 * - Gets `foo` in `class ESClass { foo(a, b) {}}` 271 * - Gets `bar` in `class ESClass { bar = function () {}}` 272 * - Gets `boo` in `class ESClass { boo = () => {}}` 273 * - Gets `#pfoo` in `class ESClass { #pfoo() => {}}` 274 */ 275 if ( 276 node.name == nodeTypes.MethodDeclaration || 277 (node.name == nodeTypes.PropertyDeclaration && 278 hasChildNodeOfType(node, nodeTypeSets.functionExpressions)) 279 ) { 280 const propDefNode = findChildNodeOfType( 281 node, 282 new Set([ 283 nodeTypes.PropertyDefinition, 284 nodeTypes.PrivatePropertyDefinition, 285 ]) 286 ); 287 288 if (!propDefNode) { 289 return null; 290 } 291 return doc.sliceString(propDefNode.from, propDefNode.to); 292 } else if ( 293 /** 294 * Examples: 295 * - Gets `foo` in `let foo = function () {};` 296 * - Gets `bar` in `const bar = () => {}` 297 */ 298 node.name == nodeTypes.VariableDeclaration && 299 hasChildNodeOfType(node, nodeTypeSets.functionExpressions) 300 ) { 301 const varDefNode = findChildNodeOfType( 302 node, 303 nodeTypeSets.variableDefinition 304 ); 305 306 if (!varDefNode) { 307 return null; 308 } 309 return doc.sliceString(varDefNode.from, varDefNode.to); 310 } else if ( 311 /** 312 * Examples: 313 * - Gets `Foo` in `function Foo() {} - FunctionDeclaration` 314 * - Gets `bar` in `function bar(a) {} - Functionexpression` 315 */ 316 node.name == nodeTypes.FunctionDeclaration || 317 node.name == nodeTypes.FunctionExpression 318 ) { 319 const varDefNode = findChildNodeOfType( 320 node, 321 nodeTypeSets.variableDefinition 322 ); 323 324 if (!varDefNode) { 325 return null; 326 } 327 return doc.sliceString(varDefNode.from, varDefNode.to); 328 } else if ( 329 /** 330 * Examples: 331 * - Gets `foo` in `const a = { foo(a, ...b) {} }` 332 * - Gets `bar` in `const a = { bar: function () {} }` 333 * - Gets `bla` in `const a = { bla: () => {} }` 334 * - Gets `1234` in `const a = { 1234: () => {} }` 335 */ 336 node.name == nodeTypes.Property && 337 hasChildNodeOfType(node, nodeTypeSets.functionExpressions) 338 ) { 339 const propDefNode = findChildNodeOfType( 340 node, 341 nodeTypeSets.numberAndProperty 342 ); 343 344 if (!propDefNode) { 345 return null; 346 } 347 return doc.sliceString(propDefNode.from, propDefNode.to); 348 } else if ( 349 /** 350 * Examples: 351 * - Gets `bar` in `const foo = {}; foo.bar = function() {}` 352 * - Gets `bla` in `const foo = {}; foo.bla = () => {}` 353 */ 354 node.name == nodeTypes.AssignmentExpression && 355 hasChildNodeOfType(node, nodeTypeSets.functionExpressions) 356 ) { 357 const memExprDefNode = findChildNodeOfType( 358 node, 359 nodeTypeSets.memberExpression 360 ); 361 362 if (!memExprDefNode) { 363 return null; 364 } 365 // Get the rightmost part of the member expression i.e for a.b.c get c 366 const exprParts = doc 367 .sliceString(memExprDefNode.from, memExprDefNode.to) 368 .split("."); 369 return exprParts.at(-1); 370 } 371 372 return null; 373 } 374 375 /** 376 * Gets the parameter names of the function as an array 377 * 378 * @param {object} doc - The codemirror document used to retrieve the part of content 379 * @param {object} node - The parser syntax node https://lezer.codemirror.net/docs/ref/#common.SyntaxNode 380 * @returns {Array} 381 */ 382 function getFunctionParameterNames(doc, node) { 383 // Find the parameter list node 384 385 let exprNode = node; 386 387 if ( 388 // Example: Gets `(a)` in `const foo = {}; foo.bar = function(a) {}` 389 node.name == nodeTypes.AssignmentExpression || 390 // Example: Gets `(a, b)` in `let foo = function (a, b) {};` 391 node.name == nodeTypes.VariableDeclaration || 392 // Example: Gets `(x, y)` in `class ESClass { bar = function (x, y) {}}` 393 node.name == nodeTypes.PropertyDeclaration || 394 // Example: Gets `(foo, ...bar)` in `const a = { foo: (foo, ...bar) {}}` 395 (node.name == nodeTypes.Property && 396 !hasChildNodeOfType(node, nodeTypeSets.paramList)) 397 ) { 398 exprNode = findChildNodeOfType(node, nodeTypeSets.functionExpressions); 399 } 400 401 /** 402 * Others 403 * Function Declarations - Gets `(x, y)` in `function Foo(x, y) {}` 404 * Method Declarations - Gets `(a, b)` in `class ESClass { foo(a, b) {}}` 405 */ 406 const paramListNode = findChildNodeOfType(exprNode, nodeTypeSets.paramList); 407 if (paramListNode == null) { 408 return []; 409 } 410 411 const names = []; 412 let currNode = paramListNode.firstChild; // "(" 413 // Get all the parameter names 414 while (currNode !== null && currNode.name !== ")") { 415 if (currNode.name == nodeTypes.VariableDefinition) { 416 // ignore spread operators i.e foo(...x) 417 if (currNode.prevSibling?.name !== nodeTypes.Spread) { 418 names.push(doc.sliceString(currNode.from, currNode.to)); 419 } 420 } 421 currNode = currNode.nextSibling; 422 } 423 return names; 424 } 425 426 function getFunctionClass(doc, node) { 427 /** 428 * Examples (Class Methods and Properties): 429 * Gets `ESClass` in `class ESClass { foo(a, b) {}}` 430 * Gets `ESClass` in `class ESClass { bar = function () {}}` 431 */ 432 if (!nodeTypeSets.declarations.has(node.name)) { 433 return null; 434 } 435 return doc.sliceString( 436 node.parent.prevSibling.from, 437 node.parent.prevSibling.to 438 ); 439 } 440 441 /** 442 * Gets the meta data for member expression nodes 443 * 444 * @param {object} doc - The codemirror document used to retrieve the part of content 445 * @param {object} node - The parser syntax node https://lezer.codemirror.net/docs/ref/#common.SyntaxNode 446 * @returns 447 */ 448 function getMetaBindings(doc, node) { 449 if (!node || node.name !== nodeTypes.MemberExpression) { 450 return null; 451 } 452 453 const memExpr = doc.sliceString(node.from, node.to).split("."); 454 return { 455 type: "member", 456 start: positionToLocation(doc, node.from), 457 end: positionToLocation(doc, node.to), 458 property: memExpr.at(-1), 459 parent: getMetaBindings(doc, node.parent), 460 }; 461 } 462 463 /** 464 * Walk the syntax tree of the langauge provided 465 * 466 * @param {object} view - Codemirror view (https://codemirror.net/docs/ref/#view) 467 * @param {object} language - Codemirror Language (https://codemirror.net/docs/ref/#language) 468 * @param {object} options 469 * {Boolean} options.forceParseTo - Force parsing the document up to a certain point 470 * {Function} options.enterVisitor - A function that is called when a node is entered 471 * {Set} options.filterSet - A set of node types which should be visited, all others should be ignored 472 * {Number} options.walkFrom - Determine the location in the AST where the iteration of the syntax tree should start 473 * {Number} options.walkTo - Determine the location in the AST where the iteration of the syntax tree should end 474 */ 475 async function walkTree(view, language, options) { 476 const { forceParsing, syntaxTree } = language; 477 if (options.forceParseTo) { 478 // Force parsing the source, up to the end of the current viewport, 479 // Also increasing the timeout threshold so we make sure 480 // all required content is parsed (this is mostly needed for larger sources). 481 await forceParsing(view, options.forceParseTo, 10000); 482 } 483 await syntaxTree(view.state).iterate({ 484 enter: node => { 485 if (options.filterSet?.has(node.name)) { 486 options.enterVisitor(node); 487 } 488 }, 489 from: options.walkFrom, 490 to: options.walkTo, 491 }); 492 } 493 494 /** 495 * This enables walking a specific part of the syntax tree using the cursor 496 * provided by the node (which is the parent) 497 * 498 * @param {object} cursor - https://lezer.codemirror.net/docs/ref/#common.TreeCursor 499 * @param {object} options 500 * {Function} options.enterVisitor - A function that is called when a node is entered 501 * {Set} options.filterSet - A set of node types which should be visited, all others should be ignored 502 */ 503 async function walkCursor(cursor, options) { 504 await cursor.iterate(node => { 505 if (options.filterSet?.has(node.name)) { 506 options.enterVisitor(node); 507 } 508 }); 509 } 510 511 /** 512 * Merge variables, arguments and child properties of member expressions 513 * into a unique "bindings" objects where arguments overrides variables. 514 * 515 * @param {object} scopeBindings 516 * @returns {object} bindings 517 */ 518 function getScopeBindings(scopeBindings) { 519 const bindings = { ...scopeBindings.variables }; 520 scopeBindings.arguments.forEach(argument => { 521 Object.keys(argument).forEach(key => { 522 bindings[key] = argument[key]; 523 }); 524 }); 525 // Find and add child properties of member expressions as bindings 526 for (const v in scopeBindings.variables) { 527 const ownProps = scopeBindings.variables[v]?.value?.preview?.ownProperties; 528 if (ownProps) { 529 Object.keys(ownProps).forEach(k => { 530 bindings[k] = ownProps[k]; 531 }); 532 } 533 } 534 return bindings; 535 } 536 537 module.exports = { 538 getFunctionName, 539 getFunctionParameterNames, 540 getFunctionClass, 541 getEnclosingFunction, 542 getTreeNodeAtLocation, 543 getMetaBindings, 544 nodeTypes, 545 nodeTypeSets, 546 walkTree, 547 getTree, 548 clear, 549 walkCursor, 550 positionToLocation, 551 getParentScopeOfType, 552 getScopeBindings, 553 };