UNPKG

9.76 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 Object<foo>, Object<foo, bar>
13//
14// Useful resources for working on this package:
15// https://eslint.org/doctrine/demo/
16// https://github.com/google/closure-compiler/wiki/Types-in-the-Closure-Type-System
17
18import * as doctrine from 'doctrine';
19
20import * as ts from './ts-ast';
21
22const {parseType, parseParamType} = require('doctrine/lib/typed.js');
23
24/**
25 * Convert from a type annotation in Closure syntax to a TypeScript type
26 * expression AST (e.g `Array` => `Array<any>|null`).
27 */
28export function closureTypeToTypeScript(
29 closureType: (string|null|undefined),
30 templateTypes: string[] = []): ts.Type {
31 if (!closureType) {
32 return ts.anyType;
33 }
34 let ast;
35 try {
36 ast = parseType(closureType);
37 } catch {
38 return ts.anyType;
39 }
40 return convert(ast, templateTypes);
41}
42
43/**
44 * Convert from a parameter type annotation in Closure syntax to a TypeScript
45 * type expression AST
46 * (e.g `Array=` => `{type: 'Array<any>|null', optional: true}`).
47 */
48export function closureParamToTypeScript(
49 closureType: (string|null|undefined),
50 templateTypes: string[] = [],
51 ): {type: ts.Type, optional: boolean, rest: boolean} {
52 if (!closureType) {
53 return {type: ts.anyType, optional: false, rest: false};
54 }
55
56 let ast;
57 try {
58 ast = parseParamType(closureType);
59 } catch {
60 return {
61 type: ts.anyType,
62 // It's important we try to get optional right even if we can't parse
63 // the annotation, because non-optional arguments can't follow optional
64 // ones.
65 optional: closureType.endsWith('='),
66 rest: false,
67 };
68 }
69
70 // Optional and Rest types are always the top-level node.
71 switch (ast.type) {
72 case 'OptionalType':
73 return {
74 type: convert(ast.expression, templateTypes),
75 optional: true,
76 rest: false,
77 };
78 case 'RestType':
79 return {
80 // The Closure type annotation for a rest parameter looks like
81 // `...foo`, where `foo` is implicitly an array. The TypeScript
82 // equivalent is explicitly an array, so we wrap it in one here.
83 type: new ts.ArrayType(convert(ast.expression, templateTypes)),
84 optional: false,
85 rest: true,
86 };
87 default:
88 return {
89 type: convert(ast, templateTypes),
90 optional: false,
91 rest: false,
92 };
93 }
94}
95
96/**
97 * Format the given Closure type expression AST node as a TypeScript type
98 * annotation string.
99 */
100function convert(node: doctrine.Type, templateTypes: string[]): ts.Type {
101 let nullable;
102 if (isNullable(node)) { // ?foo
103 nullable = true;
104 node = node.expression;
105 } else if (isNonNullable(node)) { // !foo
106 nullable = false;
107 node = node.expression;
108 } else if (isName(node) && templateTypes.includes(node.name)) {
109 // A template type "T" looks naively like a regular name type to doctrine
110 // (e.g. a class called "T"), which would be nullable by default. However,
111 // template types are not nullable by default.
112 nullable = false;
113 } else {
114 nullable = nullableByDefault(node);
115 }
116
117 let t: ts.Type;
118
119 if (isParameterizedArray(node)) { // Array<foo>
120 t = convertArray(node, templateTypes);
121 } else if (isUnion(node)) { // foo|bar
122 t = convertUnion(node, templateTypes);
123 } else if (isFunction(node)) { // function(foo): bar
124 t = convertFunction(node, templateTypes);
125 } else if (isBareArray(node)) { // Array
126 t = new ts.ArrayType(ts.anyType);
127 } else if (isRecordType(node)) { // {foo:bar}
128 t = convertRecord(node, templateTypes);
129 } else if (isAllLiteral(node)) { // *
130 t = ts.anyType;
131 } else if (isNullableLiteral(node)) { // ?
132 t = ts.anyType;
133 } else if (isNullLiteral(node)) { // null
134 t = ts.nullType;
135 } else if (isUndefinedLiteral(node)) { // undefined
136 t = ts.undefinedType;
137 } else if (isVoidLiteral(node)) { // void
138 t = new ts.NameType('void');
139 } else if (isName(node)) { // string, Object, MyClass, etc.
140 if (node.name === 'Object') {
141 // Closure's `Object` type excludes primitives, so it is closest to
142 // TypeScript's `object`. (Technically this should be `object|Symbol`,
143 // but we will concede that technicality.)
144 t = new ts.NameType('object');
145 } else {
146 t = new ts.NameType(node.name);
147 }
148 } else {
149 console.error('Unknown syntax.');
150 return ts.anyType;
151 }
152
153 if (nullable) {
154 t = new ts.UnionType([t, ts.nullType]);
155 }
156
157 return t;
158}
159
160/**
161 * Return whether the given AST node is an expression that is nullable by
162 * default in the Closure type system.
163 */
164function nullableByDefault(node: doctrine.Type): boolean {
165 if (isName(node)) {
166 switch (node.name) {
167 case 'string':
168 case 'number':
169 case 'boolean':
170 case 'void':
171 return false
172 }
173 return true;
174 }
175 return isParameterizedArray(node);
176}
177
178function convertArray(
179 node: doctrine.type.TypeApplication, templateTypes: string[]): ts.Type {
180 const applications = node.applications;
181 return new ts.ArrayType(
182 applications.length === 1 ? convert(applications[0], templateTypes) :
183 ts.anyType);
184}
185
186function convertUnion(
187 node: doctrine.type.UnionType, templateTypes: string[]): ts.Type {
188 return new ts.UnionType(
189 node.elements.map((element) => convert(element, templateTypes)));
190}
191
192function convertFunction(
193 node: doctrine.type.FunctionType,
194 templateTypes: string[]): ts.FunctionType|ts.ConstructorType {
195 const params = node.params.map(
196 (param, idx) => new ts.ParamType(
197 // TypeScript wants named parameters, but we don't have names.
198 'p' + idx,
199 convert(param, templateTypes)));
200 if (node.new) {
201 return new ts.ConstructorType(
202 params,
203 // It doesn't make sense for a constructor to return something other
204 // than a named type. Also, in this context the name type is not
205 // nullable by default, so it's simpler to just directly convert here.
206 isName(node.this) ? new ts.NameType(node.this.name) : ts.anyType);
207 } else {
208 return new ts.FunctionType(
209 params,
210 // Cast because type is wrong: `FunctionType.result` is not an array.
211 node.result ? convert(node.result as any, templateTypes) : ts.anyType);
212 }
213}
214
215function convertRecord(node: doctrine.type.RecordType, templateTypes: string[]):
216 ts.RecordType|ts.NameType {
217 const fields = [];
218 for (const field of node.fields) {
219 if (field.type !== 'FieldType') {
220 return ts.anyType;
221 }
222 let fieldType =
223 field.value ? convert(field.value, templateTypes) : ts.anyType;
224
225 // In Closure you can't declare a record field optional, instead you
226 // declare `foo: bar|undefined`. In TypeScript we can represent this as
227 // `foo?: bar`. This also matches the semantics better, since Closure would
228 // allow the field to be omitted when it is `|undefined`, but TypeScript
229 // would require it to be explicitly set to `undefined`.
230 let optional = false;
231 if (fieldType.kind === 'union') {
232 fieldType.members = fieldType.members.filter((member) => {
233 if (member.kind === 'name' && member.name === 'undefined') {
234 optional = true;
235 return false;
236 }
237 return true;
238 });
239
240 // No need for a union if we collapsed it to one member.
241 fieldType.simplify();
242 }
243
244 fields.push(new ts.ParamType(field.key, fieldType, optional));
245 }
246 return new ts.RecordType(fields);
247}
248
249function isParameterizedArray(node: doctrine.Type):
250 node is doctrine.type.TypeApplication {
251 return node.type === 'TypeApplication' &&
252 node.expression.type === 'NameExpression' &&
253 node.expression.name === 'Array';
254}
255
256function isBareArray(node: doctrine.Type):
257 node is doctrine.type.TypeApplication {
258 return node.type === 'NameExpression' && node.name === 'Array';
259}
260
261function isUnion(node: doctrine.Type): node is doctrine.type.UnionType {
262 return node.type === 'UnionType';
263}
264
265function isFunction(node: doctrine.Type): node is doctrine.type.FunctionType {
266 return node.type === 'FunctionType';
267}
268
269function isRecordType(node: doctrine.Type): node is doctrine.type.RecordType {
270 return node.type === 'RecordType';
271}
272
273function isNullable(node: doctrine.Type): node is doctrine.type.NullableType {
274 return node.type === 'NullableType';
275}
276
277function isNonNullable(node: doctrine.Type):
278 node is doctrine.type.NonNullableType {
279 return node.type === 'NonNullableType';
280}
281
282function isAllLiteral(node: doctrine.Type): node is doctrine.type.AllLiteral {
283 return node.type === 'AllLiteral';
284}
285
286function isNullLiteral(node: doctrine.Type): node is doctrine.type.NullLiteral {
287 return node.type === 'NullLiteral';
288}
289
290function isNullableLiteral(node: doctrine.Type):
291 node is doctrine.type.NullableLiteral {
292 return node.type === 'NullableLiteral';
293}
294
295function isUndefinedLiteral(node: doctrine.Type):
296 node is doctrine.type.UndefinedLiteral {
297 return node.type === 'UndefinedLiteral';
298}
299
300function isVoidLiteral(node: doctrine.Type): node is doctrine.type.VoidLiteral {
301 return node.type === 'VoidLiteral';
302}
303
304function isName(node: doctrine.Type): node is doctrine.type.NameExpression {
305 return node.type === 'NameExpression';
306}