1/* @internal */ 2namespace ts.NavigationBar { 3 /** 4 * Matches all whitespace characters in a string. Eg: 5 * 6 * "app. 7 * 8 * onactivated" 9 * 10 * matches because of the newline, whereas 11 * 12 * "app.onactivated" 13 * 14 * does not match. 15 */ 16 const whiteSpaceRegex = /\s+/g; 17 18 /** 19 * Maximum amount of characters to return 20 * The amount was chosen arbitrarily. 21 */ 22 const maxLength = 150; 23 24 // Keep sourceFile handy so we don't have to search for it every time we need to call `getText`. 25 let curCancellationToken: CancellationToken; 26 let curSourceFile: SourceFile; 27 28 /** 29 * For performance, we keep navigation bar parents on a stack rather than passing them through each recursion. 30 * `parent` is the current parent and is *not* stored in parentsStack. 31 * `startNode` sets a new parent and `endNode` returns to the previous parent. 32 */ 33 let parentsStack: NavigationBarNode[] = []; 34 let parent: NavigationBarNode; 35 36 const trackedEs5ClassesStack: (ESMap<string, boolean> | undefined)[] = []; 37 let trackedEs5Classes: ESMap<string, boolean> | undefined; 38 39 // NavigationBarItem requires an array, but will not mutate it, so just give it this for performance. 40 let emptyChildItemArray: NavigationBarItem[] = []; 41 42 /** 43 * Represents a navigation bar item and its children. 44 * The returned NavigationBarItem is more complicated and doesn't include 'parent', so we use these to do work before converting. 45 */ 46 interface NavigationBarNode { 47 node: Node; 48 name: DeclarationName | undefined; 49 additionalNodes: Node[] | undefined; 50 parent: NavigationBarNode | undefined; // Present for all but root node 51 children: NavigationBarNode[] | undefined; 52 indent: number; // # of parents 53 } 54 55 export function getNavigationBarItems(sourceFile: SourceFile, cancellationToken: CancellationToken): NavigationBarItem[] { 56 curCancellationToken = cancellationToken; 57 curSourceFile = sourceFile; 58 try { 59 return map(primaryNavBarMenuItems(rootNavigationBarNode(sourceFile)), convertToPrimaryNavBarMenuItem); 60 } 61 finally { 62 reset(); 63 } 64 } 65 66 export function getNavigationTree(sourceFile: SourceFile, cancellationToken: CancellationToken): NavigationTree { 67 curCancellationToken = cancellationToken; 68 curSourceFile = sourceFile; 69 try { 70 return convertToTree(rootNavigationBarNode(sourceFile)); 71 } 72 finally { 73 reset(); 74 } 75 } 76 77 function reset() { 78 curSourceFile = undefined!; 79 curCancellationToken = undefined!; 80 parentsStack = []; 81 parent = undefined!; 82 emptyChildItemArray = []; 83 } 84 85 function nodeText(node: Node): string { 86 return cleanText(node.getText(curSourceFile)); 87 } 88 89 function navigationBarNodeKind(n: NavigationBarNode): SyntaxKind { 90 return n.node.kind; 91 } 92 93 function pushChild(parent: NavigationBarNode, child: NavigationBarNode): void { 94 if (parent.children) { 95 parent.children.push(child); 96 } 97 else { 98 parent.children = [child]; 99 } 100 } 101 102 function rootNavigationBarNode(sourceFile: SourceFile): NavigationBarNode { 103 Debug.assert(!parentsStack.length); 104 const root: NavigationBarNode = { node: sourceFile, name: undefined, additionalNodes: undefined, parent: undefined, children: undefined, indent: 0 }; 105 parent = root; 106 for (const statement of sourceFile.statements) { 107 addChildrenRecursively(statement); 108 } 109 endNode(); 110 Debug.assert(!parent && !parentsStack.length); 111 return root; 112 } 113 114 function addLeafNode(node: Node, name?: DeclarationName): void { 115 pushChild(parent, emptyNavigationBarNode(node, name)); 116 } 117 118 function emptyNavigationBarNode(node: Node, name?: DeclarationName): NavigationBarNode { 119 return { 120 node, 121 name: name || (isDeclaration(node) || isExpression(node) ? getNameOfDeclaration(node) : undefined), 122 additionalNodes: undefined, 123 parent, 124 children: undefined, 125 indent: parent.indent + 1 126 }; 127 } 128 129 function addTrackedEs5Class(name: string) { 130 if (!trackedEs5Classes) { 131 trackedEs5Classes = new Map(); 132 } 133 trackedEs5Classes.set(name, true); 134 } 135 function endNestedNodes(depth: number): void { 136 for (let i = 0; i < depth; i++) endNode(); 137 } 138 function startNestedNodes(targetNode: Node, entityName: BindableStaticNameExpression) { 139 const names: PropertyNameLiteral[] = []; 140 while (!isPropertyNameLiteral(entityName)) { 141 const name = getNameOrArgument(entityName); 142 const nameText = getElementOrPropertyAccessName(entityName); 143 entityName = entityName.expression; 144 if (nameText === "prototype" || isPrivateIdentifier(name)) continue; 145 names.push(name); 146 } 147 names.push(entityName); 148 for (let i = names.length - 1; i > 0; i--) { 149 const name = names[i]; 150 startNode(targetNode, name); 151 } 152 return [names.length - 1, names[0]] as const; 153 } 154 155 /** 156 * Add a new level of NavigationBarNodes. 157 * This pushes to the stack, so you must call `endNode` when you are done adding to this node. 158 */ 159 function startNode(node: Node, name?: DeclarationName): void { 160 const navNode: NavigationBarNode = emptyNavigationBarNode(node, name); 161 pushChild(parent, navNode); 162 163 // Save the old parent 164 parentsStack.push(parent); 165 trackedEs5ClassesStack.push(trackedEs5Classes); 166 trackedEs5Classes = undefined; 167 parent = navNode; 168 } 169 170 /** Call after calling `startNode` and adding children to it. */ 171 function endNode(): void { 172 if (parent.children) { 173 mergeChildren(parent.children, parent); 174 sortChildren(parent.children); 175 } 176 parent = parentsStack.pop()!; 177 trackedEs5Classes = trackedEs5ClassesStack.pop(); 178 } 179 180 function addNodeWithRecursiveChild(node: Node, child: Node | undefined, name?: DeclarationName): void { 181 startNode(node, name); 182 addChildrenRecursively(child); 183 endNode(); 184 } 185 186 function addNodeWithRecursiveInitializer(node: VariableDeclaration | PropertyAssignment | BindingElement | PropertyDeclaration): void { 187 if (node.initializer && isFunctionOrClassExpression(node.initializer)) { 188 startNode(node); 189 forEachChild(node.initializer, addChildrenRecursively); 190 endNode(); 191 } 192 else { 193 addNodeWithRecursiveChild(node, node.initializer); 194 } 195 } 196 197 /** 198 * Historically, we've elided dynamic names from the nav tree (including late bound names), 199 * but included certain "well known" symbol names. While we no longer distinguish those well-known 200 * symbols from other unique symbols, we do the below to retain those members in the nav tree. 201 */ 202 function hasNavigationBarName(node: Declaration) { 203 return !hasDynamicName(node) || 204 ( 205 node.kind !== SyntaxKind.BinaryExpression && 206 isPropertyAccessExpression(node.name.expression) && 207 isIdentifier(node.name.expression.expression) && 208 idText(node.name.expression.expression) === "Symbol" 209 ); 210 } 211 212 /** Look for navigation bar items in node's subtree, adding them to the current `parent`. */ 213 function addChildrenRecursively(node: Node | undefined): void { 214 curCancellationToken.throwIfCancellationRequested(); 215 216 if (!node || isToken(node)) { 217 return; 218 } 219 220 switch (node.kind) { 221 case SyntaxKind.Constructor: 222 // Get parameter properties, and treat them as being on the *same* level as the constructor, not under it. 223 const ctr = node as ConstructorDeclaration; 224 addNodeWithRecursiveChild(ctr, ctr.body); 225 226 // Parameter properties are children of the class, not the constructor. 227 for (const param of ctr.parameters) { 228 if (isParameterPropertyDeclaration(param, ctr)) { 229 addLeafNode(param); 230 } 231 } 232 break; 233 234 case SyntaxKind.MethodDeclaration: 235 case SyntaxKind.GetAccessor: 236 case SyntaxKind.SetAccessor: 237 case SyntaxKind.MethodSignature: 238 if (hasNavigationBarName(node as ClassElement | TypeElement)) { 239 addNodeWithRecursiveChild(node, (node as FunctionLikeDeclaration).body); 240 } 241 break; 242 243 case SyntaxKind.PropertyDeclaration: 244 if (hasNavigationBarName(node as ClassElement)) { 245 addNodeWithRecursiveInitializer(node as PropertyDeclaration); 246 } 247 break; 248 case SyntaxKind.PropertySignature: 249 if (hasNavigationBarName(node as TypeElement)) { 250 addLeafNode(node); 251 } 252 break; 253 254 case SyntaxKind.ImportClause: 255 const importClause = node as ImportClause; 256 // Handle default import case e.g.: 257 // import d from "mod"; 258 if (importClause.name) { 259 addLeafNode(importClause.name); 260 } 261 262 // Handle named bindings in imports e.g.: 263 // import * as NS from "mod"; 264 // import {a, b as B} from "mod"; 265 const { namedBindings } = importClause; 266 if (namedBindings) { 267 if (namedBindings.kind === SyntaxKind.NamespaceImport) { 268 addLeafNode(namedBindings); 269 } 270 else { 271 for (const element of namedBindings.elements) { 272 addLeafNode(element); 273 } 274 } 275 } 276 break; 277 278 case SyntaxKind.ShorthandPropertyAssignment: 279 addNodeWithRecursiveChild(node, (node as ShorthandPropertyAssignment).name); 280 break; 281 case SyntaxKind.SpreadAssignment: 282 const { expression } = node as SpreadAssignment; 283 // Use the expression as the name of the SpreadAssignment, otherwise show as <unknown>. 284 isIdentifier(expression) ? addLeafNode(node, expression) : addLeafNode(node); 285 break; 286 case SyntaxKind.BindingElement: 287 case SyntaxKind.PropertyAssignment: 288 case SyntaxKind.VariableDeclaration: { 289 const child = node as VariableDeclaration | PropertyAssignment | BindingElement; 290 if (isBindingPattern(child.name)) { 291 addChildrenRecursively(child.name); 292 } 293 else { 294 addNodeWithRecursiveInitializer(child); 295 } 296 break; 297 } 298 case SyntaxKind.FunctionDeclaration: 299 const nameNode = (node as FunctionLikeDeclaration).name; 300 // If we see a function declaration track as a possible ES5 class 301 if (nameNode && isIdentifier(nameNode)) { 302 addTrackedEs5Class(nameNode.text); 303 } 304 addNodeWithRecursiveChild(node, (node as FunctionLikeDeclaration).body); 305 break; 306 case SyntaxKind.ArrowFunction: 307 case SyntaxKind.FunctionExpression: 308 addNodeWithRecursiveChild(node, (node as FunctionLikeDeclaration).body); 309 break; 310 311 case SyntaxKind.EnumDeclaration: 312 startNode(node); 313 for (const member of (node as EnumDeclaration).members) { 314 if (!isComputedProperty(member)) { 315 addLeafNode(member); 316 } 317 } 318 endNode(); 319 break; 320 321 case SyntaxKind.ClassDeclaration: 322 case SyntaxKind.ClassExpression: 323 case SyntaxKind.StructDeclaration: 324 case SyntaxKind.InterfaceDeclaration: 325 startNode(node); 326 for (const member of (node as InterfaceDeclaration).members) { 327 addChildrenRecursively(member); 328 } 329 endNode(); 330 break; 331 332 case SyntaxKind.ModuleDeclaration: 333 addNodeWithRecursiveChild(node, getInteriorModule(node as ModuleDeclaration).body); 334 break; 335 336 case SyntaxKind.ExportAssignment: { 337 const expression = (node as ExportAssignment).expression; 338 const child = isObjectLiteralExpression(expression) || isCallExpression(expression) ? expression : 339 isArrowFunction(expression) || isFunctionExpression(expression) ? expression.body : undefined; 340 if (child) { 341 startNode(node); 342 addChildrenRecursively(child); 343 endNode(); 344 } 345 else { 346 addLeafNode(node); 347 } 348 break; 349 } 350 case SyntaxKind.ExportSpecifier: 351 case SyntaxKind.ImportEqualsDeclaration: 352 case SyntaxKind.IndexSignature: 353 case SyntaxKind.CallSignature: 354 case SyntaxKind.ConstructSignature: 355 case SyntaxKind.TypeAliasDeclaration: 356 addLeafNode(node); 357 break; 358 359 case SyntaxKind.CallExpression: 360 case SyntaxKind.BinaryExpression: { 361 const special = getAssignmentDeclarationKind(node as BinaryExpression); 362 switch (special) { 363 case AssignmentDeclarationKind.ExportsProperty: 364 case AssignmentDeclarationKind.ModuleExports: 365 addNodeWithRecursiveChild(node, (node as BinaryExpression).right); 366 return; 367 case AssignmentDeclarationKind.Prototype: 368 case AssignmentDeclarationKind.PrototypeProperty: { 369 const binaryExpression = (node as BinaryExpression); 370 const assignmentTarget = binaryExpression.left as PropertyAccessExpression; 371 372 const prototypeAccess = special === AssignmentDeclarationKind.PrototypeProperty ? 373 assignmentTarget.expression as PropertyAccessExpression : 374 assignmentTarget; 375 376 let depth = 0; 377 let className: PropertyNameLiteral; 378 // If we see a prototype assignment, start tracking the target as a class 379 // This is only done for simple classes not nested assignments. 380 if (isIdentifier(prototypeAccess.expression)) { 381 addTrackedEs5Class(prototypeAccess.expression.text); 382 className = prototypeAccess.expression; 383 } 384 else { 385 [depth, className] = startNestedNodes(binaryExpression, prototypeAccess.expression as EntityNameExpression); 386 } 387 if (special === AssignmentDeclarationKind.Prototype) { 388 if (isObjectLiteralExpression(binaryExpression.right)) { 389 if (binaryExpression.right.properties.length > 0) { 390 startNode(binaryExpression, className); 391 forEachChild(binaryExpression.right, addChildrenRecursively); 392 endNode(); 393 } 394 } 395 } 396 else if (isFunctionExpression(binaryExpression.right) || isArrowFunction(binaryExpression.right)) { 397 addNodeWithRecursiveChild(node, 398 binaryExpression.right, 399 className); 400 } 401 else { 402 startNode(binaryExpression, className); 403 addNodeWithRecursiveChild(node, binaryExpression.right, assignmentTarget.name); 404 endNode(); 405 } 406 endNestedNodes(depth); 407 return; 408 } 409 case AssignmentDeclarationKind.ObjectDefinePropertyValue: 410 case AssignmentDeclarationKind.ObjectDefinePrototypeProperty: { 411 const defineCall = node as BindableObjectDefinePropertyCall; 412 const className = special === AssignmentDeclarationKind.ObjectDefinePropertyValue ? 413 defineCall.arguments[0] : 414 (defineCall.arguments[0] as PropertyAccessExpression).expression as EntityNameExpression; 415 416 const memberName = defineCall.arguments[1]; 417 const [depth, classNameIdentifier] = startNestedNodes(node, className); 418 startNode(node, classNameIdentifier); 419 startNode(node, setTextRange(factory.createIdentifier(memberName.text), memberName)); 420 addChildrenRecursively((node as CallExpression).arguments[2]); 421 endNode(); 422 endNode(); 423 endNestedNodes(depth); 424 return; 425 } 426 case AssignmentDeclarationKind.Property: { 427 const binaryExpression = (node as BinaryExpression); 428 const assignmentTarget = binaryExpression.left as PropertyAccessExpression | BindableElementAccessExpression; 429 const targetFunction = assignmentTarget.expression; 430 if (isIdentifier(targetFunction) && getElementOrPropertyAccessName(assignmentTarget) !== "prototype" && 431 trackedEs5Classes && trackedEs5Classes.has(targetFunction.text)) { 432 if (isFunctionExpression(binaryExpression.right) || isArrowFunction(binaryExpression.right)) { 433 addNodeWithRecursiveChild(node, binaryExpression.right, targetFunction); 434 } 435 else if (isBindableStaticAccessExpression(assignmentTarget)) { 436 startNode(binaryExpression, targetFunction); 437 addNodeWithRecursiveChild(binaryExpression.left, binaryExpression.right, getNameOrArgument(assignmentTarget)); 438 endNode(); 439 } 440 return; 441 } 442 break; 443 } 444 case AssignmentDeclarationKind.ThisProperty: 445 case AssignmentDeclarationKind.None: 446 case AssignmentDeclarationKind.ObjectDefinePropertyExports: 447 break; 448 default: 449 Debug.assertNever(special); 450 } 451 } 452 // falls through 453 454 default: 455 if (hasJSDocNodes(node)) { 456 forEach(node.jsDoc, jsDoc => { 457 forEach(jsDoc.tags, tag => { 458 if (isJSDocTypeAlias(tag)) { 459 addLeafNode(tag); 460 } 461 }); 462 }); 463 } 464 465 forEachChild(node, addChildrenRecursively); 466 } 467 } 468 469 /** Merge declarations of the same kind. */ 470 function mergeChildren(children: NavigationBarNode[], node: NavigationBarNode): void { 471 const nameToItems = new Map<string, NavigationBarNode | NavigationBarNode[]>(); 472 filterMutate(children, (child, index) => { 473 const declName = child.name || getNameOfDeclaration(child.node as Declaration); 474 const name = declName && nodeText(declName); 475 if (!name) { 476 // Anonymous items are never merged. 477 return true; 478 } 479 480 const itemsWithSameName = nameToItems.get(name); 481 if (!itemsWithSameName) { 482 nameToItems.set(name, child); 483 return true; 484 } 485 486 if (itemsWithSameName instanceof Array) { 487 for (const itemWithSameName of itemsWithSameName) { 488 if (tryMerge(itemWithSameName, child, index, node)) { 489 return false; 490 } 491 } 492 itemsWithSameName.push(child); 493 return true; 494 } 495 else { 496 const itemWithSameName = itemsWithSameName; 497 if (tryMerge(itemWithSameName, child, index, node)) { 498 return false; 499 } 500 nameToItems.set(name, [itemWithSameName, child]); 501 return true; 502 } 503 }); 504 } 505 const isEs5ClassMember: Record<AssignmentDeclarationKind, boolean> = { 506 [AssignmentDeclarationKind.Property]: true, 507 [AssignmentDeclarationKind.PrototypeProperty]: true, 508 [AssignmentDeclarationKind.ObjectDefinePropertyValue]: true, 509 [AssignmentDeclarationKind.ObjectDefinePrototypeProperty]: true, 510 [AssignmentDeclarationKind.None]: false, 511 [AssignmentDeclarationKind.ExportsProperty]: false, 512 [AssignmentDeclarationKind.ModuleExports]: false, 513 [AssignmentDeclarationKind.ObjectDefinePropertyExports]: false, 514 [AssignmentDeclarationKind.Prototype]: true, 515 [AssignmentDeclarationKind.ThisProperty]: false, 516 }; 517 function tryMergeEs5Class(a: NavigationBarNode, b: NavigationBarNode, bIndex: number, parent: NavigationBarNode): boolean | undefined { 518 function isPossibleConstructor(node: Node) { 519 return isFunctionExpression(node) || isFunctionDeclaration(node) || isVariableDeclaration(node); 520 } 521 const bAssignmentDeclarationKind = isBinaryExpression(b.node) || isCallExpression(b.node) ? 522 getAssignmentDeclarationKind(b.node) : 523 AssignmentDeclarationKind.None; 524 525 const aAssignmentDeclarationKind = isBinaryExpression(a.node) || isCallExpression(a.node) ? 526 getAssignmentDeclarationKind(a.node) : 527 AssignmentDeclarationKind.None; 528 529 // We treat this as an es5 class and merge the nodes in in one of several cases 530 if ((isEs5ClassMember[bAssignmentDeclarationKind] && isEs5ClassMember[aAssignmentDeclarationKind]) // merge two class elements 531 || (isPossibleConstructor(a.node) && isEs5ClassMember[bAssignmentDeclarationKind]) // ctor function & member 532 || (isPossibleConstructor(b.node) && isEs5ClassMember[aAssignmentDeclarationKind]) // member & ctor function 533 || (isClassDeclaration(a.node) && isSynthesized(a.node) && isEs5ClassMember[bAssignmentDeclarationKind]) // class (generated) & member 534 || (isClassDeclaration(b.node) && isEs5ClassMember[aAssignmentDeclarationKind]) // member & class (generated) 535 || (isClassDeclaration(a.node) && isSynthesized(a.node) && isPossibleConstructor(b.node)) // class (generated) & ctor 536 || (isClassDeclaration(b.node) && isPossibleConstructor(a.node) && isSynthesized(a.node)) // ctor & class (generated) 537 ) { 538 539 let lastANode = a.additionalNodes && lastOrUndefined(a.additionalNodes) || a.node; 540 541 if ((!isClassDeclaration(a.node) && !isClassDeclaration(b.node)) // If neither outline node is a class 542 || isPossibleConstructor(a.node) || isPossibleConstructor(b.node) // If either function is a constructor function 543 ) { 544 const ctorFunction = isPossibleConstructor(a.node) ? a.node : 545 isPossibleConstructor(b.node) ? b.node : 546 undefined; 547 548 if (ctorFunction !== undefined) { 549 const ctorNode = setTextRange( 550 factory.createConstructorDeclaration(/* modifiers */ undefined, [], /* body */ undefined), 551 ctorFunction); 552 const ctor = emptyNavigationBarNode(ctorNode); 553 ctor.indent = a.indent + 1; 554 ctor.children = a.node === ctorFunction ? a.children : b.children; 555 a.children = a.node === ctorFunction ? concatenate([ctor], b.children || [b]) : concatenate(a.children || [{ ...a }], [ctor]); 556 } 557 else { 558 if (a.children || b.children) { 559 a.children = concatenate(a.children || [{ ...a }], b.children || [b]); 560 if (a.children) { 561 mergeChildren(a.children, a); 562 sortChildren(a.children); 563 } 564 } 565 } 566 567 lastANode = a.node = setTextRange(factory.createClassDeclaration( 568 /* modifiers */ undefined, 569 a.name as Identifier || factory.createIdentifier("__class__"), 570 /* typeParameters */ undefined, 571 /* heritageClauses */ undefined, 572 [] 573 ), a.node); 574 } 575 else { 576 a.children = concatenate(a.children, b.children); 577 if (a.children) { 578 mergeChildren(a.children, a); 579 } 580 } 581 582 const bNode = b.node; 583 // We merge if the outline node previous to b (bIndex - 1) is already part of the current class 584 // We do this so that statements between class members that do not generate outline nodes do not split up the class outline: 585 // Ex This should produce one outline node C: 586 // function C() {}; a = 1; C.prototype.m = function () {} 587 // Ex This will produce 3 outline nodes: C, a, C 588 // function C() {}; let a = 1; C.prototype.m = function () {} 589 if (parent.children![bIndex - 1].node.end === lastANode.end) { 590 setTextRange(lastANode, { pos: lastANode.pos, end: bNode.end }); 591 } 592 else { 593 if (!a.additionalNodes) a.additionalNodes = []; 594 a.additionalNodes.push(setTextRange(factory.createClassDeclaration( 595 /* modifiers */ undefined, 596 a.name as Identifier || factory.createIdentifier("__class__"), 597 /* typeParameters */ undefined, 598 /* heritageClauses */ undefined, 599 [] 600 ), b.node)); 601 } 602 return true; 603 } 604 return bAssignmentDeclarationKind === AssignmentDeclarationKind.None ? false : true; 605 } 606 607 function tryMerge(a: NavigationBarNode, b: NavigationBarNode, bIndex: number, parent: NavigationBarNode): boolean { 608 // const v = false as boolean; 609 if (tryMergeEs5Class(a, b, bIndex, parent)) { 610 return true; 611 } 612 if (shouldReallyMerge(a.node, b.node, parent)) { 613 merge(a, b); 614 return true; 615 } 616 return false; 617 } 618 619 /** a and b have the same name, but they may not be mergeable. */ 620 function shouldReallyMerge(a: Node, b: Node, parent: NavigationBarNode): boolean { 621 if (a.kind !== b.kind || a.parent !== b.parent && !(isOwnChild(a, parent) && isOwnChild(b, parent))) { 622 return false; 623 } 624 switch (a.kind) { 625 case SyntaxKind.PropertyDeclaration: 626 case SyntaxKind.MethodDeclaration: 627 case SyntaxKind.GetAccessor: 628 case SyntaxKind.SetAccessor: 629 return isStatic(a) === isStatic(b); 630 case SyntaxKind.ModuleDeclaration: 631 return areSameModule(a as ModuleDeclaration, b as ModuleDeclaration) 632 && getFullyQualifiedModuleName(a as ModuleDeclaration) === getFullyQualifiedModuleName(b as ModuleDeclaration); 633 default: 634 return true; 635 } 636 } 637 638 function isSynthesized(node: Node) { 639 return !!(node.flags & NodeFlags.Synthesized); 640 } 641 642 // We want to merge own children like `I` in in `module A { interface I {} } module A { interface I {} }` 643 // We don't want to merge unrelated children like `m` in `const o = { a: { m() {} }, b: { m() {} } };` 644 function isOwnChild(n: Node, parent: NavigationBarNode): boolean { 645 const par = isModuleBlock(n.parent) ? n.parent.parent : n.parent; 646 return par === parent.node || contains(parent.additionalNodes, par); 647 } 648 649 // We use 1 NavNode to represent 'A.B.C', but there are multiple source nodes. 650 // Only merge module nodes that have the same chain. Don't merge 'A.B.C' with 'A'! 651 function areSameModule(a: ModuleDeclaration, b: ModuleDeclaration): boolean { 652 if (!a.body || !b.body) { 653 return a.body === b.body; 654 } 655 return a.body.kind === b.body.kind && (a.body.kind !== SyntaxKind.ModuleDeclaration || areSameModule(a.body as ModuleDeclaration, b.body as ModuleDeclaration)); 656 } 657 658 /** Merge source into target. Source should be thrown away after this is called. */ 659 function merge(target: NavigationBarNode, source: NavigationBarNode): void { 660 target.additionalNodes = target.additionalNodes || []; 661 target.additionalNodes.push(source.node); 662 if (source.additionalNodes) { 663 target.additionalNodes.push(...source.additionalNodes); 664 } 665 666 target.children = concatenate(target.children, source.children); 667 if (target.children) { 668 mergeChildren(target.children, target); 669 sortChildren(target.children); 670 } 671 } 672 673 /** Recursively ensure that each NavNode's children are in sorted order. */ 674 function sortChildren(children: NavigationBarNode[]): void { 675 children.sort(compareChildren); 676 } 677 678 function compareChildren(child1: NavigationBarNode, child2: NavigationBarNode) { 679 return compareStringsCaseSensitiveUI(tryGetName(child1.node)!, tryGetName(child2.node)!) // TODO: GH#18217 680 || compareValues(navigationBarNodeKind(child1), navigationBarNodeKind(child2)); 681 } 682 683 /** 684 * This differs from getItemName because this is just used for sorting. 685 * We only sort nodes by name that have a more-or-less "direct" name, as opposed to `new()` and the like. 686 * So `new()` can still come before an `aardvark` method. 687 */ 688 function tryGetName(node: Node): string | undefined { 689 if (node.kind === SyntaxKind.ModuleDeclaration) { 690 return getModuleName(node as ModuleDeclaration); 691 } 692 693 const declName = getNameOfDeclaration(node as Declaration); 694 if (declName && isPropertyName(declName)) { 695 const propertyName = getPropertyNameForPropertyNameNode(declName); 696 return propertyName && unescapeLeadingUnderscores(propertyName); 697 } 698 switch (node.kind) { 699 case SyntaxKind.FunctionExpression: 700 case SyntaxKind.ArrowFunction: 701 case SyntaxKind.ClassExpression: 702 return getFunctionOrClassName(node as FunctionExpression | ArrowFunction | ClassExpression); 703 default: 704 return undefined; 705 } 706 } 707 708 function getItemName(node: Node, name: Node | undefined): string { 709 if (node.kind === SyntaxKind.ModuleDeclaration) { 710 return cleanText(getModuleName(node as ModuleDeclaration)); 711 } 712 713 if (name) { 714 const text = isIdentifier(name) ? name.text 715 : isElementAccessExpression(name) ? `[${nodeText(name.argumentExpression)}]` 716 : nodeText(name); 717 if (text.length > 0) { 718 return cleanText(text); 719 } 720 } 721 722 switch (node.kind) { 723 case SyntaxKind.SourceFile: 724 const sourceFile = node as SourceFile; 725 return isExternalModule(sourceFile) 726 ? `"${escapeString(getBaseFileName(removeFileExtension(normalizePath(sourceFile.fileName))))}"` 727 : "<global>"; 728 case SyntaxKind.ExportAssignment: 729 return isExportAssignment(node) && node.isExportEquals ? InternalSymbolName.ExportEquals : InternalSymbolName.Default; 730 731 case SyntaxKind.ArrowFunction: 732 case SyntaxKind.FunctionDeclaration: 733 case SyntaxKind.FunctionExpression: 734 case SyntaxKind.ClassDeclaration: 735 case SyntaxKind.ClassExpression: 736 case SyntaxKind.StructDeclaration: 737 if (getSyntacticModifierFlags(node) & ModifierFlags.Default) { 738 return "default"; 739 } 740 // We may get a string with newlines or other whitespace in the case of an object dereference 741 // (eg: "app\n.onactivated"), so we should remove the whitespace for readability in the 742 // navigation bar. 743 return getFunctionOrClassName(node as ArrowFunction | FunctionExpression | ClassExpression); 744 case SyntaxKind.Constructor: 745 return "constructor"; 746 case SyntaxKind.ConstructSignature: 747 return "new()"; 748 case SyntaxKind.CallSignature: 749 return "()"; 750 case SyntaxKind.IndexSignature: 751 return "[]"; 752 default: 753 return "<unknown>"; 754 } 755 } 756 757 /** Flattens the NavNode tree to a list of items to appear in the primary navbar menu. */ 758 function primaryNavBarMenuItems(root: NavigationBarNode): NavigationBarNode[] { 759 // The primary (middle) navbar menu displays the general code navigation hierarchy, similar to the navtree. 760 // The secondary (right) navbar menu displays the child items of whichever primary item is selected. 761 // Some less interesting items without their own child navigation items (e.g. a local variable declaration) only show up in the secondary menu. 762 const primaryNavBarMenuItems: NavigationBarNode[] = []; 763 function recur(item: NavigationBarNode) { 764 if (shouldAppearInPrimaryNavBarMenu(item)) { 765 primaryNavBarMenuItems.push(item); 766 if (item.children) { 767 for (const child of item.children) { 768 recur(child); 769 } 770 } 771 } 772 } 773 recur(root); 774 return primaryNavBarMenuItems; 775 776 /** Determines if a node should appear in the primary navbar menu. */ 777 function shouldAppearInPrimaryNavBarMenu(item: NavigationBarNode): boolean { 778 // Items with children should always appear in the primary navbar menu. 779 if (item.children) { 780 return true; 781 } 782 783 // Some nodes are otherwise important enough to always include in the primary navigation menu. 784 switch (navigationBarNodeKind(item)) { 785 case SyntaxKind.ClassDeclaration: 786 case SyntaxKind.ClassExpression: 787 case SyntaxKind.StructDeclaration: 788 case SyntaxKind.EnumDeclaration: 789 case SyntaxKind.InterfaceDeclaration: 790 case SyntaxKind.ModuleDeclaration: 791 case SyntaxKind.SourceFile: 792 case SyntaxKind.TypeAliasDeclaration: 793 case SyntaxKind.JSDocTypedefTag: 794 case SyntaxKind.JSDocCallbackTag: 795 return true; 796 797 case SyntaxKind.ArrowFunction: 798 case SyntaxKind.FunctionDeclaration: 799 case SyntaxKind.FunctionExpression: 800 return isTopLevelFunctionDeclaration(item); 801 802 default: 803 return false; 804 } 805 function isTopLevelFunctionDeclaration(item: NavigationBarNode): boolean { 806 if (!(item.node as FunctionDeclaration).body) { 807 return false; 808 } 809 810 switch (navigationBarNodeKind(item.parent!)) { 811 case SyntaxKind.ModuleBlock: 812 case SyntaxKind.SourceFile: 813 case SyntaxKind.MethodDeclaration: 814 case SyntaxKind.Constructor: 815 return true; 816 default: 817 return false; 818 } 819 } 820 } 821 } 822 823 function convertToTree(n: NavigationBarNode): NavigationTree { 824 return { 825 text: getItemName(n.node, n.name), 826 kind: getNodeKind(n.node), 827 kindModifiers: getModifiers(n.node), 828 spans: getSpans(n), 829 nameSpan: n.name && getNodeSpan(n.name), 830 childItems: map(n.children, convertToTree) 831 }; 832 } 833 834 function convertToPrimaryNavBarMenuItem(n: NavigationBarNode): NavigationBarItem { 835 return { 836 text: getItemName(n.node, n.name), 837 kind: getNodeKind(n.node), 838 kindModifiers: getModifiers(n.node), 839 spans: getSpans(n), 840 childItems: map(n.children, convertToSecondaryNavBarMenuItem) || emptyChildItemArray, 841 indent: n.indent, 842 bolded: false, 843 grayed: false 844 }; 845 846 function convertToSecondaryNavBarMenuItem(n: NavigationBarNode): NavigationBarItem { 847 return { 848 text: getItemName(n.node, n.name), 849 kind: getNodeKind(n.node), 850 kindModifiers: getNodeModifiers(n.node), 851 spans: getSpans(n), 852 childItems: emptyChildItemArray, 853 indent: 0, 854 bolded: false, 855 grayed: false 856 }; 857 } 858 } 859 860 function getSpans(n: NavigationBarNode): TextSpan[] { 861 const spans = [getNodeSpan(n.node)]; 862 if (n.additionalNodes) { 863 for (const node of n.additionalNodes) { 864 spans.push(getNodeSpan(node)); 865 } 866 } 867 return spans; 868 } 869 870 function getModuleName(moduleDeclaration: ModuleDeclaration): string { 871 // We want to maintain quotation marks. 872 if (isAmbientModule(moduleDeclaration)) { 873 return getTextOfNode(moduleDeclaration.name); 874 } 875 876 return getFullyQualifiedModuleName(moduleDeclaration); 877 } 878 879 function getFullyQualifiedModuleName(moduleDeclaration: ModuleDeclaration): string { 880 // Otherwise, we need to aggregate each identifier to build up the qualified name. 881 const result = [getTextOfIdentifierOrLiteral(moduleDeclaration.name)]; 882 while (moduleDeclaration.body && moduleDeclaration.body.kind === SyntaxKind.ModuleDeclaration) { 883 moduleDeclaration = moduleDeclaration.body; 884 result.push(getTextOfIdentifierOrLiteral(moduleDeclaration.name)); 885 } 886 return result.join("."); 887 } 888 889 /** 890 * For 'module A.B.C', we want to get the node for 'C'. 891 * We store 'A' as associated with a NavNode, and use getModuleName to traverse down again. 892 */ 893 function getInteriorModule(decl: ModuleDeclaration): ModuleDeclaration { 894 return decl.body && isModuleDeclaration(decl.body) ? getInteriorModule(decl.body) : decl; 895 } 896 897 function isComputedProperty(member: EnumMember): boolean { 898 return !member.name || member.name.kind === SyntaxKind.ComputedPropertyName; 899 } 900 901 function getNodeSpan(node: Node): TextSpan { 902 return node.kind === SyntaxKind.SourceFile ? createTextSpanFromRange(node) : createTextSpanFromNode(node, curSourceFile); 903 } 904 905 function getModifiers(node: Node): string { 906 if (node.parent && node.parent.kind === SyntaxKind.VariableDeclaration) { 907 node = node.parent; 908 } 909 return getNodeModifiers(node); 910 } 911 912 function getFunctionOrClassName(node: FunctionExpression | FunctionDeclaration | ArrowFunction | ClassLikeDeclaration): string { 913 const { parent } = node; 914 if (node.name && getFullWidth(node.name) > 0) { 915 return cleanText(declarationNameToString(node.name)); 916 } 917 // See if it is a var initializer. If so, use the var name. 918 else if (isVariableDeclaration(parent)) { 919 return cleanText(declarationNameToString(parent.name)); 920 } 921 // See if it is of the form "<expr> = function(){...}". If so, use the text from the left-hand side. 922 else if (isBinaryExpression(parent) && parent.operatorToken.kind === SyntaxKind.EqualsToken) { 923 return nodeText(parent.left).replace(whiteSpaceRegex, ""); 924 } 925 // See if it is a property assignment, and if so use the property name 926 else if (isPropertyAssignment(parent)) { 927 return nodeText(parent.name); 928 } 929 // Default exports are named "default" 930 else if (getSyntacticModifierFlags(node) & ModifierFlags.Default) { 931 return "default"; 932 } 933 else if (isClassLike(node)) { 934 return "<class>"; 935 } 936 else if (isCallExpression(parent)) { 937 let name = getCalledExpressionName(parent.expression); 938 if (name !== undefined) { 939 name = cleanText(name); 940 941 if (name.length > maxLength) { 942 return `${name} callback`; 943 } 944 945 const args = cleanText(mapDefined(parent.arguments, a => isStringLiteralLike(a) ? a.getText(curSourceFile) : undefined).join(", ")); 946 return `${name}(${args}) callback`; 947 } 948 } 949 return "<function>"; 950 } 951 952 // See also 'tryGetPropertyAccessOrIdentifierToString' 953 function getCalledExpressionName(expr: Expression): string | undefined { 954 if (isIdentifier(expr)) { 955 return expr.text; 956 } 957 else if (isPropertyAccessExpression(expr)) { 958 const left = getCalledExpressionName(expr.expression); 959 const right = expr.name.text; 960 return left === undefined ? right : `${left}.${right}`; 961 } 962 else { 963 return undefined; 964 } 965 } 966 967 function isFunctionOrClassExpression(node: Node): node is ArrowFunction | FunctionExpression | ClassExpression { 968 switch (node.kind) { 969 case SyntaxKind.ArrowFunction: 970 case SyntaxKind.FunctionExpression: 971 case SyntaxKind.ClassExpression: 972 return true; 973 default: 974 return false; 975 } 976 } 977 978 function cleanText(text: string): string { 979 // Truncate to maximum amount of characters as we don't want to do a big replace operation. 980 text = text.length > maxLength ? text.substring(0, maxLength) + "..." : text; 981 982 // Replaces ECMAScript line terminators and removes the trailing `\` from each line: 983 // \n - Line Feed 984 // \r - Carriage Return 985 // \u2028 - Line separator 986 // \u2029 - Paragraph separator 987 return text.replace(/\\?(\r?\n|\r|\u2028|\u2029)/g, ""); 988 } 989} 990