blob: f0a19349b6fa1ffa6e909f22e79851bbc90c48fb [file] [log] [blame]
// 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.
// eslint-disable-next-line rulesdir/es_modules_import
import {CommentKind, IdentifierKind, MemberExpressionKind} from 'ast-types/gen/kinds';
import fs from 'fs';
import path from 'path';
import {parse, print, types} from 'recast';
import {promisify} from 'util';
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;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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}"`);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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]);