| // Copyright 2016 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import * as Platform from '../../core/platform/platform.js'; |
| import * as TextUtils from '../../models/text_utils/text_utils.js'; |
| import * as AcornLoose from '../../third_party/acorn-loose/acorn-loose.js'; |
| import * as Acorn from '../../third_party/acorn/acorn.js'; |
| |
| import {ECMA_VERSION} from './AcornTokenizer.js'; |
| import {ESTreeWalker} from './ESTreeWalker.js'; |
| import {type ChunkCallback} from './FormatterWorker.js'; |
| |
| export interface Item { |
| title: string; |
| subtitle?: string; |
| line: number; |
| column: number; |
| } |
| |
| export function javaScriptOutline(content: string, chunkCallback: ChunkCallback): void { |
| const chunkSize = 100000; |
| let chunk: Item[] = []; |
| let lastReportedOffset = 0; |
| |
| let ast; |
| try { |
| ast = Acorn.parse(content, {ecmaVersion: ECMA_VERSION, ranges: false}); |
| } catch (e) { |
| ast = AcornLoose.AcornLoose.parse(content, {ecmaVersion: ECMA_VERSION, ranges: false}); |
| } |
| |
| const contentLineEndings = Platform.StringUtilities.findLineEndingIndexes(content); |
| const textCursor = new TextUtils.TextCursor.TextCursor(contentLineEndings); |
| const walker = new ESTreeWalker(beforeVisit); |
| |
| // @ts-ignore Technically, the acorn Node type is a subclass of Acorn.ESTree.Node. |
| // However, the acorn package currently exports its type without specifying |
| // this relationship. So while this is allowed on runtime, we can't properly |
| // typecheck it. |
| walker.walk(ast); |
| |
| chunkCallback({chunk, isLastChunk: true}); |
| |
| function beforeVisit(node: Acorn.ESTree.Node): undefined { |
| if (node.type === 'ClassDeclaration') { |
| reportClass((node.id as Acorn.ESTree.Node)); |
| } else if (node.type === 'VariableDeclarator' && node.init && isClassNode(node.init)) { |
| reportClass((node.id as Acorn.ESTree.Node)); |
| } else if (node.type === 'AssignmentExpression' && isNameNode(node.left) && isClassNode(node.right)) { |
| reportClass((node.left as Acorn.ESTree.Node)); |
| } else if (node.type === 'Property' && isNameNode(node.key) && isClassNode(node.value)) { |
| reportClass((node.key as Acorn.ESTree.Node)); |
| } else if (node.type === 'FunctionDeclaration') { |
| reportFunction((node.id as Acorn.ESTree.Node), node); |
| } else if (node.type === 'VariableDeclarator' && node.init && isFunctionNode(node.init)) { |
| reportFunction((node.id as Acorn.ESTree.Node), (node.init as Acorn.ESTree.Node)); |
| } else if (node.type === 'AssignmentExpression' && isNameNode(node.left) && isFunctionNode(node.right)) { |
| reportFunction((node.left as Acorn.ESTree.Node), (node.right as Acorn.ESTree.Node)); |
| } else if ( |
| (node.type === 'MethodDefinition' || node.type === 'Property') && isNameNode(node.key) && |
| isFunctionNode(node.value)) { |
| const namePrefix = []; |
| if (node.kind === 'get' || node.kind === 'set') { |
| namePrefix.push(node.kind); |
| } |
| if ('static' in node && node.static) { |
| namePrefix.push('static'); |
| } |
| reportFunction((node.key as Acorn.ESTree.Node), node.value, namePrefix.join(' ')); |
| } |
| |
| return undefined; |
| } |
| |
| function reportClass(nameNode: Acorn.ESTree.Node): void { |
| const name = 'class ' + stringifyNameNode(nameNode); |
| textCursor.advance(nameNode.start); |
| addOutlineItem(name); |
| } |
| |
| function reportFunction(nameNode: Acorn.ESTree.Node, functionNode: Acorn.ESTree.Node, namePrefix?: string): void { |
| let name = stringifyNameNode(nameNode); |
| const functionDeclarationNode = (functionNode as Acorn.ESTree.FunctionDeclaration); |
| if (functionDeclarationNode.generator) { |
| name = '*' + name; |
| } |
| if (namePrefix) { |
| name = namePrefix + ' ' + name; |
| } |
| if (functionDeclarationNode.async) { |
| name = 'async ' + name; |
| } |
| |
| textCursor.advance(nameNode.start); |
| addOutlineItem(name, stringifyArguments((functionDeclarationNode.params as Acorn.ESTree.Node[]))); |
| } |
| |
| function isNameNode(node: Acorn.ESTree.Node): boolean { |
| if (!node) { |
| return false; |
| } |
| if (node.type === 'MemberExpression') { |
| return !node.computed && node.property.type === 'Identifier'; |
| } |
| return node.type === 'Identifier'; |
| } |
| |
| function isFunctionNode(node: Acorn.ESTree.Node): boolean { |
| if (!node) { |
| return false; |
| } |
| return node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression'; |
| } |
| |
| function isClassNode(node: Acorn.ESTree.Node): boolean { |
| return node !== undefined && node.type === 'ClassExpression'; |
| } |
| |
| function stringifyNameNode(node: Acorn.ESTree.Node): string { |
| if (node.type === 'MemberExpression') { |
| node = (node.property as Acorn.ESTree.Node); |
| } |
| console.assert(node.type === 'Identifier', 'Cannot extract identifier from unknown type: ' + node.type); |
| const identifier = (node as Acorn.ESTree.Identifier); |
| return identifier.name; |
| } |
| |
| function stringifyArguments(params: Acorn.ESTree.Node[]): string { |
| const result = []; |
| for (const param of params) { |
| if (param.type === 'Identifier') { |
| result.push(param.name); |
| } else if (param.type === 'RestElement' && param.argument.type === 'Identifier') { |
| result.push('...' + param.argument.name); |
| } else { |
| console.error('Error: unexpected function parameter type: ' + param.type); |
| } |
| } |
| return '(' + result.join(', ') + ')'; |
| } |
| |
| function addOutlineItem(title: string, subtitle?: string): void { |
| const line = textCursor.lineNumber(); |
| const column = textCursor.columnNumber(); |
| chunk.push({title, subtitle, line, column}); |
| if (textCursor.offset() - lastReportedOffset >= chunkSize) { |
| chunkCallback({chunk, isLastChunk: false}); |
| chunk = []; |
| lastReportedOffset = textCursor.offset(); |
| } |
| } |
| } |