Converter to Polymer2 classes.
See readme.txt for more information about usages.
Change-Id: I60843b32bc56faa04ad2b70898ab1f5535a2ad71
diff --git a/tools/polygerrit-updater/.gitignore b/tools/polygerrit-updater/.gitignore
new file mode 100644
index 0000000..8619a37
--- /dev/null
+++ b/tools/polygerrit-updater/.gitignore
@@ -0,0 +1,3 @@
+/.idea/
+/node_modules/
+/js/
\ No newline at end of file
diff --git a/tools/polygerrit-updater/package-lock.json b/tools/polygerrit-updater/package-lock.json
new file mode 100644
index 0000000..9256997
--- /dev/null
+++ b/tools/polygerrit-updater/package-lock.json
@@ -0,0 +1,18 @@
+{
+ "name": "polygerrit-updater",
+ "version": "1.0.0",
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "@types/node": {
+ "version": "12.7.12",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.12.tgz",
+ "integrity": "sha512-KPYGmfD0/b1eXurQ59fXD1GBzhSQfz6/lKBxkaHX9dKTzjXbK68Zt7yGUxUsCS1jeTy/8aL+d9JEr+S54mpkWQ=="
+ },
+ "typescript": {
+ "version": "3.6.4",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.4.tgz",
+ "integrity": "sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg=="
+ }
+ }
+}
diff --git a/tools/polygerrit-updater/package.json b/tools/polygerrit-updater/package.json
new file mode 100644
index 0000000..3609dad
--- /dev/null
+++ b/tools/polygerrit-updater/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "polygerrit-updater",
+ "version": "1.0.0",
+ "description": "Polygerrit source code updater",
+ "scripts": {
+ "compile": "tsc",
+ "convert": "npm run compile && node js/src/index.js"
+ },
+ "author": "",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/node": "^12.7.12",
+ "typescript": "^3.6.4"
+ }
+}
diff --git a/tools/polygerrit-updater/readme.txt b/tools/polygerrit-updater/readme.txt
new file mode 100644
index 0000000..2b2cea8
--- /dev/null
+++ b/tools/polygerrit-updater/readme.txt
@@ -0,0 +1,56 @@
+This folder contains tool to update Polymer components to class based components.
+This is a temporary tools, it will be removed in a few weeks.
+
+How to use this tool: initial steps
+1) Important - Commit and push all your changes. Otherwise, you can loose you work.
+
+2) Ensure, that tools/polygerrit-updater is your current directory
+
+3) Run
+npm install
+
+4) If you want to convert the whole project, run
+npm run convert -- --i \
+ --root ../../polygerrit-ui --src app/elements --r \
+ --exclude app/elements/core/gr-reporting/gr-reporting.js \
+ app/elements/diff/gr-comment-api/gr-comment-api-mock.js \
+ app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
+
+You can convert only specific files (can be useful if you want to convert some files in your change)
+npm run convert -- --i \
+ --root ../../polygerrit-ui
+ --src app/elements/file1.js \
+ app/elements/folder/file2.js
+
+4) Search for the following string in all .js files:
+//This file has the following problems with comments:
+
+If you find such string in a .js file - you must manually fix comments in this file.
+(It is expected that you shouldn't have such problems)
+
+5) Go to the gerrit root folder and run
+npm run eslintfix
+
+(If you are doing it for the first time, run the following command before in gerrit root folder:
+npm run install)
+
+Fix error after eslintfix (if exists)
+
+6) If you are doing conversion for the whole project, make the followin changes:
+
+a) Add
+<link rel="import" href="../../../types/polymer-behaviors.js">
+to
+polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html
+
+b) Update polymer.json with the following rules:
+ "lint": {
+ "rules": ["polymer-2"],
+ "ignoreWarnings": ["deprecated-dom-call"]
+ }
+
+
+
+5) Commit changed files.
+
+6) You can update excluded files later.
diff --git a/tools/polygerrit-updater/src/funcToClassConversion/funcToClassBasedElementConverter.ts b/tools/polygerrit-updater/src/funcToClassConversion/funcToClassBasedElementConverter.ts
new file mode 100644
index 0000000..b92a6e9
--- /dev/null
+++ b/tools/polygerrit-updater/src/funcToClassConversion/funcToClassBasedElementConverter.ts
@@ -0,0 +1,131 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {LegacyLifecycleMethodsArray, LegacyPolymerComponent} from './polymerComponentParser';
+import {LifecycleMethodsBuilder} from './lifecycleMethodsBuilder';
+import {ClassBasedPolymerElement, PolymerElementBuilder} from './polymerElementBuilder';
+import * as codeUtils from '../utils/codeUtils';
+import * as ts from 'typescript';
+
+export class PolymerFuncToClassBasedConverter {
+ public static convert(component: LegacyPolymerComponent): ClassBasedPolymerElement {
+ const legacySettings = component.componentSettings;
+ const reservedDeclarations = legacySettings.reservedDeclarations;
+
+ if(!reservedDeclarations.is) {
+ throw new Error("Legacy component doesn't have 'is' property");
+ }
+ const className = this.generateClassNameFromTagName(reservedDeclarations.is.data);
+ const updater = new PolymerElementBuilder(component, className);
+ updater.addIsAccessor(reservedDeclarations.is.data);
+
+ if(reservedDeclarations.properties) {
+ updater.addPolymerPropertiesAccessor(reservedDeclarations.properties);
+ }
+
+ updater.addMixin("Polymer.Element");
+ updater.addMixin("Polymer.LegacyElementMixin");
+ updater.addMixin("Polymer.GestureEventListeners");
+
+ if(reservedDeclarations._legacyUndefinedCheck) {
+ updater.addMixin("Polymer.LegacyDataMixin");
+ }
+
+ if(reservedDeclarations.behaviors) {
+ updater.addMixin("Polymer.mixinBehaviors", [reservedDeclarations.behaviors.data]);
+ const mixinNames = this.getMixinNamesFromBehaviors(reservedDeclarations.behaviors.data);
+ const jsDocLines = mixinNames.map(mixinName => {
+ return `@appliesMixin ${mixinName}`;
+ });
+ updater.addClassJSDocComments(jsDocLines);
+ }
+
+ if(reservedDeclarations.observers) {
+ updater.addPolymerPropertiesObservers(reservedDeclarations.observers.data);
+ }
+
+ if(reservedDeclarations.keyBindings) {
+ updater.addKeyBindings(reservedDeclarations.keyBindings.data);
+ }
+
+
+ const lifecycleBuilder = new LifecycleMethodsBuilder();
+ if (reservedDeclarations.listeners) {
+ lifecycleBuilder.addListeners(reservedDeclarations.listeners.data, legacySettings.ordinaryMethods);
+ }
+
+ if (reservedDeclarations.hostAttributes) {
+ lifecycleBuilder.addHostAttributes(reservedDeclarations.hostAttributes.data);
+ }
+
+ for(const name of LegacyLifecycleMethodsArray) {
+ const existingMethod = legacySettings.lifecycleMethods.get(name);
+ if(existingMethod) {
+ lifecycleBuilder.addLegacyLifecycleMethod(name, existingMethod)
+ }
+ }
+
+ const newLifecycleMethods = lifecycleBuilder.buildNewMethods();
+ updater.addLifecycleMethods(newLifecycleMethods);
+
+
+ updater.addOrdinaryMethods(legacySettings.ordinaryMethods);
+ updater.addOrdinaryGetAccessors(legacySettings.ordinaryGetAccessors);
+ updater.addOrdinaryShorthandProperties(legacySettings.ordinaryShorthandProperties);
+ updater.addOrdinaryPropertyAssignments(legacySettings.ordinaryPropertyAssignments);
+
+ return updater.build();
+ }
+
+ private static generateClassNameFromTagName(tagName: string) {
+ let result = "";
+ let nextUppercase = true;
+ for(const ch of tagName) {
+ if (ch === '-') {
+ nextUppercase = true;
+ continue;
+ }
+ result += nextUppercase ? ch.toUpperCase() : ch;
+ nextUppercase = false;
+ }
+ return result;
+ }
+
+ private static getMixinNamesFromBehaviors(behaviors: ts.ArrayLiteralExpression): string[] {
+ return behaviors.elements.map((expression) => {
+ const propertyAccessExpression = codeUtils.assertNodeKind(expression, ts.SyntaxKind.PropertyAccessExpression) as ts.PropertyAccessExpression;
+ const namespaceName = codeUtils.assertNodeKind(propertyAccessExpression.expression, ts.SyntaxKind.Identifier) as ts.Identifier;
+ const behaviorName = propertyAccessExpression.name;
+ if(namespaceName.text === 'Gerrit') {
+ let behaviorNameText = behaviorName.text;
+ const suffix = 'Behavior';
+ if(behaviorNameText.endsWith(suffix)) {
+ behaviorNameText =
+ behaviorNameText.substr(0, behaviorNameText.length - suffix.length);
+ }
+ const mixinName = behaviorNameText + 'Mixin';
+ return `${namespaceName.text}.${mixinName}`
+ } else if(namespaceName.text === 'Polymer') {
+ let behaviorNameText = behaviorName.text;
+ if(behaviorNameText === "IronFitBehavior") {
+ return "Polymer.IronFitMixin";
+ } else if(behaviorNameText === "IronOverlayBehavior") {
+ return "";
+ }
+ throw new Error(`Unsupported behavior: ${propertyAccessExpression.getText()}`);
+ }
+ throw new Error(`Unsupported behavior name ${expression.getFullText()}`)
+ }).filter(name => name.length > 0);
+ }
+}
diff --git a/tools/polygerrit-updater/src/funcToClassConversion/legacyPolymerFuncReplacer.ts b/tools/polygerrit-updater/src/funcToClassConversion/legacyPolymerFuncReplacer.ts
new file mode 100644
index 0000000..57b7b8d
--- /dev/null
+++ b/tools/polygerrit-updater/src/funcToClassConversion/legacyPolymerFuncReplacer.ts
@@ -0,0 +1,74 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import * as ts from 'typescript';
+import * as codeUtils from '../utils/codeUtils'
+import {LegacyPolymerComponent} from './polymerComponentParser';
+import {ClassBasedPolymerElement} from './polymerElementBuilder';
+
+export class LegacyPolymerFuncReplaceResult {
+ public constructor(
+ private readonly transformationResult: ts.TransformationResult<ts.SourceFile>,
+ public readonly leadingComments: string[]) {
+ }
+ public get file(): ts.SourceFile {
+ return this.transformationResult.transformed[0];
+ }
+ public dispose() {
+ this.transformationResult.dispose();
+ }
+
+}
+
+export class LegacyPolymerFuncReplacer {
+ private readonly callStatement: ts.ExpressionStatement;
+ private readonly parentBlock: ts.Block;
+ private readonly callStatementIndexInBlock: number;
+ public constructor(private readonly legacyComponent: LegacyPolymerComponent) {
+ this.callStatement = codeUtils.assertNodeKind(legacyComponent.polymerFuncCallExpr.parent, ts.SyntaxKind.ExpressionStatement);
+ this.parentBlock = codeUtils.assertNodeKind(this.callStatement.parent, ts.SyntaxKind.Block);
+ this.callStatementIndexInBlock = this.parentBlock.statements.indexOf(this.callStatement);
+ if(this.callStatementIndexInBlock < 0) {
+ throw new Error("Internal error! Couldn't find statement in its own parent");
+ }
+ }
+ public replace(classBasedElement: ClassBasedPolymerElement): LegacyPolymerFuncReplaceResult {
+ const classDeclarationWithComments = this.appendLeadingCommentToClassDeclaration(classBasedElement.classDeclaration);
+ return new LegacyPolymerFuncReplaceResult(
+ this.replaceLegacyPolymerFunction(classDeclarationWithComments.classDeclarationWithCommentsPlaceholder, classBasedElement.componentRegistration),
+ classDeclarationWithComments.leadingComments);
+ }
+ private appendLeadingCommentToClassDeclaration(classDeclaration: ts.ClassDeclaration): {classDeclarationWithCommentsPlaceholder: ts.ClassDeclaration, leadingComments: string[]} {
+ const text = this.callStatement.getFullText();
+ let classDeclarationWithCommentsPlaceholder = classDeclaration;
+ const leadingComments: string[] = [];
+ ts.forEachLeadingCommentRange(text, 0, (pos, end, kind, hasTrailingNewLine) => {
+ classDeclarationWithCommentsPlaceholder = codeUtils.addReplacableCommentBeforeNode(classDeclarationWithCommentsPlaceholder, String(leadingComments.length));
+ leadingComments.push(text.substring(pos, end));
+ });
+ return {
+ classDeclarationWithCommentsPlaceholder: classDeclarationWithCommentsPlaceholder,
+ leadingComments: leadingComments
+ }
+ }
+ private replaceLegacyPolymerFunction(classDeclaration: ts.ClassDeclaration, componentRegistration: ts.ExpressionStatement): ts.TransformationResult<ts.SourceFile> {
+ const newStatements = Array.from(this.parentBlock.statements);
+ newStatements.splice(this.callStatementIndexInBlock, 1, classDeclaration, componentRegistration);
+
+ const updatedBlock = ts.getMutableClone(this.parentBlock);
+ updatedBlock.statements = ts.createNodeArray(newStatements);
+ return codeUtils.replaceNode(this.legacyComponent.parsedFile, this.parentBlock, updatedBlock);
+
+ }
+}
\ No newline at end of file
diff --git a/tools/polygerrit-updater/src/funcToClassConversion/lifecycleMethodsBuilder.ts b/tools/polygerrit-updater/src/funcToClassConversion/lifecycleMethodsBuilder.ts
new file mode 100644
index 0000000..e9e13f5
--- /dev/null
+++ b/tools/polygerrit-updater/src/funcToClassConversion/lifecycleMethodsBuilder.ts
@@ -0,0 +1,140 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import * as ts from 'typescript';
+import * as codeUtils from '../utils/codeUtils';
+import {LegacyLifecycleMethodName, OrdinaryMethods} from './polymerComponentParser';
+
+interface LegacyLifecycleMethodContent {
+ codeAtMethodStart: ts.Statement[];
+ existingMethod?: ts.MethodDeclaration;
+ codeAtMethodEnd: ts.Statement[];
+}
+
+export interface LifecycleMethod {
+ originalPos: number;//-1 - no original method exists
+ method: ts.MethodDeclaration;
+ name: LegacyLifecycleMethodName;
+}
+
+export class LifecycleMethodsBuilder {
+ private readonly methods: Map<LegacyLifecycleMethodName, LegacyLifecycleMethodContent> = new Map();
+
+ private getMethodContent(name: LegacyLifecycleMethodName): LegacyLifecycleMethodContent {
+ if(!this.methods.has(name)) {
+ this.methods.set(name, {
+ codeAtMethodStart: [],
+ codeAtMethodEnd: []
+ });
+ }
+ return this.methods.get(name)!;
+ }
+
+ public addListeners(legacyListeners: ts.ObjectLiteralExpression, legacyOrdinaryMethods: OrdinaryMethods) {
+ for(const listener of legacyListeners.properties) {
+ const propertyAssignment = codeUtils.assertNodeKind(listener, ts.SyntaxKind.PropertyAssignment) as ts.PropertyAssignment;
+ if(!propertyAssignment.name) {
+ throw new Error("Listener must have event name");
+ }
+ let eventNameLiteral: ts.StringLiteral;
+ let commentsToRestore: string[] = [];
+ if(propertyAssignment.name.kind === ts.SyntaxKind.StringLiteral) {
+ //We don't loose comment in this case, because we keep literal as is
+ eventNameLiteral = propertyAssignment.name;
+ } else if(propertyAssignment.name.kind === ts.SyntaxKind.Identifier) {
+ eventNameLiteral = ts.createStringLiteral(propertyAssignment.name.text);
+ commentsToRestore = codeUtils.getLeadingComments(propertyAssignment);
+ } else {
+ throw new Error(`Unsupported type ${ts.SyntaxKind[propertyAssignment.name.kind]}`);
+ }
+
+ const handlerLiteral = codeUtils.assertNodeKind(propertyAssignment.initializer, ts.SyntaxKind.StringLiteral) as ts.StringLiteral;
+ const handlerImpl = legacyOrdinaryMethods.get(handlerLiteral.text);
+ if(!handlerImpl) {
+ throw new Error(`Can't find event handler '${handlerLiteral.text}'`);
+ }
+ const eventHandlerAccess = ts.createPropertyAccess(ts.createThis(), handlerLiteral.text);
+ //ts.forEachChild(handler)
+ const args: ts.Identifier[] = handlerImpl.parameters.map((arg) => codeUtils.assertNodeKind(arg.name, ts.SyntaxKind.Identifier));
+ const eventHandlerCall = ts.createCall(eventHandlerAccess, [], args);
+ let arrowFunc = ts.createArrowFunction([], [], handlerImpl.parameters, undefined, undefined, eventHandlerCall);
+ arrowFunc = codeUtils.addNewLineBeforeNode(arrowFunc);
+
+ const methodContent = this.getMethodContent("created");
+ //See https://polymer-library.polymer-project.org/3.0/docs/devguide/gesture-events for a list of events
+ if(["down", "up", "tap", "track"].indexOf(eventNameLiteral.text) >= 0) {
+ const methodCall = ts.createCall(codeUtils.createNameExpression("Polymer.Gestures.addListener"), [], [ts.createThis(), eventNameLiteral, arrowFunc]);
+ methodContent.codeAtMethodEnd.push(ts.createExpressionStatement(methodCall));
+ }
+ else {
+ let methodCall = ts.createCall(ts.createPropertyAccess(ts.createThis(), "addEventListener"), [], [eventNameLiteral, arrowFunc]);
+ methodCall = codeUtils.restoreLeadingComments(methodCall, commentsToRestore);
+ methodContent.codeAtMethodEnd.push(ts.createExpressionStatement(methodCall));
+ }
+ }
+ }
+
+ public addHostAttributes(legacyHostAttributes: ts.ObjectLiteralExpression) {
+ for(const listener of legacyHostAttributes.properties) {
+ const propertyAssignment = codeUtils.assertNodeKind(listener, ts.SyntaxKind.PropertyAssignment) as ts.PropertyAssignment;
+ if(!propertyAssignment.name) {
+ throw new Error("Listener must have event name");
+ }
+ let attributeNameLiteral: ts.StringLiteral;
+ if(propertyAssignment.name.kind === ts.SyntaxKind.StringLiteral) {
+ attributeNameLiteral = propertyAssignment.name;
+ } else if(propertyAssignment.name.kind === ts.SyntaxKind.Identifier) {
+ attributeNameLiteral = ts.createStringLiteral(propertyAssignment.name.text);
+ } else {
+ throw new Error(`Unsupported type ${ts.SyntaxKind[propertyAssignment.name.kind]}`);
+ }
+ let attributeValueLiteral: ts.StringLiteral | ts.NumericLiteral;
+ if(propertyAssignment.initializer.kind === ts.SyntaxKind.StringLiteral) {
+ attributeValueLiteral = propertyAssignment.initializer as ts.StringLiteral;
+ } else if(propertyAssignment.initializer.kind === ts.SyntaxKind.NumericLiteral) {
+ attributeValueLiteral = propertyAssignment.initializer as ts.NumericLiteral;
+ } else {
+ throw new Error(`Unsupported type ${ts.SyntaxKind[propertyAssignment.initializer.kind]}`);
+ }
+ const methodCall = ts.createCall(ts.createPropertyAccess(ts.createThis(), "_ensureAttribute"), [], [attributeNameLiteral, attributeValueLiteral]);
+ this.getMethodContent("ready").codeAtMethodEnd.push(ts.createExpressionStatement(methodCall));
+ }
+ }
+
+ public addLegacyLifecycleMethod(name: LegacyLifecycleMethodName, method: ts.MethodDeclaration) {
+ const content = this.getMethodContent(name);
+ if(content.existingMethod) {
+ throw new Error(`Legacy lifecycle method ${name} already added`);
+ }
+ content.existingMethod = method;
+ }
+
+ public buildNewMethods(): LifecycleMethod[] {
+ const result = [];
+ for(const [name, content] of this.methods) {
+ const newMethod = this.createLifecycleMethod(name, content.existingMethod, content.codeAtMethodStart, content.codeAtMethodEnd);
+ if(!newMethod) continue;
+ result.push({
+ name,
+ originalPos: content.existingMethod ? content.existingMethod.pos : -1,
+ method: newMethod
+ })
+ }
+ return result;
+ }
+
+ private createLifecycleMethod(name: string, methodDecl: ts.MethodDeclaration | undefined, codeAtStart: ts.Statement[], codeAtEnd: ts.Statement[]): ts.MethodDeclaration | undefined {
+ return codeUtils.createMethod(name, methodDecl, codeAtStart, codeAtEnd, true);
+ }
+}
\ No newline at end of file
diff --git a/tools/polygerrit-updater/src/funcToClassConversion/polymerComponentParser.ts b/tools/polygerrit-updater/src/funcToClassConversion/polymerComponentParser.ts
new file mode 100644
index 0000000..6006608
--- /dev/null
+++ b/tools/polygerrit-updater/src/funcToClassConversion/polymerComponentParser.ts
@@ -0,0 +1,301 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import * as ts from "typescript";
+import * as fs from "fs";
+import * as path from "path";
+import { unexpectedValue } from "../utils/unexpectedValue";
+import * as codeUtils from "../utils/codeUtils";
+import {CommentsParser} from '../utils/commentsParser';
+
+export class LegacyPolymerComponentParser {
+ public constructor(private readonly rootDir: string, private readonly htmlFiles: Set<string>) {
+ }
+ public async parse(jsFile: string): Promise<ParsedPolymerComponent | null> {
+ const sourceFile: ts.SourceFile = this.parseJsFile(jsFile);
+ const legacyComponent = this.tryParseLegacyComponent(sourceFile);
+ if (legacyComponent) {
+ return legacyComponent;
+ }
+ return null;
+ }
+ private parseJsFile(jsFile: string): ts.SourceFile {
+ return ts.createSourceFile(jsFile, fs.readFileSync(path.resolve(this.rootDir, jsFile)).toString(), ts.ScriptTarget.ES2015, true);
+ }
+
+ private tryParseLegacyComponent(sourceFile: ts.SourceFile): ParsedPolymerComponent | null {
+ const polymerFuncCalls: ts.CallExpression[] = [];
+
+ function addPolymerFuncCall(node: ts.Node) {
+ if(node.kind === ts.SyntaxKind.CallExpression) {
+ const callExpression: ts.CallExpression = node as ts.CallExpression;
+ if(callExpression.expression.kind === ts.SyntaxKind.Identifier) {
+ const identifier = callExpression.expression as ts.Identifier;
+ if(identifier.text === "Polymer") {
+ polymerFuncCalls.push(callExpression);
+ }
+ }
+ }
+ ts.forEachChild(node, addPolymerFuncCall);
+ }
+
+ addPolymerFuncCall(sourceFile);
+
+
+ if (polymerFuncCalls.length === 0) {
+ return null;
+ }
+ if (polymerFuncCalls.length > 1) {
+ throw new Error("Each .js file must contain only one Polymer component");
+ }
+ const parsedPath = path.parse(sourceFile.fileName);
+ const htmlFullPath = path.format({
+ dir: parsedPath.dir,
+ name: parsedPath.name,
+ ext: ".html"
+ });
+ if (!this.htmlFiles.has(htmlFullPath)) {
+ throw new Error("Legacy .js component dosn't have associated .html file");
+ }
+
+ const polymerFuncCall = polymerFuncCalls[0];
+ if(polymerFuncCall.arguments.length !== 1) {
+ throw new Error("The Polymer function must be called with exactly one parameter");
+ }
+ const argument = polymerFuncCall.arguments[0];
+ if(argument.kind !== ts.SyntaxKind.ObjectLiteralExpression) {
+ throw new Error("The parameter for Polymer function must be ObjectLiteralExpression (i.e. '{...}')");
+ }
+ const infoArg = argument as ts.ObjectLiteralExpression;
+
+ return {
+ jsFile: sourceFile.fileName,
+ htmlFile: htmlFullPath,
+ parsedFile: sourceFile,
+ polymerFuncCallExpr: polymerFuncCalls[0],
+ componentSettings: this.parseLegacyComponentSettings(infoArg),
+ };
+ }
+
+ private parseLegacyComponentSettings(info: ts.ObjectLiteralExpression): LegacyPolymerComponentSettings {
+ const props: Map<string, ts.ObjectLiteralElementLike> = new Map();
+ for(const property of info.properties) {
+ const name = property.name;
+ if (name === undefined) {
+ throw new Error("Property name is not defined");
+ }
+ switch(name.kind) {
+ case ts.SyntaxKind.Identifier:
+ case ts.SyntaxKind.StringLiteral:
+ if (props.has(name.text)) {
+ throw new Error(`Property ${name.text} appears more than once`);
+ }
+ props.set(name.text, property);
+ break;
+ case ts.SyntaxKind.ComputedPropertyName:
+ continue;
+ default:
+ unexpectedValue(ts.SyntaxKind[name.kind]);
+ }
+ }
+
+ if(props.has("_noAccessors")) {
+ throw new Error("_noAccessors is not supported");
+ }
+
+ const legacyLifecycleMethods: LegacyLifecycleMethods = new Map();
+ for(const name of LegacyLifecycleMethodsArray) {
+ const methodDecl = this.getLegacyMethodDeclaration(props, name);
+ if(methodDecl) {
+ legacyLifecycleMethods.set(name, methodDecl);
+ }
+ }
+
+ const ordinaryMethods: OrdinaryMethods = new Map();
+ const ordinaryShorthandProperties: OrdinaryShorthandProperties = new Map();
+ const ordinaryGetAccessors: OrdinaryGetAccessors = new Map();
+ const ordinaryPropertyAssignments: OrdinaryPropertyAssignments = new Map();
+ for(const [name, val] of props) {
+ if(RESERVED_NAMES.hasOwnProperty(name)) continue;
+ switch(val.kind) {
+ case ts.SyntaxKind.MethodDeclaration:
+ ordinaryMethods.set(name, val as ts.MethodDeclaration);
+ break;
+ case ts.SyntaxKind.ShorthandPropertyAssignment:
+ ordinaryShorthandProperties.set(name, val as ts.ShorthandPropertyAssignment);
+ break;
+ case ts.SyntaxKind.GetAccessor:
+ ordinaryGetAccessors.set(name, val as ts.GetAccessorDeclaration);
+ break;
+ case ts.SyntaxKind.PropertyAssignment:
+ ordinaryPropertyAssignments.set(name, val as ts.PropertyAssignment);
+ break;
+ default:
+ throw new Error(`Unsupported element kind: ${ts.SyntaxKind[val.kind]}`);
+ }
+ //ordinaryMethods.set(name, tsUtils.assertNodeKind(val, ts.SyntaxKind.MethodDeclaration) as ts.MethodDeclaration);
+ }
+
+ const eventsComments: string[] = this.getEventsComments(info.getFullText());
+
+ return {
+ reservedDeclarations: {
+ is: this.getStringLiteralValueWithComments(this.getLegacyPropertyInitializer(props, "is")),
+ _legacyUndefinedCheck: this.getBooleanLiteralValueWithComments(this.getLegacyPropertyInitializer(props, "_legacyUndefinedCheck")),
+ properties: this.getObjectLiteralExpressionWithComments(this.getLegacyPropertyInitializer(props, "properties")),
+ behaviors: this.getArrayLiteralExpressionWithComments(this.getLegacyPropertyInitializer(props, "behaviors")),
+ observers: this.getArrayLiteralExpressionWithComments(this.getLegacyPropertyInitializer(props, "observers")),
+ listeners: this.getObjectLiteralExpressionWithComments(this.getLegacyPropertyInitializer(props, "listeners")),
+ hostAttributes: this.getObjectLiteralExpressionWithComments(this.getLegacyPropertyInitializer(props, "hostAttributes")),
+ keyBindings: this.getObjectLiteralExpressionWithComments(this.getLegacyPropertyInitializer(props, "keyBindings")),
+ },
+ eventsComments: eventsComments,
+ lifecycleMethods: legacyLifecycleMethods,
+ ordinaryMethods: ordinaryMethods,
+ ordinaryShorthandProperties: ordinaryShorthandProperties,
+ ordinaryGetAccessors: ordinaryGetAccessors,
+ ordinaryPropertyAssignments: ordinaryPropertyAssignments,
+ };
+ }
+
+ private convertLegacyProeprtyInitializer<T>(initializer: LegacyPropertyInitializer | undefined, converter: (exp: ts.Expression) => T): DataWithComments<T> | undefined {
+ if(!initializer) {
+ return undefined;
+ }
+ return {
+ data: converter(initializer.data),
+ leadingComments: initializer.leadingComments,
+ }
+ }
+
+ private getObjectLiteralExpressionWithComments(initializer: LegacyPropertyInitializer | undefined): DataWithComments<ts.ObjectLiteralExpression> | undefined {
+ return this.convertLegacyProeprtyInitializer(initializer,
+ expr => codeUtils.getObjectLiteralExpression(expr));
+ }
+
+ private getStringLiteralValueWithComments(initializer: LegacyPropertyInitializer | undefined): DataWithComments<string> | undefined {
+ return this.convertLegacyProeprtyInitializer(initializer,
+ expr => codeUtils.getStringLiteralValue(expr));
+ }
+
+ private getBooleanLiteralValueWithComments(initializer: LegacyPropertyInitializer | undefined): DataWithComments<boolean> | undefined {
+ return this.convertLegacyProeprtyInitializer(initializer,
+ expr => codeUtils.getBooleanLiteralValue(expr));
+ }
+
+
+ private getArrayLiteralExpressionWithComments(initializer: LegacyPropertyInitializer | undefined): DataWithComments<ts.ArrayLiteralExpression> | undefined {
+ return this.convertLegacyProeprtyInitializer(initializer,
+ expr => codeUtils.getArrayLiteralExpression(expr));
+ }
+
+ private getLegacyPropertyInitializer(props: Map<String, ts.ObjectLiteralElementLike>, propName: string): LegacyPropertyInitializer | undefined {
+ const property = props.get(propName);
+ if (!property) {
+ return undefined;
+ }
+ const assignment = codeUtils.getPropertyAssignment(property);
+ if (!assignment) {
+ return undefined;
+ }
+ const comments: string[] = codeUtils.getLeadingComments(property)
+ .filter(c => !this.isEventComment(c));
+ return {
+ data: assignment.initializer,
+ leadingComments: comments,
+ };
+ }
+
+ private isEventComment(comment: string): boolean {
+ return comment.indexOf('@event') >= 0;
+ }
+
+ private getEventsComments(polymerComponentSource: string): string[] {
+ return CommentsParser.collectAllComments(polymerComponentSource)
+ .filter(c => this.isEventComment(c));
+ }
+
+ private getLegacyMethodDeclaration(props: Map<String, ts.ObjectLiteralElementLike>, propName: string): ts.MethodDeclaration | undefined {
+ const property = props.get(propName);
+ if (!property) {
+ return undefined;
+ }
+ return codeUtils.assertNodeKind(property, ts.SyntaxKind.MethodDeclaration) as ts.MethodDeclaration;
+ }
+
+}
+
+export type ParsedPolymerComponent = LegacyPolymerComponent;
+
+export interface LegacyPolymerComponent {
+ jsFile: string;
+ htmlFile: string;
+ parsedFile: ts.SourceFile;
+ polymerFuncCallExpr: ts.CallExpression;
+ componentSettings: LegacyPolymerComponentSettings;
+}
+
+export interface LegacyReservedDeclarations {
+ is?: DataWithComments<string>;
+ _legacyUndefinedCheck?: DataWithComments<boolean>;
+ properties?: DataWithComments<ts.ObjectLiteralExpression>;
+ behaviors?: DataWithComments<ts.ArrayLiteralExpression>,
+ observers? :DataWithComments<ts.ArrayLiteralExpression>,
+ listeners? :DataWithComments<ts.ObjectLiteralExpression>,
+ hostAttributes?: DataWithComments<ts.ObjectLiteralExpression>,
+ keyBindings?: DataWithComments<ts.ObjectLiteralExpression>,
+}
+
+export const LegacyLifecycleMethodsArray = <const>["beforeRegister", "registered", "created", "ready", "attached" , "detached", "attributeChanged"];
+export type LegacyLifecycleMethodName = typeof LegacyLifecycleMethodsArray[number];
+export type LegacyLifecycleMethods = Map<LegacyLifecycleMethodName, ts.MethodDeclaration>;
+export type OrdinaryMethods = Map<string, ts.MethodDeclaration>;
+export type OrdinaryShorthandProperties = Map<string, ts.ShorthandPropertyAssignment>;
+export type OrdinaryGetAccessors = Map<string, ts.GetAccessorDeclaration>;
+export type OrdinaryPropertyAssignments = Map<string, ts.PropertyAssignment>;
+export type ReservedName = LegacyLifecycleMethodName | keyof LegacyReservedDeclarations;
+export const RESERVED_NAMES: {[x in ReservedName]: boolean} = {
+ attached: true,
+ detached: true,
+ ready: true,
+ created: true,
+ beforeRegister: true,
+ registered: true,
+ attributeChanged: true,
+ is: true,
+ _legacyUndefinedCheck: true,
+ properties: true,
+ behaviors: true,
+ observers: true,
+ listeners: true,
+ hostAttributes: true,
+ keyBindings: true,
+};
+
+export interface LegacyPolymerComponentSettings {
+ reservedDeclarations: LegacyReservedDeclarations;
+ lifecycleMethods: LegacyLifecycleMethods,
+ ordinaryMethods: OrdinaryMethods,
+ ordinaryShorthandProperties: OrdinaryShorthandProperties,
+ ordinaryGetAccessors: OrdinaryGetAccessors,
+ ordinaryPropertyAssignments: OrdinaryPropertyAssignments,
+ eventsComments: string[];
+}
+
+export interface DataWithComments<T> {
+ data: T;
+ leadingComments: string[];
+}
+
+type LegacyPropertyInitializer = DataWithComments<ts.Expression>;
\ No newline at end of file
diff --git a/tools/polygerrit-updater/src/funcToClassConversion/polymerElementBuilder.ts b/tools/polygerrit-updater/src/funcToClassConversion/polymerElementBuilder.ts
new file mode 100644
index 0000000..d6e113c
--- /dev/null
+++ b/tools/polygerrit-updater/src/funcToClassConversion/polymerElementBuilder.ts
@@ -0,0 +1,142 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {DataWithComments, LegacyPolymerComponent, LegacyReservedDeclarations, OrdinaryGetAccessors, OrdinaryMethods, OrdinaryPropertyAssignments, OrdinaryShorthandProperties} from './polymerComponentParser';
+import * as ts from 'typescript';
+import * as codeUtils from '../utils/codeUtils';
+import {LifecycleMethod} from './lifecycleMethodsBuilder';
+import {PolymerClassBuilder} from '../utils/polymerClassBuilder';
+import {SyntaxKind} from 'typescript';
+
+export interface ClassBasedPolymerElement {
+ classDeclaration: ts.ClassDeclaration;
+ componentRegistration: ts.ExpressionStatement;
+ eventsComments: string[];
+ generatedComments: string[];
+}
+
+export class PolymerElementBuilder {
+ private readonly reservedDeclarations: LegacyReservedDeclarations;
+ private readonly classBuilder: PolymerClassBuilder;
+ private mixins: ts.ExpressionWithTypeArguments | null;
+
+ public constructor(private readonly legacyComponent: LegacyPolymerComponent, className: string) {
+ this.reservedDeclarations = legacyComponent.componentSettings.reservedDeclarations;
+ this.classBuilder = new PolymerClassBuilder(className);
+ this.mixins = null;
+ }
+
+ public addIsAccessor(tagName: string) {
+ this.classBuilder.addIsAccessor(this.createIsAccessor(tagName));
+ }
+
+ public addPolymerPropertiesAccessor(legacyProperties: DataWithComments<ts.ObjectLiteralExpression>) {
+ const returnStatement = ts.createReturn(legacyProperties.data);
+ const block = ts.createBlock([returnStatement]);
+ let propertiesAccessor = ts.createGetAccessor(undefined, [ts.createModifier(ts.SyntaxKind.StaticKeyword)], "properties", [], undefined, block);
+ if(legacyProperties.leadingComments.length > 0) {
+ propertiesAccessor = codeUtils.restoreLeadingComments(propertiesAccessor, legacyProperties.leadingComments);
+ }
+ this.classBuilder.addPolymerPropertiesAccessor(legacyProperties.data.pos, propertiesAccessor);
+ }
+
+ public addPolymerPropertiesObservers(legacyObservers: ts.ArrayLiteralExpression) {
+ const returnStatement = ts.createReturn(legacyObservers);
+ const block = ts.createBlock([returnStatement]);
+ const propertiesAccessor = ts.createGetAccessor(undefined, [ts.createModifier(ts.SyntaxKind.StaticKeyword)], "observers", [], undefined, block);
+
+ this.classBuilder.addPolymerObserversAccessor(legacyObservers.pos, propertiesAccessor);
+ }
+
+ public addKeyBindings(keyBindings: ts.ObjectLiteralExpression) {
+ //In Polymer 2 keyBindings must be a property with get accessor
+ const returnStatement = ts.createReturn(keyBindings);
+ const block = ts.createBlock([returnStatement]);
+ const keyBindingsAccessor = ts.createGetAccessor(undefined, [], "keyBindings", [], undefined, block);
+
+ this.classBuilder.addGetAccessor(keyBindings.pos, keyBindingsAccessor);
+ }
+ public addOrdinaryMethods(ordinaryMethods: OrdinaryMethods) {
+ for(const [name, method] of ordinaryMethods) {
+ this.classBuilder.addMethod(method.pos, method);
+ }
+ }
+
+ public addOrdinaryGetAccessors(ordinaryGetAccessors: OrdinaryGetAccessors) {
+ for(const [name, accessor] of ordinaryGetAccessors) {
+ this.classBuilder.addGetAccessor(accessor.pos, accessor);
+ }
+ }
+
+ public addOrdinaryShorthandProperties(ordinaryShorthandProperties: OrdinaryShorthandProperties) {
+ for (const [name, property] of ordinaryShorthandProperties) {
+ this.classBuilder.addClassFieldInitializer(property.name, property.name);
+ }
+ }
+
+ public addOrdinaryPropertyAssignments(ordinaryPropertyAssignments: OrdinaryPropertyAssignments) {
+ for (const [name, property] of ordinaryPropertyAssignments) {
+ const propertyName = codeUtils.assertNodeKind(property.name, ts.SyntaxKind.Identifier) as ts.Identifier;
+ this.classBuilder.addClassFieldInitializer(propertyName, property.initializer);
+ }
+ }
+
+ public addMixin(name: string, mixinArguments?: ts.Expression[]) {
+ let fullMixinArguments: ts.Expression[] = [];
+ if(mixinArguments) {
+ fullMixinArguments.push(...mixinArguments);
+ }
+ if(this.mixins) {
+ fullMixinArguments.push(this.mixins.expression);
+ }
+ if(fullMixinArguments.length > 0) {
+ this.mixins = ts.createExpressionWithTypeArguments([], ts.createCall(codeUtils.createNameExpression(name), [], fullMixinArguments.length > 0 ? fullMixinArguments : undefined));
+ }
+ else {
+ this.mixins = ts.createExpressionWithTypeArguments([], codeUtils.createNameExpression(name));
+ }
+ }
+
+ public addClassJSDocComments(lines: string[]) {
+ this.classBuilder.addClassJSDocComments(lines);
+ }
+
+ public build(): ClassBasedPolymerElement {
+ if(this.mixins) {
+ this.classBuilder.setBaseType(this.mixins);
+ }
+ const className = this.classBuilder.className;
+ const callExpression = ts.createCall(ts.createPropertyAccess(ts.createIdentifier("customElements"), "define"), undefined, [ts.createPropertyAccess(ts.createIdentifier(className), "is"), ts.createIdentifier(className)]);
+ const classBuilderResult = this.classBuilder.build();
+ return {
+ classDeclaration: classBuilderResult.classDeclaration,
+ generatedComments: classBuilderResult.generatedComments,
+ componentRegistration: ts.createExpressionStatement(callExpression),
+ eventsComments: this.legacyComponent.componentSettings.eventsComments,
+ };
+ }
+
+ private createIsAccessor(tagName: string): ts.GetAccessorDeclaration {
+ const returnStatement = ts.createReturn(ts.createStringLiteral(tagName));
+ const block = ts.createBlock([returnStatement]);
+ const accessor = ts.createGetAccessor([], [ts.createModifier(ts.SyntaxKind.StaticKeyword)], "is", [], undefined, block);
+ return codeUtils.addReplacableCommentAfterNode(accessor, "eventsComments");
+ }
+
+ public addLifecycleMethods(newLifecycleMethods: LifecycleMethod[]) {
+ for(const lifecycleMethod of newLifecycleMethods) {
+ this.classBuilder.addLifecycleMethod(lifecycleMethod.name, lifecycleMethod.originalPos, lifecycleMethod.method);
+ }
+ }
+}
diff --git a/tools/polygerrit-updater/src/funcToClassConversion/updatedFileWriter.ts b/tools/polygerrit-updater/src/funcToClassConversion/updatedFileWriter.ts
new file mode 100644
index 0000000..a147f50
--- /dev/null
+++ b/tools/polygerrit-updater/src/funcToClassConversion/updatedFileWriter.ts
@@ -0,0 +1,248 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {LegacyPolymerComponent} from './polymerComponentParser';
+import * as ts from 'typescript';
+import * as codeUtils from '../utils/codeUtils';
+import * as path from "path";
+import * as fs from "fs";
+import {LegacyPolymerFuncReplaceResult} from './legacyPolymerFuncReplacer';
+import {CommentsParser} from '../utils/commentsParser';
+
+export interface UpdatedFileWriterParameters {
+ out: string;
+ inplace: boolean;
+ writeOutput: boolean;
+ rootDir: string;
+}
+
+interface Replacement {
+ start: number;
+ length: number;
+ newText: string;
+}
+
+const elementRegistrationRegex = /^(\s*)customElements.define\((\w+).is, \w+\);$/m;
+const maxLineLength = 80;
+
+export class UpdatedFileWriter {
+ public constructor(private readonly component: LegacyPolymerComponent, private readonly params: UpdatedFileWriterParameters) {
+ }
+
+ public write(replaceResult: LegacyPolymerFuncReplaceResult, eventsComments: string[], generatedComments: string[]) {
+ const options: ts.PrinterOptions = {
+ removeComments: false,
+ newLine: ts.NewLineKind.LineFeed,
+ };
+ const printer = ts.createPrinter(options);
+ let newContent = codeUtils.applyNewLines(printer.printFile(replaceResult.file));
+ //ts printer doesn't keep original formatting of the file (spacing, new lines, comments, etc...).
+ //The following code tries restore original formatting
+
+ const existingComments = this.collectAllComments(newContent, []);
+
+ newContent = this.restoreEventsComments(newContent, eventsComments, existingComments);
+ newContent = this.restoreLeadingComments(newContent, replaceResult.leadingComments);
+ newContent = this.restoreFormating(printer, newContent);
+ newContent = this.splitLongLines(newContent);
+ newContent = this.addCommentsWarnings(newContent, generatedComments);
+
+ if (this.params.writeOutput) {
+ const outDir = this.params.inplace ? this.params.rootDir : this.params.out;
+ const fullOutPath = path.resolve(outDir, this.component.jsFile);
+ const fullOutDir = path.dirname(fullOutPath);
+ if (!fs.existsSync(fullOutDir)) {
+ fs.mkdirSync(fullOutDir, {
+ recursive: true,
+ mode: fs.lstatSync(this.params.rootDir).mode
+ });
+ }
+ fs.writeFileSync(fullOutPath, newContent);
+ }
+ }
+
+ private restoreEventsComments(content: string, eventsComments: string[], existingComments: Map<string, number>): string {
+ //In some cases Typescript compiler keep existing comments. These comments
+ // must not be restored here
+ eventsComments = eventsComments.filter(c => !existingComments.has(this.getNormalizedComment(c)));
+ return codeUtils.replaceComment(content, "eventsComments", "\n" + eventsComments.join("\n\n") + "\n");
+ }
+
+ private restoreLeadingComments(content: string, leadingComments: string[]): string {
+ return leadingComments.reduce(
+ (newContent, comment, commentIndex) =>
+ codeUtils.replaceComment(newContent, String(commentIndex), comment),
+ content);
+ }
+
+ private restoreFormating(printer: ts.Printer, newContent: string): string {
+ const originalFile = this.component.parsedFile;
+ const newFile = ts.createSourceFile(originalFile.fileName, newContent, originalFile.languageVersion, true, ts.ScriptKind.JS);
+ const textMap = new Map<ts.SyntaxKind, Map<string, Set<string>>>();
+ const comments = new Set<string>();
+ this.collectAllStrings(printer, originalFile, textMap);
+
+ const replacements: Replacement[] = [];
+ this.collectReplacements(printer, newFile, textMap, replacements);
+ replacements.sort((a, b) => b.start - a.start);
+ let result = newFile.getFullText();
+ let prevReplacement: Replacement | null = null;
+ for (const replacement of replacements) {
+ if (prevReplacement) {
+ if (replacement.start + replacement.length > prevReplacement.start) {
+ throw new Error('Internal error! Replacements must not intersect');
+ }
+ }
+ result = result.substring(0, replacement.start) + replacement.newText + result.substring(replacement.start + replacement.length);
+ prevReplacement = replacement;
+ }
+ return result;
+ }
+
+ private splitLongLines(content: string): string {
+ content = content.replace(elementRegistrationRegex, (match, indent, className) => {
+ if (match.length > maxLineLength) {
+ return `${indent}customElements.define(${className}.is,\n` +
+ `${indent} ${className});`;
+ }
+ else {
+ return match;
+ }
+ });
+
+ return content
+ .replace(
+ "Polymer.LegacyDataMixin(Polymer.GestureEventListeners(Polymer.LegacyElementMixin(Polymer.Element)))",
+ "Polymer.LegacyDataMixin(\nPolymer.GestureEventListeners(\nPolymer.LegacyElementMixin(\nPolymer.Element)))")
+ .replace(
+ "Polymer.GestureEventListeners(Polymer.LegacyElementMixin(Polymer.Element))",
+ "Polymer.GestureEventListeners(\nPolymer.LegacyElementMixin(\nPolymer.Element))");
+
+ }
+
+ private addCommentsWarnings(newContent: string, generatedComments: string[]): string {
+ const expectedComments = this.collectAllComments(this.component.parsedFile.getFullText(), generatedComments);
+ const newComments = this.collectAllComments(newContent, []);
+ const commentsWarnings = [];
+ for (const [text, count] of expectedComments) {
+ const newCount = newComments.get(text);
+ if (!newCount) {
+ commentsWarnings.push(`Comment '${text}' is missing in the new content.`);
+ }
+ else if (newCount != count) {
+ commentsWarnings.push(`Comment '${text}' appears ${newCount} times in the new file and ${count} times in the old file.`);
+ }
+ }
+
+ for (const [text, newCount] of newComments) {
+ if (!expectedComments.has(text)) {
+ commentsWarnings.push(`Comment '${text}' appears only in the new content`);
+ }
+ }
+ if (commentsWarnings.length === 0) {
+ return newContent;
+ }
+ let commentsProblemStr = "";
+ if (commentsWarnings.length > 0) {
+ commentsProblemStr = commentsWarnings.join("-----------------------------\n");
+ console.log(commentsProblemStr);
+ }
+
+ return "//This file has the following problems with comments:\n" + commentsProblemStr + "\n" + newContent;
+
+ }
+
+ private collectAllComments(content: string, additionalComments: string[]): Map<string, number> {
+ const comments = CommentsParser.collectAllComments(content);
+ comments.push(...additionalComments);
+ const result = new Map<string, number>();
+ for (const comment of comments) {
+ let normalizedComment = this.getNormalizedComment(comment);
+ const count = result.get(normalizedComment);
+ if (count) {
+ result.set(normalizedComment, count + 1);
+ } else {
+ result.set(normalizedComment, 1);
+ }
+ }
+ return result;
+ }
+
+ private getNormalizedComment(comment: string): string {
+ if(comment.startsWith('/**')) {
+ comment = comment.replace(/^\s+\*/gm, "*");
+ }
+ return comment;
+ }
+
+ private collectAllStrings(printer: ts.Printer, node: ts.Node, map: Map<ts.SyntaxKind, Map<string, Set<string>>>) {
+ const formattedText = printer.printNode(ts.EmitHint.Unspecified, node, node.getSourceFile())
+ const originalText = node.getFullText();
+ this.addIfNotExists(map, node.kind, formattedText, originalText);
+ ts.forEachChild(node, child => this.collectAllStrings(printer, child, map));
+ }
+
+ private collectReplacements(printer: ts.Printer, node: ts.Node, map: Map<ts.SyntaxKind, Map<string, Set<string>>>, replacements: Replacement[]) {
+ if(node.kind === ts.SyntaxKind.ThisKeyword || node.kind === ts.SyntaxKind.Identifier || node.kind === ts.SyntaxKind.StringLiteral || node.kind === ts.SyntaxKind.NumericLiteral) {
+ return;
+ }
+ const replacement = this.getReplacement(printer, node, map);
+ if(replacement) {
+ replacements.push(replacement);
+ return;
+ }
+ ts.forEachChild(node, child => this.collectReplacements(printer, child, map, replacements));
+ }
+
+ private addIfNotExists(map: Map<ts.SyntaxKind, Map<string, Set<string>>>, kind: ts.SyntaxKind, formattedText: string, originalText: string) {
+ let mapForKind = map.get(kind);
+ if(!mapForKind) {
+ mapForKind = new Map();
+ map.set(kind, mapForKind);
+ }
+
+ let existingOriginalText = mapForKind.get(formattedText);
+ if(!existingOriginalText) {
+ existingOriginalText = new Set<string>();
+ mapForKind.set(formattedText, existingOriginalText);
+ //throw new Error(`Different formatting of the same string exists. Kind: ${ts.SyntaxKind[kind]}.\nFormatting 1:\n${originalText}\nFormatting2:\n${existingOriginalText}\n `);
+ }
+ existingOriginalText.add(originalText);
+ }
+
+ private getReplacement(printer: ts.Printer, node: ts.Node, map: Map<ts.SyntaxKind, Map<string, Set<string>>>): Replacement | undefined {
+ const replacementsForKind = map.get(node.kind);
+ if(!replacementsForKind) {
+ return;
+ }
+ // Use printer instead of getFullText to "isolate" node content.
+ // node.getFullText returns text with indents from the original file.
+ const newText = printer.printNode(ts.EmitHint.Unspecified, node, node.getSourceFile());
+ const originalSet = replacementsForKind.get(newText);
+ if(!originalSet || originalSet.size === 0) {
+ return;
+ }
+ if(originalSet.size >= 2) {
+ console.log(`Multiple replacements possible. Formatting of some lines can be changed`);
+ }
+ const replacementText: string = originalSet.values().next().value;
+ const nodeText = node.getFullText();
+ return {
+ start: node.pos,
+ length: nodeText.length,//Do not use newText here!
+ newText: replacementText,
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/tools/polygerrit-updater/src/index.ts b/tools/polygerrit-updater/src/index.ts
new file mode 100644
index 0000000..1b7c315
--- /dev/null
+++ b/tools/polygerrit-updater/src/index.ts
@@ -0,0 +1,168 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import * as fs from "fs";
+import * as path from "path";
+import {LegacyPolymerComponent, LegacyPolymerComponentParser} from './funcToClassConversion/polymerComponentParser';
+import {ClassBasedPolymerElement} from './funcToClassConversion/polymerElementBuilder';
+import {PolymerFuncToClassBasedConverter} from './funcToClassConversion/funcToClassBasedElementConverter';
+import {LegacyPolymerFuncReplacer} from './funcToClassConversion/legacyPolymerFuncReplacer';
+import {UpdatedFileWriter} from './funcToClassConversion/updatedFileWriter';
+import {CommandLineParser} from './utils/commandLineParser';
+
+interface UpdaterParameters {
+ htmlFiles: Set<string>;
+ jsFiles: Set<string>;
+ out: string;
+ inplace: boolean;
+ writeOutput: boolean;
+ rootDir: string;
+}
+
+interface InputFilesFilter {
+ includeDir(path: string): boolean;
+ includeFile(path: string): boolean;
+}
+
+function addFile(filePath: string, params: UpdaterParameters, filter: InputFilesFilter) {
+ const parsedPath = path.parse(filePath);
+ const ext = parsedPath.ext.toLowerCase();
+ const relativePath = path.relative(params.rootDir, filePath);
+ if(!filter.includeFile(relativePath)) return;
+ if(relativePath.startsWith("../")) {
+ throw new Error(`${filePath} is not in rootDir ${params.rootDir}`);
+ }
+ if(ext === ".html") {
+ params.htmlFiles.add(relativePath);
+ } if(ext === ".js") {
+ params.jsFiles.add(relativePath);
+ }
+}
+
+function addDirectory(dirPath: string, params: UpdaterParameters, recursive: boolean, filter: InputFilesFilter): void {
+ const entries = fs.readdirSync(dirPath, {withFileTypes: true});
+ for(const entry of entries) {
+ const dirEnt = entry as fs.Dirent;
+ const fullPath = path.join(dirPath, dirEnt.name);
+ const relativePath = path.relative(params.rootDir, fullPath);
+ if(dirEnt.isDirectory()) {
+ if (!filter.includeDir(relativePath)) continue;
+ if(recursive) {
+ addDirectory(fullPath, params, recursive, filter);
+ }
+ }
+ else if(dirEnt.isFile()) {
+ addFile(fullPath, params, filter);
+ } else {
+ throw Error(`Unsupported dir entry '${entry.name}' in '${fullPath}'`);
+ }
+ }
+}
+
+async function updateLegacyComponent(component: LegacyPolymerComponent, params: UpdaterParameters) {
+ const classBasedElement: ClassBasedPolymerElement = PolymerFuncToClassBasedConverter.convert(component);
+
+ const replacer = new LegacyPolymerFuncReplacer(component);
+ const replaceResult = replacer.replace(classBasedElement);
+ try {
+ const writer = new UpdatedFileWriter(component, params);
+ writer.write(replaceResult, classBasedElement.eventsComments, classBasedElement.generatedComments);
+ }
+ finally {
+ replaceResult.dispose();
+ }
+}
+
+async function main() {
+ const params: UpdaterParameters = await getParams();
+ if(params.jsFiles.size === 0) {
+ console.log("No files found");
+ return;
+ }
+ const legacyPolymerComponentParser = new LegacyPolymerComponentParser(params.rootDir, params.htmlFiles)
+ for(const jsFile of params.jsFiles) {
+ console.log(`Processing ${jsFile}`);
+ const legacyComponent = await legacyPolymerComponentParser.parse(jsFile);
+ if(legacyComponent) {
+ await updateLegacyComponent(legacyComponent, params);
+ continue;
+ }
+ }
+}
+
+interface CommandLineParameters {
+ src: string[];
+ recursive: boolean;
+ excludes: string[];
+ out: string;
+ inplace: boolean;
+ noOutput: boolean;
+ rootDir: string;
+}
+
+async function getParams(): Promise<UpdaterParameters> {
+ const parser = new CommandLineParser({
+ src: CommandLineParser.createStringArrayOption("src", ".js file or folder to process", []),
+ recursive: CommandLineParser.createBooleanOption("r", "process folder recursive", false),
+ excludes: CommandLineParser.createStringArrayOption("exclude", "List of file prefixes to exclude. If relative file path starts with one of the prefixes, it will be excluded", []),
+ out: CommandLineParser.createStringOption("out", "Output folder.", null),
+ rootDir: CommandLineParser.createStringOption("root", "Root directory for src files", "/"),
+ inplace: CommandLineParser.createBooleanOption("i", "Update files inplace", false),
+ noOutput: CommandLineParser.createBooleanOption("noout", "Do everything, but do not write anything to files", false),
+ });
+ const commandLineParams: CommandLineParameters = parser.parse(process.argv) as CommandLineParameters;
+
+ const params: UpdaterParameters = {
+ htmlFiles: new Set(),
+ jsFiles: new Set(),
+ writeOutput: !commandLineParams.noOutput,
+ inplace: commandLineParams.inplace,
+ out: commandLineParams.out,
+ rootDir: path.resolve(commandLineParams.rootDir)
+ };
+
+ if(params.writeOutput && !params.inplace && !params.out) {
+ throw new Error("You should specify output directory (--out directory_name)");
+ }
+
+ const filter = new ExcludeFilesFilter(commandLineParams.excludes);
+ for(const srcPath of commandLineParams.src) {
+ const resolvedPath = path.resolve(params.rootDir, srcPath);
+ if(fs.lstatSync(resolvedPath).isFile()) {
+ addFile(resolvedPath, params, filter);
+ } else {
+ addDirectory(resolvedPath, params, commandLineParams.recursive, filter);
+ }
+ }
+ return params;
+}
+
+class ExcludeFilesFilter implements InputFilesFilter {
+ public constructor(private readonly excludes: string[]) {
+ }
+ includeDir(path: string): boolean {
+ return this.excludes.every(exclude => !path.startsWith(exclude));
+ }
+
+ includeFile(path: string): boolean {
+ return this.excludes.every(exclude => !path.startsWith(exclude));
+ }
+}
+
+main().then(() => {
+ process.exit(0);
+}).catch(e => {
+ console.error(e);
+ process.exit(1);
+});
diff --git a/tools/polygerrit-updater/src/utils/codeUtils.ts b/tools/polygerrit-updater/src/utils/codeUtils.ts
new file mode 100644
index 0000000..53a7f0d
--- /dev/null
+++ b/tools/polygerrit-updater/src/utils/codeUtils.ts
@@ -0,0 +1,183 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import * as ts from 'typescript';
+import {SyntaxKind} from 'typescript';
+import {Node} from 'typescript';
+
+export function assertNodeKind<T extends U, U extends ts.Node>(node: U, expectedKind: ts.SyntaxKind): T {
+ if (node.kind !== expectedKind) {
+ throw new Error(`Invlid node kind. Expected: ${ts.SyntaxKind[expectedKind]}, actual: ${ts.SyntaxKind[node.kind]}`);
+ }
+ return node as T;
+}
+
+export function assertNodeKindOrUndefined<T extends U, U extends ts.Node>(node: U | undefined, expectedKind: ts.SyntaxKind): T | undefined {
+ if (!node) {
+ return undefined;
+ }
+ return assertNodeKind<T, U>(node, expectedKind);
+}
+
+export function getPropertyAssignment(expression?: ts.ObjectLiteralElementLike): ts.PropertyAssignment | undefined {
+ return assertNodeKindOrUndefined(expression, ts.SyntaxKind.PropertyAssignment);
+}
+
+export function getStringLiteralValue(expression: ts.Expression): string {
+ const literal: ts.StringLiteral = assertNodeKind(expression, ts.SyntaxKind.StringLiteral);
+ return literal.text;
+}
+
+export function getBooleanLiteralValue(expression: ts.Expression): boolean {
+ if (expression.kind === ts.SyntaxKind.TrueKeyword) {
+ return true;
+ }
+ if (expression.kind === ts.SyntaxKind.FalseKeyword) {
+ return false;
+ }
+ throw new Error(`Invalid expression kind - ${expression.kind}`);
+}
+
+export function getObjectLiteralExpression(expression: ts.Expression): ts.ObjectLiteralExpression {
+ return assertNodeKind(expression, ts.SyntaxKind.ObjectLiteralExpression);
+}
+
+export function getArrayLiteralExpression(expression: ts.Expression): ts.ArrayLiteralExpression {
+ return assertNodeKind(expression, ts.SyntaxKind.ArrayLiteralExpression);
+}
+
+export function replaceNode(file: ts.SourceFile, originalNode: ts.Node, newNode: ts.Node): ts.TransformationResult<ts.SourceFile> {
+ const nodeReplacerTransformer: ts.TransformerFactory<ts.SourceFile> = (context: ts.TransformationContext) => {
+ const visitor: ts.Visitor = (node) => {
+ if(node === originalNode) {
+ return newNode;
+ }
+ return ts.visitEachChild(node, visitor, context);
+ };
+
+
+ return source => ts.visitNode(source, visitor);
+ };
+ return ts.transform(file, [nodeReplacerTransformer]);
+}
+
+export type NameExpression = ts.Identifier | ts.ThisExpression | ts.PropertyAccessExpression;
+export function createNameExpression(fullPath: string): NameExpression {
+ const parts = fullPath.split(".");
+ let result: NameExpression = parts[0] === "this" ? ts.createThis() : ts.createIdentifier(parts[0]);
+ for(let i = 1; i < parts.length; i++) {
+ result = ts.createPropertyAccess(result, parts[i]);
+ }
+ return result;
+}
+
+const generatedCommentNewLineAfterText = "-Generated code - new line after - 9cb292bc-5d88-4c5e-88f4-49535c93beb9 -";
+const generatedCommentNewLineBeforeText = "-Generated code - new line-before - 9cb292bc-5d88-4c5e-88f4-49535c93beb9 -";
+const generatedCommentNewLineAfterRegExp = new RegExp("//" + generatedCommentNewLineAfterText, 'g');
+const generatedCommentNewLineBeforeRegExp = new RegExp("//" + generatedCommentNewLineBeforeText + "\n", 'g');
+const replacableCommentText = "- Replacepoint - 9cb292bc-5d88-4c5e-88f4-49535c93beb9 -";
+
+export function addNewLineAfterNode<T extends ts.Node>(node: T): T {
+ const comment = ts.getSyntheticTrailingComments(node);
+ if(comment && comment.some(c => c.text === generatedCommentNewLineAfterText)) {
+ return node;
+ }
+ return ts.addSyntheticTrailingComment(node, ts.SyntaxKind.SingleLineCommentTrivia, generatedCommentNewLineAfterText, true);
+}
+
+export function addNewLineBeforeNode<T extends ts.Node>(node: T): T {
+ const comment = ts.getSyntheticLeadingComments(node);
+ if(comment && comment.some(c => c.text === generatedCommentNewLineBeforeText)) {
+ return node;
+ }
+ return ts.addSyntheticLeadingComment(node, ts.SyntaxKind.SingleLineCommentTrivia, generatedCommentNewLineBeforeText, true);
+}
+
+
+export function applyNewLines(text: string): string {
+ return text.replace(generatedCommentNewLineAfterRegExp, "").replace(generatedCommentNewLineBeforeRegExp, "");
+
+}
+export function addReplacableCommentAfterNode<T extends ts.Node>(node: T, name: string): T {
+ return ts.addSyntheticTrailingComment(node, ts.SyntaxKind.SingleLineCommentTrivia, replacableCommentText + name, true);
+}
+
+export function addReplacableCommentBeforeNode<T extends ts.Node>(node: T, name: string): T {
+ return ts.addSyntheticLeadingComment(node, ts.SyntaxKind.SingleLineCommentTrivia, replacableCommentText + name, true);
+}
+
+export function replaceComment(text: string, commentName: string, newContent: string): string {
+ return text.replace("//" + replacableCommentText + commentName, newContent);
+}
+
+export function createMethod(name: string, methodDecl: ts.MethodDeclaration | undefined, codeAtStart: ts.Statement[], codeAtEnd: ts.Statement[], callSuperMethod: boolean): ts.MethodDeclaration | undefined {
+ if(!methodDecl && (codeAtEnd.length > 0 || codeAtEnd.length > 0)) {
+ methodDecl = ts.createMethod([], [], undefined, name, undefined, [], [],undefined, ts.createBlock([]));
+ }
+ if(!methodDecl) {
+ return;
+ }
+ if (!methodDecl.body) {
+ throw new Error("Method must have a body");
+ }
+ if(methodDecl.parameters.length > 0) {
+ throw new Error("Methods with parameters are not supported");
+ }
+ let newStatements = [...codeAtStart];
+ if(callSuperMethod) {
+ const superCall: ts.CallExpression = ts.createCall(ts.createPropertyAccess(ts.createSuper(), assertNodeKind(methodDecl.name, ts.SyntaxKind.Identifier) as ts.Identifier), [], []);
+ const superCallExpression = ts.createExpressionStatement(superCall);
+ newStatements.push(superCallExpression);
+ }
+ newStatements.push(...codeAtEnd);
+ const newBody = ts.getMutableClone(methodDecl.body);
+
+ newStatements = newStatements.map(m => addNewLineAfterNode(m));
+ newStatements.splice(codeAtStart.length + 1, 0, ...newBody.statements);
+
+ newBody.statements = ts.createNodeArray(newStatements);
+
+ const newMethod = ts.getMutableClone(methodDecl);
+ newMethod.body = newBody;
+
+ return newMethod;
+}
+
+export function restoreLeadingComments<T extends Node>(node: T, originalComments: string[]): T {
+ if(originalComments.length === 0) {
+ return node;
+ }
+ for(const comment of originalComments) {
+ if(comment.startsWith("//")) {
+ node = ts.addSyntheticLeadingComment(node, SyntaxKind.SingleLineCommentTrivia, comment.substr(2), true);
+ } else if(comment.startsWith("/*")) {
+ if(!comment.endsWith("*/")) {
+ throw new Error(`Not support comment: ${comment}`);
+ }
+ node = ts.addSyntheticLeadingComment(node, SyntaxKind.MultiLineCommentTrivia, comment.substr(2, comment.length - 4), true);
+ } else {
+ throw new Error(`Not supported comment: ${comment}`);
+ }
+ }
+ return node;
+}
+
+export function getLeadingComments(node: ts.Node): string[] {
+ const nodeText = node.getFullText();
+ const commentRanges = ts.getLeadingCommentRanges(nodeText, 0);
+ if(!commentRanges) {
+ return [];
+ }
+ return commentRanges.map(range => nodeText.substring(range.pos, range.end))
+}
\ No newline at end of file
diff --git a/tools/polygerrit-updater/src/utils/commandLineParser.ts b/tools/polygerrit-updater/src/utils/commandLineParser.ts
new file mode 100644
index 0000000..658b7ff
--- /dev/null
+++ b/tools/polygerrit-updater/src/utils/commandLineParser.ts
@@ -0,0 +1,134 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed un der the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+export class CommandLineParser {
+ public static createStringArrayOption(optionName: string, help: string, defaultValue: string[]): CommandLineArgument {
+ return new StringArrayOption(optionName, help, defaultValue);
+ }
+ public static createBooleanOption(optionName: string, help: string, defaultValue: boolean): CommandLineArgument {
+ return new BooleanOption(optionName, help, defaultValue);
+ }
+ public static createStringOption(optionName: string, help: string, defaultValue: string | null): CommandLineArgument {
+ return new StringOption(optionName, help, defaultValue);
+ }
+
+ public constructor(private readonly argumentTypes: {[name: string]: CommandLineArgument}) {
+ }
+ public parse(argv: string[]): object {
+ const result = Object.assign({});
+ let index = 2; //argv[0] - node interpreter, argv[1] - index.js
+ for(const argumentField in this.argumentTypes) {
+ result[argumentField] = this.argumentTypes[argumentField].getDefaultValue();
+ }
+ while(index < argv.length) {
+ let knownArgument = false;
+ for(const argumentField in this.argumentTypes) {
+ const argumentType = this.argumentTypes[argumentField];
+ const argumentValue = argumentType.tryRead(argv, index);
+ if(argumentValue) {
+ knownArgument = true;
+ index += argumentValue.consumed;
+ result[argumentField] = argumentValue.value;
+ break;
+ }
+ }
+ if(!knownArgument) {
+ throw new Error(`Unknown argument ${argv[index]}`);
+ }
+ }
+ return result;
+ }
+}
+
+interface CommandLineArgumentReadResult {
+ consumed: number;
+ value: any;
+}
+
+export interface CommandLineArgument {
+ getDefaultValue(): any;
+ tryRead(argv: string[], startIndex: number): CommandLineArgumentReadResult | null;
+}
+
+abstract class CommandLineOption implements CommandLineArgument {
+ protected constructor(protected readonly optionName: string, protected readonly help: string, private readonly defaultValue: any) {
+ }
+ public tryRead(argv: string[], startIndex: number): CommandLineArgumentReadResult | null {
+ if(argv[startIndex] !== "--" + this.optionName) {
+ return null;
+ }
+ const readArgumentsResult = this.readArguments(argv, startIndex + 1);
+ if(!readArgumentsResult) {
+ return null;
+ }
+ readArgumentsResult.consumed++; // Add option name
+ return readArgumentsResult;
+ }
+ public getDefaultValue(): any {
+ return this.defaultValue;
+ }
+
+ protected abstract readArguments(argv: string[], startIndex: number) : CommandLineArgumentReadResult | null;
+}
+
+class StringArrayOption extends CommandLineOption {
+ public constructor(optionName: string, help: string, defaultValue: string[]) {
+ super(optionName, help, defaultValue);
+ }
+
+ protected readArguments(argv: string[], startIndex: number): CommandLineArgumentReadResult {
+ const result = [];
+ let index = startIndex;
+ while(index < argv.length) {
+ if(argv[index].startsWith("--")) {
+ break;
+ }
+ result.push(argv[index]);
+ index++;
+ }
+ return {
+ consumed: index - startIndex,
+ value: result
+ }
+ }
+}
+
+class BooleanOption extends CommandLineOption {
+ public constructor(optionName: string, help: string, defaultValue: boolean) {
+ super(optionName, help, defaultValue);
+ }
+
+ protected readArguments(argv: string[], startIndex: number): CommandLineArgumentReadResult {
+ return {
+ consumed: 0,
+ value: true
+ }
+ }
+}
+
+class StringOption extends CommandLineOption {
+ public constructor(optionName: string, help: string, defaultValue: string | null) {
+ super(optionName, help, defaultValue);
+ }
+
+ protected readArguments(argv: string[], startIndex: number): CommandLineArgumentReadResult | null {
+ if(startIndex >= argv.length) {
+ return null;
+ }
+ return {
+ consumed: 1,
+ value: argv[startIndex]
+ }
+ }
+}
diff --git a/tools/polygerrit-updater/src/utils/commentsParser.ts b/tools/polygerrit-updater/src/utils/commentsParser.ts
new file mode 100644
index 0000000..b849829
--- /dev/null
+++ b/tools/polygerrit-updater/src/utils/commentsParser.ts
@@ -0,0 +1,79 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+enum CommentScannerState {
+ Text,
+ SingleLineComment,
+ MultLineComment
+}
+export class CommentsParser {
+ public static collectAllComments(text: string): string[] {
+ const result: string[] = [];
+ let state = CommentScannerState.Text;
+ let pos = 0;
+ function readSingleLineComment() {
+ const startPos = pos;
+ while(pos < text.length && text[pos] !== '\n') {
+ pos++;
+ }
+ return text.substring(startPos, pos);
+ }
+ function readMultiLineComment() {
+ const startPos = pos;
+ while(pos < text.length) {
+ if(pos < text.length - 1 && text[pos] === '*' && text[pos + 1] === '/') {
+ pos += 2;
+ break;
+ }
+ pos++;
+ }
+ return text.substring(startPos, pos);
+ }
+
+ function skipString(lastChar: string) {
+ pos++;
+ while(pos < text.length) {
+ if(text[pos] === lastChar) {
+ pos++;
+ return;
+ } else if(text[pos] === '\\') {
+ pos+=2;
+ continue;
+ }
+ pos++;
+ }
+ }
+
+
+ while(pos < text.length - 1) {
+ if(text[pos] === '/' && text[pos + 1] === '/') {
+ result.push(readSingleLineComment());
+ } else if(text[pos] === '/' && text[pos + 1] === '*') {
+ result.push(readMultiLineComment());
+ } else if(text[pos] === "'") {
+ skipString("'");
+ } else if(text[pos] === '"') {
+ skipString('"');
+ } else if(text[pos] === '`') {
+ skipString('`');
+ } else if(text[pos] == '/') {
+ skipString('/');
+ } {
+ pos++;
+ }
+
+ }
+ return result;
+ }
+}
diff --git a/tools/polygerrit-updater/src/utils/polymerClassBuilder.ts b/tools/polygerrit-updater/src/utils/polymerClassBuilder.ts
new file mode 100644
index 0000000..b1a4320
--- /dev/null
+++ b/tools/polygerrit-updater/src/utils/polymerClassBuilder.ts
@@ -0,0 +1,270 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import * as ts from 'typescript';
+import * as codeUtils from './codeUtils';
+import {LegacyLifecycleMethodName, LegacyLifecycleMethodsArray} from '../funcToClassConversion/polymerComponentParser';
+import {SyntaxKind} from 'typescript';
+
+enum PolymerClassMemberType {
+ IsAccessor,
+ Constructor,
+ PolymerPropertiesAccessor,
+ PolymerObserversAccessor,
+ Method,
+ ExistingLifecycleMethod,
+ NewLifecycleMethod,
+ GetAccessor,
+}
+
+type PolymerClassMember = PolymerClassIsAccessor | PolymerClassConstructor | PolymerClassExistingLifecycleMethod | PolymerClassNewLifecycleMethod | PolymerClassSimpleMember;
+
+interface PolymerClassExistingLifecycleMethod {
+ member: ts.MethodDeclaration;
+ memberType: PolymerClassMemberType.ExistingLifecycleMethod;
+ name: string;
+ lifecycleOrder: number;
+ originalPos: number;
+}
+
+interface PolymerClassNewLifecycleMethod {
+ member: ts.MethodDeclaration;
+ memberType: PolymerClassMemberType.NewLifecycleMethod;
+ name: string;
+ lifecycleOrder: number;
+ originalPos: -1
+}
+
+interface PolymerClassIsAccessor {
+ member: ts.GetAccessorDeclaration;
+ memberType: PolymerClassMemberType.IsAccessor;
+ originalPos: -1
+}
+
+interface PolymerClassConstructor {
+ member: ts.ConstructorDeclaration;
+ memberType: PolymerClassMemberType.Constructor;
+ originalPos: -1
+}
+
+interface PolymerClassSimpleMember {
+ memberType: PolymerClassMemberType.PolymerPropertiesAccessor | PolymerClassMemberType.PolymerObserversAccessor | PolymerClassMemberType.Method | PolymerClassMemberType.GetAccessor;
+ member: ts.ClassElement;
+ originalPos: number;
+}
+
+export interface PolymerClassBuilderResult {
+ classDeclaration: ts.ClassDeclaration;
+ generatedComments: string[];
+}
+
+export class PolymerClassBuilder {
+ private readonly members: PolymerClassMember[] = [];
+ public readonly constructorStatements: ts.Statement[] = [];
+ private baseType: ts.ExpressionWithTypeArguments | undefined;
+ private classJsDocComments: string[];
+
+ public constructor(public readonly className: string) {
+ this.classJsDocComments = [];
+ }
+
+ public addIsAccessor(accessor: ts.GetAccessorDeclaration) {
+ this.members.push({
+ member: accessor,
+ memberType: PolymerClassMemberType.IsAccessor,
+ originalPos: -1
+ });
+ }
+
+ public addPolymerPropertiesAccessor(originalPos: number, accessor: ts.GetAccessorDeclaration) {
+ this.members.push({
+ member: accessor,
+ memberType: PolymerClassMemberType.PolymerPropertiesAccessor,
+ originalPos: originalPos
+ });
+ }
+
+ public addPolymerObserversAccessor(originalPos: number, accessor: ts.GetAccessorDeclaration) {
+ this.members.push({
+ member: accessor,
+ memberType: PolymerClassMemberType.PolymerObserversAccessor,
+ originalPos: originalPos
+ });
+ }
+
+
+ public addClassFieldInitializer(name: string | ts.Identifier, initializer: ts.Expression) {
+ const assignment = ts.createAssignment(ts.createPropertyAccess(ts.createThis(), name), initializer);
+ this.constructorStatements.push(codeUtils.addNewLineAfterNode(ts.createExpressionStatement(assignment)));
+ }
+ public addMethod(originalPos: number, method: ts.MethodDeclaration) {
+ this.members.push({
+ member: method,
+ memberType: PolymerClassMemberType.Method,
+ originalPos: originalPos
+ });
+ }
+
+ public addGetAccessor(originalPos: number, accessor: ts.GetAccessorDeclaration) {
+ this.members.push({
+ member: accessor,
+ memberType: PolymerClassMemberType.GetAccessor,
+ originalPos: originalPos
+ });
+ }
+
+ public addLifecycleMethod(name: LegacyLifecycleMethodName, originalPos: number, method: ts.MethodDeclaration) {
+ const lifecycleOrder = LegacyLifecycleMethodsArray.indexOf(name);
+ if(lifecycleOrder < 0) {
+ throw new Error(`Invalid lifecycle name`);
+ }
+ if(originalPos >= 0) {
+ this.members.push({
+ member: method,
+ memberType: PolymerClassMemberType.ExistingLifecycleMethod,
+ originalPos: originalPos,
+ name: name,
+ lifecycleOrder: lifecycleOrder
+ })
+ } else {
+ this.members.push({
+ member: method,
+ memberType: PolymerClassMemberType.NewLifecycleMethod,
+ name: name,
+ lifecycleOrder: lifecycleOrder,
+ originalPos: -1
+ })
+ }
+ }
+
+ public setBaseType(type: ts.ExpressionWithTypeArguments) {
+ if(this.baseType) {
+ throw new Error("Class can have only one base type");
+ }
+ this.baseType = type;
+ }
+
+ public build(): PolymerClassBuilderResult {
+ let heritageClauses: ts.HeritageClause[] = [];
+ if (this.baseType) {
+ const extendClause = ts.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, [this.baseType]);
+ heritageClauses.push(extendClause);
+ }
+ const finalMembers: PolymerClassMember[] = [];
+ const isAccessors = this.members.filter(member => member.memberType === PolymerClassMemberType.IsAccessor);
+ if(isAccessors.length !== 1) {
+ throw new Error("Class must have exactly one 'is'");
+ }
+ finalMembers.push(isAccessors[0]);
+ const constructorMember = this.createConstructor();
+ if(constructorMember) {
+ finalMembers.push(constructorMember);
+ }
+
+ const newLifecycleMethods: PolymerClassNewLifecycleMethod[] = [];
+ this.members.forEach(member => {
+ if(member.memberType === PolymerClassMemberType.NewLifecycleMethod) {
+ newLifecycleMethods.push(member);
+ }
+ });
+
+ const methodsWithKnownPosition = this.members.filter(member => member.originalPos >= 0);
+ methodsWithKnownPosition.sort((a, b) => a.originalPos - b.originalPos);
+
+ finalMembers.push(...methodsWithKnownPosition);
+
+
+ for(const newLifecycleMethod of newLifecycleMethods) {
+ //Number of methods is small - use brute force solution
+ let closestNextIndex = -1;
+ let closestNextOrderDiff: number = LegacyLifecycleMethodsArray.length;
+ let closestPrevIndex = -1;
+ let closestPrevOrderDiff: number = LegacyLifecycleMethodsArray.length;
+ for (let i = 0; i < finalMembers.length; i++) {
+ const member = finalMembers[i];
+ if (member.memberType !== PolymerClassMemberType.NewLifecycleMethod && member.memberType !== PolymerClassMemberType.ExistingLifecycleMethod) {
+ continue;
+ }
+ const orderDiff = member.lifecycleOrder - newLifecycleMethod.lifecycleOrder;
+ if (orderDiff > 0) {
+ if (orderDiff < closestNextOrderDiff) {
+ closestNextIndex = i;
+ closestNextOrderDiff = orderDiff;
+ }
+ } else if (orderDiff < 0) {
+ if (orderDiff < closestPrevOrderDiff) {
+ closestPrevIndex = i;
+ closestPrevOrderDiff = orderDiff;
+ }
+ }
+ }
+ let insertIndex;
+ if (closestNextIndex !== -1 || closestPrevIndex !== -1) {
+ insertIndex = closestNextOrderDiff < closestPrevOrderDiff ?
+ closestNextIndex : closestPrevIndex + 1;
+ } else {
+ insertIndex = Math.max(
+ finalMembers.findIndex(m => m.memberType === PolymerClassMemberType.Constructor),
+ finalMembers.findIndex(m => m.memberType === PolymerClassMemberType.IsAccessor),
+ finalMembers.findIndex(m => m.memberType === PolymerClassMemberType.PolymerPropertiesAccessor),
+ finalMembers.findIndex(m => m.memberType === PolymerClassMemberType.PolymerObserversAccessor),
+ );
+ if(insertIndex < 0) {
+ insertIndex = finalMembers.length;
+ } else {
+ insertIndex++;//Insert after
+ }
+ }
+ finalMembers.splice(insertIndex, 0, newLifecycleMethod);
+ }
+ //Asserts about finalMembers
+ const nonConstructorMembers = finalMembers.filter(m => m.memberType !== PolymerClassMemberType.Constructor);
+
+ if(nonConstructorMembers.length !== this.members.length) {
+ throw new Error(`Internal error! Some methods are missed`);
+ }
+ let classDeclaration = ts.createClassDeclaration(undefined, undefined, this.className, undefined, heritageClauses, finalMembers.map(m => m.member))
+ const generatedComments: string[] = [];
+ if(this.classJsDocComments.length > 0) {
+ const commentContent = '*\n' + this.classJsDocComments.map(line => `* ${line}`).join('\n') + '\n';
+ classDeclaration = ts.addSyntheticLeadingComment(classDeclaration, ts.SyntaxKind.MultiLineCommentTrivia, commentContent, true);
+ generatedComments.push(`/*${commentContent}*/`);
+ }
+ return {
+ classDeclaration,
+ generatedComments,
+ };
+
+ }
+
+ private createConstructor(): PolymerClassConstructor | null {
+ if(this.constructorStatements.length === 0) {
+ return null;
+ }
+ let superCall: ts.CallExpression = ts.createCall(ts.createSuper(), [], []);
+ const superCallExpression = ts.createExpressionStatement(superCall);
+ const statements = [superCallExpression, ...this.constructorStatements];
+ const constructorDeclaration = ts.createConstructor([], [], [], ts.createBlock(statements, true));
+
+ return {
+ memberType: PolymerClassMemberType.Constructor,
+ member: constructorDeclaration,
+ originalPos: -1
+ };
+ }
+
+ public addClassJSDocComments(lines: string[]) {
+ this.classJsDocComments.push(...lines);
+ }
+}
\ No newline at end of file
diff --git a/tools/polygerrit-updater/src/utils/unexpectedValue.ts b/tools/polygerrit-updater/src/utils/unexpectedValue.ts
new file mode 100644
index 0000000..690c283
--- /dev/null
+++ b/tools/polygerrit-updater/src/utils/unexpectedValue.ts
@@ -0,0 +1,17 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+export function unexpectedValue<T>(x: T): never {
+ throw new Error(`Unexpected value '${x}'`);
+}
diff --git a/tools/polygerrit-updater/tsconfig.json b/tools/polygerrit-updater/tsconfig.json
new file mode 100644
index 0000000..37ff1b2
--- /dev/null
+++ b/tools/polygerrit-updater/tsconfig.json
@@ -0,0 +1,67 @@
+{
+ "compilerOptions": {
+ /* Basic Options */
+ // "incremental": true, /* Enable incremental compilation */
+ "target": "es2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
+ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
+ // "lib": [], /* Specify library files to be included in the compilation. */
+ // "allowJs": true, /* Allow javascript files to be compiled. */
+ // "checkJs": true, /* Report errors in .js files. */
+ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
+ // "declaration": true, /* Generates corresponding '.d.ts' file. */
+ // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
+ "sourceMap": true, /* Generates corresponding '.map' file. */
+ // "outFile": "./", /* Concatenate and emit output to single file. */
+ "outDir": "./js", /* Redirect output structure to the directory. */
+ "rootDir": ".", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
+ // "composite": true, /* Enable project compilation */
+ // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
+ // "removeComments": true, /* Do not emit comments to output. */
+ // "noEmit": true, /* Do not emit outputs. */
+ // "importHelpers": true, /* Import emit helpers from 'tslib'. */
+ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
+ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
+
+ /* Strict Type-Checking Options */
+ "strict": true, /* Enable all strict type-checking options. */
+ "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
+ // "strictNullChecks": true, /* Enable strict null checks. */
+ // "strictFunctionTypes": true, /* Enable strict checking of function types. */
+ // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
+ // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
+ // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
+ // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
+
+ /* Additional Checks */
+ // "noUnusedLocals": true, /* Report errors on unused locals. */
+ // "noUnusedParameters": true, /* Report errors on unused parameters. */
+ // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
+ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
+
+ /* Module Resolution Options */
+ "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
+ "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
+ "paths": {
+ "*": [ "node_modules/*" ]
+ }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
+ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
+ // "typeRoots": [], /* List of folders to include type definitions from. */
+ // "types": [], /* Type declaration files to be included in compilation. */
+ "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
+ "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
+ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
+ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
+
+ /* Source Map Options */
+ // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
+ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
+ // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
+ // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
+
+ /* Experimental Options */
+ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
+ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
+
+ },
+ "include": ["./src/**/*"]
+}