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.
|
@ -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