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