@polymer/gen-typescript-declarations
Version:
Generate TypeScript type declarations for Polymer components.
556 lines (501 loc) • 13.7 kB
text/typescript
/**
* @license
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt The complete set of authors may be found
* at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
* be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
* Google as part of the polymer project is also subject to an additional IP
* rights grant found at http://polymer.github.io/PATENTS.txt
*/
import {formatComment, indent, quotePropertyName} from './formatting';
import {Node} from './index';
import {anyType, ParamType, Type} from './types';
/** An AST node that can appear directly in a document or namespace. */
export type Declaration = GlobalNamespace|Namespace|Class|Interface|Function|
ConstValue|Import|Export|TypeAssignment;
export class GlobalNamespace {
readonly kind = 'globalNamespace';
members: Declaration[];
constructor(members?: Declaration[]) {
this.members = members || [];
}
* traverse(): Iterable<Node> {
for (const m of this.members) {
yield* m.traverse();
}
yield this;
}
serialize(depth: number = 0): string {
const i = indent(depth);
let out = `${i}`;
if (depth === 0) {
out += 'declare ';
}
out += `global {\n`;
for (const member of this.members) {
out += '\n' + member.serialize(depth + 1);
}
out += `${i}}\n`;
return out;
}
}
export class Namespace {
readonly kind = 'namespace';
name: string;
description: string;
style: 'namespace'|'module';
members: Declaration[];
constructor(data: {
name: string,
description?: string,
members?: Declaration[],
style?: 'namespace'|'module'
}) {
this.name = data.name;
this.description = data.description || '';
this.members = data.members || [];
this.style = data.style || 'namespace';
}
* traverse(): Iterable<Node> {
for (const m of this.members) {
yield* m.traverse();
}
yield this;
}
serialize(depth: number = 0): string {
let out = '';
if (this.description) {
out += formatComment(this.description, depth);
}
const i = indent(depth);
out += i;
if (depth === 0) {
out += 'declare ';
}
let name = this.name;
if (this.style === 'module') {
// module names can have syntax that's invalid for namespaces,
// like 'goog:foo' or './bar.js'
name = `'${name}'`;
}
out += `${this.style} ${name} {\n`;
for (const member of this.members) {
out += '\n' + member.serialize(depth + 1);
}
out += `${i}}\n`;
return out;
}
}
export class Class {
readonly kind = 'class';
name: string;
description: string;
extends: string;
mixins: string[];
properties: Property[];
methods: Method[];
constructorMethod?: Method;
constructor(data: {
name: string,
description?: string,
extends?: string,
mixins?: string[],
properties?: Property[],
methods?: Method[],
constructorMethod?: Method
}) {
this.name = data.name;
this.description = data.description || '';
this.extends = data.extends || '';
this.mixins = data.mixins || [];
this.properties = data.properties || [];
this.methods = data.methods || [];
this.constructorMethod = data.constructorMethod;
}
* traverse(): Iterable<Node> {
for (const p of this.properties) {
yield* p.traverse();
}
if (this.constructorMethod) {
yield* this.constructorMethod.traverse();
}
for (const m of this.methods) {
yield* m.traverse();
}
yield this;
}
serialize(depth: number = 0): string {
let out = '';
const i = indent(depth);
if (this.description) {
out += formatComment(this.description, depth);
}
out += i;
if (depth === 0) {
out += 'declare ';
}
out += `class ${this.name}`;
if (this.mixins.length) {
const i2 = indent(depth + 1);
out += ' extends';
for (const mixin of this.mixins) {
out += `\n${i2}${mixin}(`;
}
out += `\n${i2}${this.extends || 'Object'}`;
out += ')'.repeat(this.mixins.length);
} else if (this.extends) {
out += ' extends ' + this.extends;
}
out += ' {\n';
for (const property of this.properties) {
out += property.serialize(depth + 1);
}
if (this.constructorMethod) {
out += this.constructorMethod.serialize(depth + 1);
}
for (const method of this.methods) {
out += method.serialize(depth + 1);
}
if (!out.endsWith('\n')) {
out += '\n';
}
out += `${i}}\n`;
return out;
}
}
export class Interface {
readonly kind = 'interface';
name: string;
description: string;
extends: string[];
properties: Property[];
methods: Method[];
constructor(data: {
name: string,
description?: string,
extends?: string[],
properties?: Property[],
methods?: Method[]
}) {
this.name = data.name;
this.description = data.description || '';
this.extends = data.extends || [];
this.properties = data.properties || [];
this.methods = data.methods || [];
}
* traverse(): Iterable<Node> {
for (const p of this.properties) {
yield* p.traverse();
}
for (const m of this.methods) {
yield* m.traverse();
}
yield this;
}
serialize(depth: number = 0): string {
let out = '';
const i = indent(depth);
if (this.description) {
out += formatComment(this.description, depth);
}
out += i;
out += `interface ${this.name}`;
if (this.extends.length) {
out += ' extends ' + this.extends.join(', ');
}
out += ' {\n';
for (const property of this.properties) {
out += property.serialize(depth + 1);
}
for (const method of this.methods) {
out += method.serialize(depth + 1);
}
if (!out.endsWith('\n')) {
out += '\n';
}
out += `${i}}\n`;
return out;
}
}
export abstract class FunctionLike {
kind?: 'method'|'function';
name: string;
description: string;
params: ParamType[];
templateTypes: string[];
returns?: Type;
returnsDescription: string;
isStatic: boolean;
ignoreTypeCheck: boolean;
constructor(data: {
name: string,
description?: string,
params?: ParamType[],
templateTypes?: string[],
returns?: Type,
returnsDescription?: string,
isStatic?: boolean,
ignoreTypeCheck?: boolean,
}) {
this.name = data.name;
this.description = data.description || '';
this.params = data.params || [];
this.returns = data.returns || anyType;
this.templateTypes = data.templateTypes || [];
this.returnsDescription = data.returnsDescription || '';
this.isStatic = data.isStatic || false;
this.ignoreTypeCheck = data.ignoreTypeCheck || false;
}
serialize(depth: number = 0): string {
let out = '';
const i = indent(depth);
const annotations = [];
for (const p of this.params) {
if (p.description) {
annotations.push(` ${p.name} ${p.description}`);
}
}
if (this.returnsDescription) {
annotations.push(` ${this.returnsDescription}`);
}
let combinedDescription = this.description;
if (annotations.length > 0) {
if (combinedDescription) {
combinedDescription += '\n\n';
}
combinedDescription += annotations.join('\n');
}
if (combinedDescription) {
out += '\n' + formatComment(combinedDescription, depth);
}
if (this.ignoreTypeCheck) {
out += indent(depth) + '// @ts-ignore\n';
}
out += i;
if (depth === 0) {
out += 'declare ';
}
if (this.kind === 'method' && this.isStatic) {
out += 'static ';
}
if (this.kind === 'function') {
out += 'function ';
}
out += this.name;
if (this.templateTypes.length > 0) {
out += `<${this.templateTypes.join(', ')}>`;
}
out += '(';
out += this.params.map((p) => p.serialize()).join(', ');
out += `)`;
if (this.returns) {
out += `: ${this.returns.serialize()}`;
}
out += `;\n`;
return out;
}
}
export class Function extends FunctionLike {
readonly kind = 'function';
* traverse(): Iterable<Node> {
for (const p of this.params) {
yield* p.traverse();
}
if (this.returns) {
yield* this.returns.traverse();
}
yield this;
}
}
export class Method extends FunctionLike {
readonly kind = 'method';
* traverse(): Iterable<Node> {
for (const p of this.params) {
yield* p.traverse();
}
if (this.returns) {
yield* this.returns.traverse();
}
yield this;
}
}
export class Property {
readonly kind = 'property';
name: string;
description: string;
type: Type;
readOnly: boolean;
constructor(data: {
name: string,
description?: string,
type?: Type,
readOnly?: boolean,
}) {
this.name = data.name;
this.description = data.description || '';
this.type = data.type || anyType;
this.readOnly = data.readOnly || false;
}
* traverse(): Iterable<Node> {
yield* this.type.traverse();
yield this;
}
serialize(depth: number = 0): string {
let out = '';
const i = indent(depth);
if (this.description) {
out += '\n' + formatComment(this.description, depth);
}
out += i;
if (this.readOnly) {
out += 'readonly ';
}
out += `${quotePropertyName(this.name)}: ${this.type.serialize()};\n`;
return out;
}
}
export class ConstValue {
readonly kind = 'constValue';
name: string;
type: Type;
constructor(data: {
name: string,
type: Type,
}) {
this.name = data.name;
this.type = data.type;
}
* traverse(): Iterable<Node> {
yield* this.type.traverse();
yield this;
}
serialize(depth: number = 0): string {
return indent(depth) + (depth === 0 ? 'declare ' : '') +
`const ${this.name}: ${this.type.serialize()};\n`;
}
}
/**
* The "*" token in an import or export.
*/
export const AllIdentifiers = Symbol('*');
export type AllIdentifiers = typeof AllIdentifiers;
/**
* An identifier that is imported, possibly with a different name.
*/
export interface ImportSpecifier {
identifier: string|AllIdentifiers;
alias?: string;
}
/**
* A JavaScript module import.
*/
export class Import {
readonly kind = 'import';
identifiers: ImportSpecifier[];
fromModuleSpecifier: string;
trailingComment?: string;
constructor(data: {
identifiers: ImportSpecifier[]; fromModuleSpecifier: string,
}) {
this.identifiers = data.identifiers;
this.fromModuleSpecifier = data.fromModuleSpecifier;
}
* traverse(): Iterable<Node> {
yield this;
}
serialize(depth: number = 0): string {
const i = indent(depth);
if (this.identifiers.some((i) => i.identifier === AllIdentifiers)) {
// Namespace imports have a different form. You can also have a default
// import, but no named imports.
const parts = [];
for (const identifier of this.identifiers) {
if (identifier.identifier === 'default') {
parts.push(identifier.alias);
} else if (identifier.identifier === AllIdentifiers) {
parts.push(`* as ${identifier.alias}`);
}
}
return `${i}import ${parts.join(', ')} ` +
`from '${this.fromModuleSpecifier}';` +
`${this.trailingComment || ''}\n`;
}
else {
const parts = [];
for (const {identifier, alias} of this.identifiers) {
if (identifier === AllIdentifiers) {
// Can't happen, see above.
continue;
}
parts.push(
identifier +
(alias !== undefined && alias !== identifier ? ` as ${alias}` :
''));
}
return `${i}import {${parts.join(', ')}} ` +
`from '${this.fromModuleSpecifier}';${this.trailingComment || ''}\n`;
}
}
}
/**
* An identifier that is imported, possibly with a different name.
*/
export interface ExportSpecifier {
identifier: string;
alias?: string;
}
/**
* A JavaScript module export.
*/
export class Export {
readonly kind = 'export';
identifiers: ExportSpecifier[]|AllIdentifiers;
fromModuleSpecifier: string;
trailingComment?: string;
constructor(data: {
identifiers: ExportSpecifier[]|AllIdentifiers,
fromModuleSpecifier?: string
}) {
this.identifiers = data.identifiers;
this.fromModuleSpecifier = data.fromModuleSpecifier || '';
}
* traverse(): Iterable<Node> {
yield this;
}
serialize(depth: number = 0): string {
const i = indent(depth);
let out = `${i}export `;
if (this.identifiers === AllIdentifiers) {
out += '*';
} else {
const specifiers = this.identifiers.map(({identifier, alias}) => {
return identifier +
(alias !== undefined && alias !== identifier ? ` as ${alias}` : '');
});
out += `{${specifiers.join(', ')}}`;
}
if (this.fromModuleSpecifier !== '') {
out += ` from '${this.fromModuleSpecifier}'`;
}
out += `;${this.trailingComment || ''}\n`;
return out;
}
}
export class TypeAssignment {
readonly kind = 'typeAssignment';
name: string;
value: Type;
constructor(data: {name: string, value: Type}) {
this.name = data.name;
this.value = data.value;
}
* traverse(): Iterable<Node> {
yield* this.value.traverse();
yield this;
}
serialize(depth: number = 0): string {
const i = indent(depth);
return `${i}type ${this.name} = ${this.value.serialize()};\n`;
}
}