UNPKG

chrome-devtools-frontend

Version:
154 lines (134 loc) • 5.86 kB
// 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(); } } }