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