Converter to Polymer2 classes.
See readme.txt for more information about usages. Change-Id: I60843b32bc56faa04ad2b70898ab1f5535a2ad71
This commit is contained in:
parent
2f67ad2509
commit
f0d5b6e49c
3
tools/polygerrit-updater/.gitignore
vendored
Normal file
3
tools/polygerrit-updater/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/.idea/
|
||||||
|
/node_modules/
|
||||||
|
/js/
|
18
tools/polygerrit-updater/package-lock.json
generated
Normal file
18
tools/polygerrit-updater/package-lock.json
generated
Normal file
@ -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=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
tools/polygerrit-updater/package.json
Normal file
15
tools/polygerrit-updater/package.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
56
tools/polygerrit-updater/readme.txt
Normal file
56
tools/polygerrit-updater/readme.txt
Normal file
@ -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.
|
131
tools/polygerrit-updater/src/funcToClassConversion/funcToClassBasedElementConverter.ts
Normal file
131
tools/polygerrit-updater/src/funcToClassConversion/funcToClassBasedElementConverter.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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>;
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
168
tools/polygerrit-updater/src/index.ts
Normal file
168
tools/polygerrit-updater/src/index.ts
Normal file
@ -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);
|
||||||
|
});
|
183
tools/polygerrit-updater/src/utils/codeUtils.ts
Normal file
183
tools/polygerrit-updater/src/utils/codeUtils.ts
Normal file
@ -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))
|
||||||
|
}
|
134
tools/polygerrit-updater/src/utils/commandLineParser.ts
Normal file
134
tools/polygerrit-updater/src/utils/commandLineParser.ts
Normal file
@ -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]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
79
tools/polygerrit-updater/src/utils/commentsParser.ts
Normal file
79
tools/polygerrit-updater/src/utils/commentsParser.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
270
tools/polygerrit-updater/src/utils/polymerClassBuilder.ts
Normal file
270
tools/polygerrit-updater/src/utils/polymerClassBuilder.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
17
tools/polygerrit-updater/src/utils/unexpectedValue.ts
Normal file
17
tools/polygerrit-updater/src/utils/unexpectedValue.ts
Normal file
@ -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}'`);
|
||||||
|
}
|
67
tools/polygerrit-updater/tsconfig.json
Normal file
67
tools/polygerrit-updater/tsconfig.json
Normal file
@ -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/**/*"]
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user