| // Copyright 2011 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import * as Acorn from '../../third_party/acorn/acorn.js'; |
| |
| import {AcornTokenizer, ECMA_VERSION, type TokenOrComment} from './AcornTokenizer.js'; |
| import {ESTreeWalker} from './ESTreeWalker.js'; |
| import type {FormattedContentBuilder} from './FormattedContentBuilder.js'; |
| |
| export class JavaScriptFormatter { |
| readonly #builder: FormattedContentBuilder; |
| #tokenizer!: AcornTokenizer; |
| #content!: string; |
| #fromOffset!: number; |
| #lastLineNumber!: number; |
| #toOffset?: number; |
| constructor(builder: FormattedContentBuilder) { |
| this.#builder = builder; |
| } |
| |
| format(text: string, _lineEndings: number[], fromOffset: number, toOffset: number): void { |
| this.#fromOffset = fromOffset; |
| this.#toOffset = toOffset; |
| this.#content = text.substring(this.#fromOffset, this.#toOffset); |
| this.#lastLineNumber = 0; |
| const tokens: Array<Acorn.Token|Acorn.Comment> = []; |
| const ast = Acorn.parse(this.#content, { |
| ranges: false, |
| preserveParens: true, |
| allowAwaitOutsideFunction: true, |
| allowImportExportEverywhere: true, |
| ecmaVersion: ECMA_VERSION, |
| allowHashBang: true, |
| onToken: tokens as Acorn.Token[], |
| onComment: tokens as Acorn.Comment[], |
| }); |
| this.#tokenizer = new AcornTokenizer(this.#content, tokens); |
| const walker = new ESTreeWalker(this.#beforeVisit.bind(this), this.#afterVisit.bind(this)); |
| // @ts-expect-error 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); |
| } |
| |
| #push(token: Acorn.Token|Acorn.Comment|null, format: string): void { |
| for (let i = 0; i < format.length; ++i) { |
| if (format[i] === 's') { |
| this.#builder.addSoftSpace(); |
| } else if (format[i] === 'S') { |
| this.#builder.addHardSpace(); |
| } else if (format[i] === 'n') { |
| this.#builder.addNewLine(); |
| } else if (format[i] === '>') { |
| this.#builder.increaseNestingLevel(); |
| } else if (format[i] === '<') { |
| this.#builder.decreaseNestingLevel(); |
| } else if (format[i] === 't') { |
| if (this.#tokenizer.tokenLineStart() - this.#lastLineNumber > 1) { |
| this.#builder.addNewLine(true); |
| } |
| this.#lastLineNumber = this.#tokenizer.tokenLineEnd(); |
| if (token) { |
| this.#builder.addToken(this.#content.substring(token.start, token.end), this.#fromOffset + token.start); |
| } |
| } |
| } |
| } |
| |
| #beforeVisit(node: Acorn.ESTree.Node): undefined { |
| if (!node.parent) { |
| return; |
| } |
| if (node.type === 'TemplateLiteral') { |
| this.#builder.setEnforceSpaceBetweenWords(false); |
| } |
| let token; |
| while ((token = this.#tokenizer.peekToken()) && token.start < node.start) { |
| const token = (this.#tokenizer.nextToken() as TokenOrComment); |
| // @ts-expect-error Same reason as above about Acorn types and ESTree types |
| const format = this.#formatToken(node.parent, token); |
| this.#push(token, format); |
| } |
| } |
| |
| #afterVisit(node: Acorn.ESTree.Node): void { |
| // ${expressions} within a template literal need space enforced. |
| const restore = this.#builder.setEnforceSpaceBetweenWords(node.type !== 'TemplateElement'); |
| let token; |
| while ((token = this.#tokenizer.peekToken()) && token.start < node.end) { |
| const token = (this.#tokenizer.nextToken() as TokenOrComment); |
| const format = this.#formatToken(node, token); |
| this.#push(token, format); |
| } |
| this.#push(null, this.#finishNode(node)); |
| this.#builder.setEnforceSpaceBetweenWords(restore || node.type === 'TemplateLiteral'); |
| } |
| |
| #inForLoopHeader(node: Acorn.ESTree.Node): boolean { |
| const parent = node.parent; |
| if (!parent) { |
| return false; |
| } |
| if (parent.type === 'ForStatement') { |
| const parentNode = (parent as Acorn.ESTree.ForStatement); |
| return node === parentNode.init || node === parentNode.test || node === parentNode.update; |
| } |
| if (parent.type === 'ForInStatement' || parent.type === 'ForOfStatement') { |
| const parentNode = (parent as Acorn.ESTree.ForInStatement | Acorn.ESTree.ForOfStatement); |
| return node === parentNode.left || node === parentNode.right; |
| } |
| return false; |
| } |
| |
| #formatToken(node: Acorn.ESTree.Node, tokenOrComment: TokenOrComment): string { |
| const AT = AcornTokenizer; |
| if (AT.lineComment(tokenOrComment)) { |
| return 'tn'; |
| } |
| if (AT.blockComment(tokenOrComment)) { |
| return 'tn'; |
| } |
| const token = (tokenOrComment as Acorn.Token); |
| const nodeType = node.type; |
| if (nodeType === 'ContinueStatement' || nodeType === 'BreakStatement') { |
| return node.label && AT.keyword(token) ? 'ts' : 't'; |
| } |
| if (nodeType === 'Identifier') { |
| return 't'; |
| } |
| if (nodeType === 'PrivateIdentifier') { |
| return 't'; |
| } |
| if (nodeType === 'ReturnStatement') { |
| if (AT.punctuator(token, ';')) { |
| return 't'; |
| } |
| return node.argument ? 'ts' : 't'; |
| } |
| if (nodeType === 'AwaitExpression') { |
| if (AT.punctuator(token, ';')) { |
| return 't'; |
| } |
| return node.argument ? 'ts' : 't'; |
| } |
| if (nodeType === 'Property') { |
| if (AT.punctuator(token, ':')) { |
| return 'ts'; |
| } |
| return 't'; |
| } |
| if (nodeType === 'ArrayExpression') { |
| if (AT.punctuator(token, ',')) { |
| return 'ts'; |
| } |
| return 't'; |
| } |
| if (nodeType === 'LabeledStatement') { |
| if (AT.punctuator(token, ':')) { |
| return 'ts'; |
| } |
| } else if ( |
| nodeType === 'LogicalExpression' || nodeType === 'AssignmentExpression' || nodeType === 'BinaryExpression') { |
| if (AT.punctuator(token) && !AT.punctuator(token, '()')) { |
| return 'sts'; |
| } |
| } else if (nodeType === 'ConditionalExpression') { |
| if (AT.punctuator(token, '?:')) { |
| return 'sts'; |
| } |
| } else if (nodeType === 'VariableDeclarator') { |
| if (AT.punctuator(token, '=')) { |
| return 'sts'; |
| } |
| } else if (nodeType === 'ObjectPattern') { |
| if (node.parent?.type === 'VariableDeclarator' && AT.punctuator(token, '{')) { |
| return 'st'; |
| } |
| if (AT.punctuator(token, ',')) { |
| return 'ts'; |
| } |
| } else if (nodeType === 'FunctionDeclaration') { |
| if (AT.punctuator(token, ',)')) { |
| return 'ts'; |
| } |
| } else if (nodeType === 'FunctionExpression') { |
| if (AT.punctuator(token, ',)')) { |
| return 'ts'; |
| } |
| if (AT.keyword(token, 'function')) { |
| return node.id ? 'ts' : 't'; |
| } |
| } else if (nodeType === 'ArrowFunctionExpression') { |
| if (AT.punctuator(token, ',)')) { |
| return 'ts'; |
| } |
| if (AT.punctuator(token, '(')) { |
| return 'st'; |
| } |
| if (AT.arrowIdentifier(token, '=>')) { |
| return 'sts'; |
| } |
| } else if (nodeType === 'WithStatement') { |
| if (AT.punctuator(token, ')')) { |
| return node.body?.type === 'BlockStatement' ? 'ts' : 'tn>'; |
| } |
| } else if (nodeType === 'SwitchStatement') { |
| if (AT.punctuator(token, '{')) { |
| return 'tn>'; |
| } |
| if (AT.punctuator(token, '}')) { |
| return 'n<tn'; |
| } |
| if (AT.punctuator(token, ')')) { |
| return 'ts'; |
| } |
| } else if (nodeType === 'SwitchCase') { |
| if (AT.keyword(token, 'case')) { |
| return 'n<ts'; |
| } |
| if (AT.keyword(token, 'default')) { |
| return 'n<t'; |
| } |
| if (AT.punctuator(token, ':')) { |
| return 'tn>'; |
| } |
| } else if (nodeType === 'VariableDeclaration') { |
| if (AT.punctuator(token, ',')) { |
| let allVariablesInitialized = true; |
| const declarations = (node.declarations as Acorn.ESTree.Node[]); |
| for (let i = 0; i < declarations.length; ++i) { |
| // @ts-expect-error We are doing a subtype check, without properly checking whether |
| // it exists. We can't fix that, unless we use proper typechecking |
| allVariablesInitialized = allVariablesInitialized && Boolean(declarations[i].init); |
| } |
| return !this.#inForLoopHeader(node) && allVariablesInitialized ? 'nSSts' : 'ts'; |
| } |
| } else if (nodeType === 'PropertyDefinition') { |
| if (AT.punctuator(token, '=')) { |
| return 'sts'; |
| } |
| if (AT.punctuator(token, ';')) { |
| return 'tn'; |
| } |
| } else if (nodeType === 'BlockStatement') { |
| if (AT.punctuator(token, '{')) { |
| return node.body.length ? 'tn>' : 't'; |
| } |
| if (AT.punctuator(token, '}')) { |
| return node.body.length ? 'n<t' : 't'; |
| } |
| } else if (nodeType === 'CatchClause') { |
| if (AT.punctuator(token, ')')) { |
| return 'ts'; |
| } |
| } else if (nodeType === 'ObjectExpression') { |
| if (!node.properties.length) { |
| return 't'; |
| } |
| if (AT.punctuator(token, '{')) { |
| return 'tn>'; |
| } |
| if (AT.punctuator(token, '}')) { |
| return 'n<t'; |
| } |
| if (AT.punctuator(token, ',')) { |
| return 'tn'; |
| } |
| } else if (nodeType === 'IfStatement') { |
| if (AT.punctuator(token, ')')) { |
| return node.consequent?.type === 'BlockStatement' ? 'ts' : 'tn>'; |
| } |
| |
| if (AT.keyword(token, 'else')) { |
| const preFormat = node.consequent?.type === 'BlockStatement' ? 'st' : 'n<t'; |
| let postFormat = 'n>'; |
| if (node.alternate && (node.alternate.type === 'BlockStatement' || node.alternate.type === 'IfStatement')) { |
| postFormat = 's'; |
| } |
| return preFormat + postFormat; |
| } |
| } else if (nodeType === 'CallExpression') { |
| if (AT.punctuator(token, ',')) { |
| return 'ts'; |
| } |
| } else if (nodeType === 'SequenceExpression' && AT.punctuator(token, ',')) { |
| return node.parent?.type === 'SwitchCase' ? 'ts' : 'tn'; |
| } else if (nodeType === 'ForStatement' || nodeType === 'ForOfStatement' || nodeType === 'ForInStatement') { |
| if (AT.punctuator(token, ';')) { |
| return 'ts'; |
| } |
| if (AT.keyword(token, 'in') || AT.identifier(token, 'of')) { |
| return 'sts'; |
| } |
| |
| if (AT.punctuator(token, ')')) { |
| return node.body?.type === 'BlockStatement' ? 'ts' : 'tn>'; |
| } |
| } else if (nodeType === 'WhileStatement') { |
| if (AT.punctuator(token, ')')) { |
| return node.body?.type === 'BlockStatement' ? 'ts' : 'tn>'; |
| } |
| } else if (nodeType === 'DoWhileStatement') { |
| const blockBody = node.body?.type === 'BlockStatement'; |
| if (AT.keyword(token, 'do')) { |
| return blockBody ? 'ts' : 'tn>'; |
| } |
| if (AT.keyword(token, 'while')) { |
| return blockBody ? 'sts' : 'n<ts'; |
| } |
| if (AT.punctuator(token, ';')) { |
| return 'tn'; |
| } |
| } else if (nodeType === 'ClassBody') { |
| if (AT.punctuator(token, '{')) { |
| return 'stn>'; |
| } |
| if (AT.punctuator(token, '}')) { |
| return '<ntn'; |
| } |
| return 't'; |
| } else if (nodeType === 'YieldExpression') { |
| return 't'; |
| } else if (nodeType === 'Super') { |
| return 't'; |
| } else if (nodeType === 'ImportExpression') { |
| return 't'; |
| } else if (nodeType === 'ExportAllDeclaration') { |
| if (AT.punctuator(token, '*')) { |
| return 'sts'; |
| } |
| return 't'; |
| } else if (nodeType === 'ExportNamedDeclaration' || nodeType === 'ImportDeclaration') { |
| if (AT.punctuator(token, '{')) { |
| return 'st'; |
| } |
| if (AT.punctuator(token, ',')) { |
| return 'ts'; |
| } |
| if (AT.punctuator(token, '}')) { |
| return node.source ? 'ts' : 't'; |
| } |
| if (AT.punctuator(token, '*')) { |
| return 'sts'; |
| } |
| return 't'; |
| } else if (nodeType === 'MemberExpression') { |
| if (node.object.type === 'Literal' && typeof (node.object.value) === 'number') { |
| return 'st'; |
| } |
| return 't'; |
| } |
| return AT.keyword(token) && !AT.keyword(token, 'this') ? 'ts' : 't'; |
| } |
| |
| #finishNode(node: Acorn.ESTree.Node): string { |
| const nodeType = node.type; |
| if (nodeType === 'WithStatement') { |
| if (node.body && node.body.type !== 'BlockStatement') { |
| return 'n<'; |
| } |
| } else if (nodeType === 'VariableDeclaration') { |
| if (!this.#inForLoopHeader(node)) { |
| return 'n'; |
| } |
| } else if (nodeType === 'ForStatement' || nodeType === 'ForOfStatement' || nodeType === 'ForInStatement') { |
| if (node.body && node.body.type !== 'BlockStatement') { |
| return 'n<'; |
| } |
| } else if (nodeType === 'BlockStatement') { |
| if (node.parent?.type === 'IfStatement') { |
| const parentNode = (node.parent as Acorn.ESTree.IfStatement); |
| if (parentNode.alternate && parentNode.consequent === node) { |
| return ''; |
| } |
| } |
| if (node.parent?.type === 'FunctionExpression' && node.parent.parent?.type === 'Property') { |
| return ''; |
| } |
| if (node.parent?.type === 'FunctionExpression' && node.parent.parent?.type === 'VariableDeclarator') { |
| return ''; |
| } |
| if (node.parent?.type === 'FunctionExpression' && node.parent.parent?.type === 'CallExpression') { |
| return ''; |
| } |
| if (node.parent?.type === 'DoWhileStatement') { |
| return ''; |
| } |
| if (node.parent?.type === 'TryStatement') { |
| const parentNode = (node.parent as Acorn.ESTree.TryStatement); |
| if (parentNode.block === node) { |
| return 's'; |
| } |
| } |
| if (node.parent?.type === 'CatchClause') { |
| const parentNode = (node.parent as Acorn.ESTree.CatchClause); |
| // @ts-expect-error We are doing a subtype check, without properly checking whether |
| // it exists. We can't fix that, unless we use proper typechecking |
| if (parentNode.parent?.finalizer) { |
| return 's'; |
| } |
| } |
| return 'n'; |
| } else if (nodeType === 'WhileStatement') { |
| if (node.body && node.body.type !== 'BlockStatement') { |
| return 'n<'; |
| } |
| } else if (nodeType === 'IfStatement') { |
| if (node.alternate) { |
| if (node.alternate.type !== 'BlockStatement' && node.alternate.type !== 'IfStatement') { |
| return '<'; |
| } |
| } else if (node.consequent) { |
| if (node.consequent.type !== 'BlockStatement') { |
| return '<'; |
| } |
| } |
| } else if ( |
| nodeType === 'BreakStatement' || nodeType === 'ContinueStatement' || nodeType === 'ThrowStatement' || |
| nodeType === 'ReturnStatement' || nodeType === 'ExpressionStatement') { |
| return 'n'; |
| } else if ( |
| nodeType === 'ImportDeclaration' || nodeType === 'ExportAllDeclaration' || |
| nodeType === 'ExportDefaultDeclaration' || nodeType === 'ExportNamedDeclaration') { |
| return 'n'; |
| } |
| return ''; |
| } |
| } |