Converter to Polymer2 classes.
See readme.txt for more information about usages.
Change-Id: I60843b32bc56faa04ad2b70898ab1f5535a2ad71
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