UNPKG

16.6 kBPlain TextView Raw
1/**
2 * @license
3 * Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
4 * This code may only be used under the BSD style license found at
5 * http://polymer.github.io/LICENSE.txt The complete set of authors may be found
6 * at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
7 * be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
8 * Google as part of the polymer project is also subject to an additional IP
9 * rights grant found at http://polymer.github.io/PATENTS.txt
10 */
11
12// TODO Document classes better.
13// TODO Try to make serialization methods easier to read.
14
15export type Node = Document|Namespace|Class|Interface|Mixin|Function|Method|
16 Type|ParamType|Property;
17
18export class Document {
19 readonly kind = 'document';
20 path: string;
21 members: Array<Namespace|Class|Interface|Mixin|Function>;
22 referencePaths: Set<string>;
23 header: string;
24
25 constructor(data: {
26 path: string,
27 members?: Array<Namespace|Class|Interface|Mixin|Function>
28 referencePaths?: Iterable<string>,
29 header?: string
30 }) {
31 this.path = data.path;
32 this.members = data.members || [];
33 this.referencePaths = new Set(Array.from(data.referencePaths || []));
34 this.header = data.header || '';
35 }
36
37 /**
38 * Iterate over all nodes in the document, depth first. Includes all
39 * recursive ancestors, and the document itself.
40 */
41 * traverse(): Iterable<Node> {
42 for (const m of this.members) {
43 yield* m.traverse();
44 }
45 yield this;
46 }
47
48 /**
49 * Clean up this AST.
50 */
51 simplify() {
52 for (const node of this.traverse()) {
53 if (node.kind === 'union') {
54 node.simplify();
55 }
56 }
57 }
58
59 serialize(): string {
60 let out = '';
61 if (this.header) {
62 out += formatComment(this.header, 0) + '\n';
63 }
64 if (this.referencePaths.size > 0) {
65 for (const ref of this.referencePaths) {
66 out += `/// <reference path="${ref}" />\n`;
67 }
68 out += '\n';
69 }
70 out += this.members.map((m) => m.serialize()).join('\n');
71 return out;
72 }
73}
74
75export class Namespace {
76 readonly kind = 'namespace';
77 name: string;
78 description: string;
79 members: Array<Namespace|Class|Interface|Mixin|Function>;
80
81 constructor(data: {
82 name: string,
83 description?: string;
84 members?: Array<Namespace|Class|Interface|Mixin|Function>,
85 }) {
86 this.name = data.name;
87 this.description = data.description || '';
88 this.members = data.members || [];
89 }
90
91 * traverse(): Iterable<Node> {
92 for (const m of this.members) {
93 yield* m.traverse();
94 }
95 yield this;
96 }
97
98 serialize(depth: number = 0): string {
99 let out = ''
100 if (this.description) {
101 out += formatComment(this.description, depth);
102 }
103 const i = indent(depth)
104 out += i
105 if (depth === 0) {
106 out += 'declare ';
107 }
108 out += `namespace ${this.name} {\n`;
109 for (const member of this.members) {
110 out += '\n' + member.serialize(depth + 1);
111 }
112 out += `${i}}\n`;
113 return out;
114 }
115}
116
117export class Class {
118 readonly kind = 'class';
119 name: string;
120 description: string;
121 extends: string;
122 mixins: string[];
123 properties: Property[];
124 methods: Method[];
125
126 constructor(data: {
127 name: string,
128 description?: string,
129 extends?: string,
130 mixins?: string[],
131 properties?: Property[],
132 methods?: Method[]
133 }) {
134 this.name = data.name;
135 this.description = data.description || '';
136 this.extends = data.extends || '';
137 this.mixins = data.mixins || [];
138 this.properties = data.properties || [];
139 this.methods = data.methods || [];
140 }
141
142 * traverse(): Iterable<Node> {
143 for (const p of this.properties) {
144 yield* p.traverse();
145 }
146 for (const m of this.methods) {
147 yield* m.traverse();
148 }
149 yield this;
150 }
151
152 serialize(depth: number = 0): string {
153 let out = '';
154 const i = indent(depth);
155 if (this.description) {
156 out += formatComment(this.description, depth);
157 }
158 out += i;
159 if (depth === 0) {
160 out += 'declare ';
161 }
162 out += `class ${this.name}`;
163
164 if (this.mixins.length) {
165 const i2 = indent(depth + 1);
166 out += ' extends';
167 for (const mixin of this.mixins) {
168 out += `\n${i2}${mixin}(`;
169 }
170 out += `\n${i2}${this.extends || 'Object'}`;
171 out += ')'.repeat(this.mixins.length)
172
173 } else if (this.extends) {
174 out += ' extends ' + this.extends;
175 }
176
177 out += ' {\n';
178 for (const property of this.properties) {
179 out += property.serialize(depth + 1);
180 }
181 for (const method of this.methods) {
182 out += method.serialize(depth + 1);
183 }
184 if (!out.endsWith('\n')) {
185 out += '\n';
186 }
187 out += `${i}}\n`;
188 return out;
189 }
190}
191
192export class Interface {
193 readonly kind = 'interface';
194 name: string;
195 description: string;
196 extends: string[];
197 properties: Property[];
198 methods: Method[];
199
200 constructor(data: {
201 name: string,
202 description?: string,
203 extends?: string[],
204 properties?: Property[],
205 methods?: Method[]
206 }) {
207 this.name = data.name;
208 this.description = data.description || '';
209 this.extends = data.extends || [];
210 this.properties = data.properties || [];
211 this.methods = data.methods || [];
212 }
213
214 * traverse(): Iterable<Node> {
215 for (const p of this.properties) {
216 yield* p.traverse();
217 }
218 for (const m of this.methods) {
219 yield* m.traverse();
220 }
221 yield this;
222 }
223
224 serialize(depth: number = 0): string {
225 let out = '';
226 const i = indent(depth);
227 if (this.description) {
228 out += formatComment(this.description, depth);
229 }
230 out += i;
231 out += `interface ${this.name}`;
232 if (this.extends.length) {
233 out += ' extends ' + this.extends.join(', ');
234 }
235 out += ' {\n';
236 for (const property of this.properties) {
237 out += property.serialize(depth + 1);
238 }
239 for (const method of this.methods) {
240 out += method.serialize(depth + 1);
241 }
242 if (!out.endsWith('\n')) {
243 out += '\n';
244 }
245 out += `${i}}\n`;
246 return out;
247 }
248}
249
250// A class mixin function using the pattern described at:
251// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html
252export class Mixin {
253 readonly kind = 'mixin';
254 name: string;
255 description: string;
256 interfaces: string[];
257
258 constructor(data: {
259 name: string,
260 description?: string,
261 interfaces?: string[],
262 }) {
263 this.name = data.name;
264 this.description = data.description || '';
265 this.interfaces = data.interfaces || [];
266 }
267
268 * traverse(): Iterable<Node> {
269 yield this;
270 }
271
272 serialize(depth: number = 0): string {
273 let out = '';
274 const i = indent(depth);
275 const i2 = indent(depth + 1);
276 if (this.description) {
277 out += formatComment(this.description, depth);
278 }
279 out += i;
280 if (depth === 0) {
281 out += 'declare ';
282 }
283 out += `function ${this.name}`;
284 out += `<T extends new(...args: any[]) => {}>(base: T): {\n`;
285 out += `${i2}new(...args: any[]): ${this.interfaces.join(' & ')}\n`;
286 out += `${i}} & T\n`;
287 return out;
288 }
289}
290
291export abstract class FunctionLike {
292 kind: string;
293 name: string;
294 description: string;
295 params: Param[];
296 templateTypes: string[];
297 returns: Type;
298 returnsDescription: string;
299
300 constructor(data: {
301 name: string,
302 description?: string,
303 params?: Param[],
304 templateTypes?: string[],
305 returns?: Type,
306 returnsDescription?: string
307 }) {
308 this.name = data.name;
309 this.description = data.description || '';
310 this.params = data.params || [];
311 this.returns = data.returns || anyType;
312 this.templateTypes = data.templateTypes || [];
313 this.returnsDescription = data.returnsDescription || '';
314 }
315
316 serialize(depth: number = 0): string {
317 let out = ''
318 const i = indent(depth);
319
320 const annotations = [];
321 for (const p of this.params) {
322 if (p.description) {
323 annotations.push(`@param ${p.name} ${p.description}`);
324 }
325 }
326 if (this.returnsDescription) {
327 annotations.push(`@returns ${this.returnsDescription}`);
328 }
329
330 let combinedDescription = this.description;
331 if (annotations.length > 0) {
332 if (combinedDescription) {
333 combinedDescription += '\n\n';
334 }
335 combinedDescription += annotations.join('\n');
336 }
337 if (combinedDescription) {
338 out += '\n' + formatComment(combinedDescription, depth);
339 }
340
341 if (depth === 0) {
342 out += 'declare ';
343 }
344 out += i;
345 if (this.kind === 'function') {
346 out += 'function ';
347 }
348 out += this.name;
349 if (this.templateTypes.length > 0) {
350 out += `<${this.templateTypes.join(', ')}>`;
351 }
352 out += '(';
353 out += this.params.map((p) => p.serialize()).join(', ');
354 out += `): ${this.returns.serialize()};\n`;
355 return out;
356 }
357}
358
359export class Function extends FunctionLike {
360 readonly kind = 'function';
361
362 * traverse(): Iterable<Node> {
363 for (const p of this.params) {
364 yield* p.traverse();
365 }
366 yield* this.returns.traverse();
367 yield this;
368 }
369}
370
371export class Method extends FunctionLike {
372 readonly kind = 'method';
373
374 * traverse(): Iterable<Node> {
375 for (const p of this.params) {
376 yield* p.traverse();
377 }
378 yield* this.returns.traverse();
379 yield this;
380 }
381}
382
383export class Property {
384 readonly kind = 'property';
385 name: string;
386 description: string;
387 type: Type;
388
389 constructor(data: {name: string, description?: string, type?: Type}) {
390 this.name = data.name;
391 this.description = data.description || '';
392 this.type = data.type || anyType;
393 }
394
395 * traverse(): Iterable<Node> {
396 yield* this.type.traverse();
397 yield this;
398 }
399
400 serialize(depth: number = 0): string {
401 let out = '';
402 const i = indent(depth);
403 if (this.description) {
404 out += '\n' + formatComment(this.description, depth);
405 }
406 out += `${i}${quotePropertyName(this.name)}: ${this.type.serialize()};\n`;
407 return out;
408 }
409}
410
411export class Param {
412 readonly kind = 'param';
413 name: string;
414 type: Type;
415 optional: boolean;
416 rest: boolean;
417 description: string;
418
419 constructor(data: {
420 name: string,
421 type: Type,
422 optional?: boolean,
423 rest?: boolean,
424 description?: string
425 }) {
426 this.name = data.name;
427 this.type = data.type || anyType;
428 this.optional = data.optional || false;
429 this.rest = data.rest || false;
430 this.description = data.description || '';
431 }
432
433 * traverse(): Iterable<Node> {
434 yield* this.type.traverse();
435 yield this;
436 }
437
438 serialize(): string {
439 let out = '';
440 if (this.rest) {
441 out += '...';
442 }
443 out += this.name;
444 if (this.optional) {
445 out += '?';
446 }
447 out += ': ' + this.type.serialize();
448 return out;
449 }
450}
451
452// A TypeScript type expression.
453export type Type =
454 NameType|UnionType|ArrayType|FunctionType|ConstructorType|RecordType;
455
456// string, MyClass, null, undefined, any
457export class NameType {
458 readonly kind = 'name';
459 name: string;
460
461 constructor(name: string) {
462 this.name = name;
463 };
464
465 * traverse(): Iterable<Node> {
466 yield this;
467 }
468
469 serialize(): string {
470 return this.name;
471 }
472}
473
474// foo|bar
475export class UnionType {
476 readonly kind = 'union';
477 members: Type[];
478
479 constructor(members: Type[]) {
480 this.members = members;
481 }
482
483 * traverse(): Iterable<Node> {
484 for (const m of this.members) {
485 yield* m.traverse();
486 }
487 yield this;
488 }
489
490 /**
491 * Simplify this union type:
492 *
493 * 1) Flatten nested unions (`foo|(bar|baz)` -> `foo|bar|baz`).
494 * 2) De-duplicate identical members (`foo|bar|foo` -> `foo|bar`).
495 */
496 simplify() {
497 const flattened = [];
498 for (const m of this.members) {
499 if (m.kind === 'union') {
500 // Note we are not recursing here, because we assume we're being called
501 // via a depth-first walk, so any union members have already been
502 // simplified.
503 flattened.push(...m.members);
504 } else {
505 flattened.push(m);
506 }
507 }
508
509 // TODO This only de-dupes Name types. We should de-dupe Arrays and
510 // Functions too.
511 const deduped = [];
512 const names = new Set();
513 let hasNull = false;
514 let hasUndefined = false;
515 for (const m of flattened) {
516 if (m.kind === 'name') {
517 if (m.name === 'null') {
518 hasNull = true;
519 } else if (m.name === 'undefined') {
520 hasUndefined = true;
521 } else if (!names.has(m.name)) {
522 deduped.push(m);
523 names.add(m.name);
524 }
525 } else {
526 deduped.push(m);
527 }
528 }
529 // Always put `null` and `undefined` at the end because it's more readable.
530 // Preserve declared order for everything else.
531 if (hasNull) {
532 deduped.push(nullType);
533 }
534 if (hasUndefined) {
535 deduped.push(undefinedType);
536 }
537 this.members = deduped;
538 }
539
540 serialize(): string {
541 return this.members
542 .map((member) => {
543 let s = member.serialize();
544 if (member.kind === 'function') {
545 // The function syntax is ambiguous when part of a union, so add
546 // parens (e.g. `() => string|null` vs `(() => string)|null`).
547 s = '(' + s + ')';
548 }
549 return s;
550 })
551 .join('|');
552 }
553}
554
555// Array<foo>
556export class ArrayType {
557 readonly kind = 'array';
558 itemType: Type;
559
560 constructor(itemType: Type) {
561 this.itemType = itemType;
562 }
563
564 * traverse(): Iterable<Node> {
565 yield* this.itemType.traverse();
566 yield this;
567 }
568
569 serialize(): string {
570 if (this.itemType.kind === 'name') {
571 // Use the concise `foo[]` syntax when the item type is simple.
572 return `${this.itemType.serialize()}[]`;
573 } else {
574 // Otherwise use the `Array<foo>` syntax which is easier to read with
575 // complex types (e.g. arrays of arrays).
576 return `Array<${this.itemType.serialize()}>`;
577 }
578 }
579}
580
581// (foo: bar) => baz
582export class FunctionType {
583 readonly kind = 'function';
584 params: ParamType[];
585 returns: Type;
586
587 constructor(params: ParamType[], returns: Type) {
588 this.params = params;
589 this.returns = returns;
590 }
591
592 * traverse(): Iterable<Node> {
593 for (const p of this.params) {
594 yield* p.traverse();
595 }
596 yield* this.returns.traverse();
597 yield this;
598 }
599
600 serialize(): string {
601 const params = this.params.map((param) => param.serialize());
602 return `(${params.join(', ')}) => ${this.returns.serialize()}`;
603 }
604}
605
606// {new(foo): bar}
607export class ConstructorType {
608 readonly kind = 'constructor';
609 params: ParamType[];
610 returns: NameType;
611
612 constructor(params: ParamType[], returns: NameType) {
613 this.params = params;
614 this.returns = returns;
615 }
616
617 * traverse(): Iterable<Node> {
618 for (const p of this.params) {
619 yield* p.traverse();
620 }
621 yield* this.returns.traverse();
622 yield this;
623 }
624
625 serialize(): string {
626 const params = this.params.map((param) => param.serialize());
627 return `{new(${params.join(', ')}): ${this.returns.serialize()}}`;
628 }
629}
630
631// foo: bar
632export class ParamType {
633 readonly kind = 'param';
634 name: string;
635 type: Type;
636 optional: boolean;
637
638 * traverse(): Iterable<Node> {
639 yield* this.type.traverse();
640 yield this;
641 }
642
643 constructor(name: string, type: Type, optional: boolean = false) {
644 this.name = name;
645 this.type = type;
646 this.optional = optional;
647 }
648
649 serialize() {
650 return `${this.name}${this.optional ? '?' : ''}: ${this.type.serialize()}`;
651 }
652}
653
654export class RecordType {
655 readonly kind = 'record';
656 fields: ParamType[];
657
658 constructor(fields: ParamType[]) {
659 this.fields = fields;
660 }
661
662 * traverse(): Iterable<Node> {
663 for (const m of this.fields) {
664 yield* m.traverse();
665 }
666 yield this;
667 }
668
669 serialize(): string {
670 const fields = this.fields.map((field) => field.serialize());
671 return `{${fields.join(', ')}}`;
672 }
673}
674
675export const anyType = new NameType('any');
676export const nullType = new NameType('null');
677export const undefinedType = new NameType('undefined');
678
679function quotePropertyName(name: string): string {
680 // TODO We should escape reserved words, and there are many more safe
681 // characters than are included in this RegExp.
682 // See https://mathiasbynens.be/notes/javascript-identifiers-es6
683 const safe = name.match(/^[_$a-zA-Z][_$a-zA-Z0-9]*$/);
684 return safe ? name : JSON.stringify(name);
685}
686
687const indentSpaces = 2;
688
689function indent(depth: number): string {
690 return ' '.repeat(depth * indentSpaces);
691}
692
693function formatComment(comment: string, depth: number): string {
694 const i = indent(depth);
695 return `${i}/**\n` +
696 comment.replace(/^(.)/gm, ' $1').replace(/^/gm, `${i} *`) + `\n${i} */\n`;
697}