| // Copyright 2019 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 { parse, print, types } from 'recast'; | |
| import fs from 'fs'; | |
| import path from 'path'; | |
| import { promisify } from 'util'; | |
| import { IdentifierKind, MemberExpressionKind, ExpressionKind, CommentKind } from 'ast-types/gen/kinds'; | |
| const readFile = promisify(fs.readFile); | |
| const writeFile = promisify(fs.writeFile); | |
| const FRONT_END_FOLDER = path.join(__dirname, '..', '..', 'front_end') | |
| function capitalizeFirstLetter(string: string) { | |
| return string.charAt(0).toUpperCase() + string.slice(1); | |
| } | |
| const b = types.builders; | |
| function createReplacementDeclaration(propertyName: IdentifierKind, declaration: any): any { | |
| // UI.ARIAUtils.Foo = class {} -> export class Foo {} | |
| if (declaration.type === 'ClassExpression') { | |
| return b.exportDeclaration(false, b.classDeclaration(propertyName, declaration.body)); | |
| } | |
| // UI.ARIAUtils.Foo = expression; -> export const Foo = expression; | |
| if (declaration.type === 'Literal' || declaration.type.endsWith('Expression')) { | |
| return b.exportNamedDeclaration(b.variableDeclaration("const", [b.variableDeclarator(propertyName, declaration)])); | |
| } | |
| console.error(`Unable to refactor declaration of type "${declaration.type}" named "${propertyName.name}"`); | |
| } | |
| function getTopLevelMemberExpression(expression: MemberExpressionKind): any { | |
| while (expression.object.type === 'MemberExpression') { | |
| expression = expression.object; | |
| } | |
| return expression; | |
| } | |
| function rewriteSource(source: string, refactoringNamespace: string, refactoringFileName: string) { | |
| const exportedMembers: {prop: IdentifierKind, comments: CommentKind[]}[] = []; | |
| const ast = parse(source); | |
| ast.program.body = ast.program.body.map((expression: any) => { | |
| // UI.ARIAUtils.Foo = 5; | |
| if (expression.type === 'ExpressionStatement') { | |
| // UI.ARIAUtils.Foo = 5 | |
| if (expression.expression.type === 'AssignmentExpression') { | |
| const assignment = expression.expression; | |
| // UI.ARIAUtils.Foo | |
| if (assignment.left.type === 'MemberExpression') { | |
| // UI.ARIAUtils.Foo -> UI.ARIAUtils | |
| // UI.ARIAUtils.Nested.Foo -> UI.ARIAUtils | |
| const topLevelAssignment = getTopLevelMemberExpression(assignment.left); | |
| // If there is a nested export, such as `UI.ARIAUtils.Nested.Field` | |
| if (topLevelAssignment !== assignment.left.object) { | |
| // Exports itself. E.g. `UI.ARIAUtils = <...>` | |
| if (assignment.left.object.name === refactoringNamespace && assignment.left.property.name === refactoringFileName) { | |
| const {declaration} = createReplacementDeclaration(assignment.left.property, assignment.right); | |
| // If it is a variable declaration, we need to use the actual variabledeclator. E.g.: | |
| // UI.ARIAUtils = 5; -> export ARIAUtils = 5; instead of "export const ARIAUtils = 5;" | |
| const declarationStatement = b.exportDefaultDeclaration(declaration.type === 'VariableDeclaration' ? declaration.declarations[0].init : declaration); | |
| declarationStatement.comments = expression.comments; | |
| return declarationStatement; | |
| } | |
| console.error(`Nested field "${assignment.left.property.name}" detected! Requires manual changes.`); | |
| expression.comments = expression.comments || []; | |
| expression.comments.push(b.commentLine(' TODO(http://crbug.com/1006759): Fix exported symbol')); | |
| return expression; | |
| } | |
| const propertyName = assignment.left.property; | |
| const {object, property} = topLevelAssignment; | |
| if (object.type === 'Identifier' && property.type === 'Identifier') { | |
| // UI | |
| const namespace = object.name; | |
| // ARIAUtils | |
| const fileName = property.name; | |
| if (namespace === refactoringNamespace && fileName === refactoringFileName) { | |
| const declaration = createReplacementDeclaration(propertyName, assignment.right); | |
| if (declaration) { | |
| exportedMembers.push({prop: propertyName, comments: expression.comments || [b.commentLine(' TODO(http://crbug.com/1006759): Add type information if necessary')]}); | |
| declaration.comments = expression.comments; | |
| return declaration; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| return expression; | |
| }); | |
| // self.UI = self.UI || {}; | |
| { | |
| const legacyNamespaceName = b.memberExpression(b.identifier('self'), b.identifier(refactoringNamespace), false); | |
| const legacyNamespaceOr = b.logicalExpression("||", legacyNamespaceName, b.objectExpression([])); | |
| ast.program.body.push(b.expressionStatement.from({ | |
| expression: b.assignmentExpression('=', legacyNamespaceName, legacyNamespaceOr), | |
| comments: [b.commentBlock(' Legacy exported object ', true, false)] | |
| })); | |
| } | |
| // UI = UI || {}; | |
| const legacyNamespaceName = b.identifier(refactoringNamespace) | |
| { | |
| const legacyNamespaceOr = b.logicalExpression("||", legacyNamespaceName, b.objectExpression([])); | |
| ast.program.body.push(b.expressionStatement.from({ | |
| expression: b.assignmentExpression('=', legacyNamespaceName, legacyNamespaceOr), | |
| comments: [b.commentBlock(' Legacy exported object ', true, false)] | |
| })); | |
| } | |
| // UI.ARIAUtils = ARIAUtils; | |
| const legacyNamespaceExport = b.memberExpression(b.identifier(refactoringNamespace), b.identifier(refactoringFileName), false); | |
| ast.program.body.push(b.expressionStatement(b.assignmentExpression.from({ | |
| operator: '=', | |
| left: legacyNamespaceExport, | |
| right: b.identifier(refactoringFileName), | |
| comments: [b.commentLine(' TODO(http://crbug.com/1006759): Add type information if necessary')] | |
| }))); | |
| // UI.ARIAUtils.Foo = Foo; | |
| exportedMembers.forEach(({prop, comments}) => { | |
| const legacyExportedProperty = b.memberExpression(legacyNamespaceExport, b.identifier(prop.name), false); | |
| ast.program.body.push(b.expressionStatement(b.assignmentExpression.from({ | |
| operator: '=', | |
| left: legacyExportedProperty, | |
| right: b.identifier(prop.name), | |
| comments, | |
| }))); | |
| }); | |
| return print(ast).code; | |
| } | |
| const FOLDER_MAPPING: {[name: string]: string} = require(path.join('..', 'build', 'special_case_namespaces.json')); | |
| function computeNamespaceName(folderName: string): string { | |
| if (folderName in FOLDER_MAPPING) { | |
| return FOLDER_MAPPING[folderName]; | |
| } | |
| return capitalizeFirstLetter(folderName); | |
| } | |
| async function main(refactoringNamespace: string, refactoringFileName: string) { | |
| const pathName = path.join(FRONT_END_FOLDER, refactoringNamespace, `${refactoringFileName}.js`); | |
| const source = await readFile(pathName, {encoding: 'utf-8'}); | |
| const rewrittenSource = rewriteSource(source, computeNamespaceName(process.argv[2]), refactoringFileName); | |
| await writeFile(pathName, rewrittenSource); | |
| // console.log(`Succesfully written source to "${pathName}". Make sure that no other errors are reported before submitting!`); | |
| } | |
| if (!process.argv[2]) { | |
| console.error(`No arguments specified. Run this script with "<folder-name> <filename>". For example: "common Color"`); | |
| process.exit(1); | |
| } | |
| main(process.argv[2], process.argv[3]); |