UNPKG

12.9 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// Useful resources for working on this package:
13// https://eslint.org/doctrine/demo/
14// https://github.com/google/closure-compiler/wiki/Types-in-the-Closure-Type-System
15
16import * as doctrine from 'doctrine';
17
18import * as ts from './ts-ast';
19
20const {parseType, parseParamType} = require('doctrine/lib/typed.js');
21
22/**
23 * Convert a Closure type expression string to its equivalent TypeScript AST
24 * node.
25 *
26 * Note that function and method parameters should instead use
27 * `closureParamToTypeScript`.
28 */
29export function closureTypeToTypeScript(
30 closureType: string|null|undefined, 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 a Closure function or method parameter type expression string to its
45 * equivalent TypeScript AST node.
46 *
47 * This differs from `closureTypeToTypeScript` in that it always returns a
48 * `ParamType`, and can parse the optional (`foo=`) and rest (`...foo`)
49 * syntaxes, which only apply when parsing an expression in the context of a
50 * parameter.
51 */
52export function closureParamToTypeScript(
53 name: string,
54 closureType: string|null|undefined,
55 templateTypes: string[] = [],
56 ): ts.ParamType {
57 if (!closureType) {
58 return new ts.ParamType({
59 name,
60 type: ts.anyType,
61 optional: false,
62 rest: false,
63 });
64 }
65
66 let ast;
67 try {
68 ast = parseParamType(closureType);
69 } catch {
70 return new ts.ParamType({
71 name,
72 type: ts.anyType,
73 // It's important we try to get optional right even if we can't parse
74 // the annotation, because non-optional arguments can't follow optional
75 // ones.
76 optional: closureType.endsWith('='),
77 rest: false,
78 });
79 }
80
81 return convertParam(name, ast, templateTypes);
82}
83
84/**
85 * Convert a doctrine function or method parameter AST node to its equivalent
86 * TypeScript parameter AST node.
87 */
88function convertParam(
89 name: string, node: doctrine.Type, templateTypes: string[]): ts.ParamType {
90 switch (node.type) {
91 case 'OptionalType':
92 return new ts.ParamType({
93 name,
94 type: convert(node.expression, templateTypes),
95 optional: true,
96 rest: false,
97 });
98
99 case 'RestType':
100 return new ts.ParamType({
101 name,
102 // The Closure type annotation for a rest parameter looks like
103 // `...foo`, where `foo` is implicitly an array. The TypeScript
104 // equivalent is explicitly an array, so we wrap it in one here.
105 type: new ts.ArrayType(
106 node.expression !== undefined ?
107 convert(node.expression, templateTypes) :
108 ts.anyType),
109 optional: false,
110 rest: true,
111 });
112
113 default:
114 return new ts.ParamType({
115 name,
116 type: convert(node, templateTypes),
117 optional: false,
118 rest: false,
119 });
120 }
121}
122
123/**
124 * Convert a doctrine AST node to its equivalent TypeScript AST node.
125 */
126function convert(node: doctrine.Type, templateTypes: string[]): ts.Type {
127 let nullable;
128 if (isNullable(node)) { // ?foo
129 nullable = true;
130 node = node.expression;
131 } else if (isNonNullable(node)) { // !foo
132 nullable = false;
133 node = node.expression;
134 } else if (isName(node) && templateTypes.includes(node.name)) {
135 // A template type "T" looks naively like a regular name type to doctrine
136 // (e.g. a class called "T"), which would be nullable by default. However,
137 // template types are not nullable by default.
138 nullable = false;
139 } else {
140 nullable = nullableByDefault(node);
141 }
142
143 let t: ts.Type;
144
145 if (isParameterizedArray(node)) { // Array<foo>
146 t = convertArray(node, templateTypes);
147 } else if (isParameterizedObject(node)) { // Object<foo, bar>
148 t = convertIndexableObject(node, templateTypes);
149 } else if (isBarePromise(node)) { // Promise
150 // In Closure, `Promise` is ok, but in TypeScript this is invalid and must
151 // be explicitly parameterized as `Promise<any>`.
152 t = new ts.ParameterizedType('Promise', [ts.anyType]);
153 } else if (isParameterizedType(node)) { // foo<T>
154 t = convertParameterizedType(node, templateTypes);
155 } else if (isUnion(node)) { // foo|bar
156 t = convertUnion(node, templateTypes);
157 } else if (isFunction(node)) { // function(foo): bar
158 t = convertFunction(node, templateTypes);
159 } else if (isBareArray(node)) { // Array
160 t = new ts.ArrayType(ts.anyType);
161 } else if (isRecordType(node)) { // {foo:bar}
162 t = convertRecord(node, templateTypes);
163 } else if (isAllLiteral(node)) { // *
164 t = ts.anyType;
165 } else if (isNullableLiteral(node)) { // ?
166 t = ts.anyType;
167 } else if (isNullLiteral(node)) { // null
168 t = ts.nullType;
169 } else if (isUndefinedLiteral(node)) { // undefined
170 t = ts.undefinedType;
171 } else if (isVoidLiteral(node)) { // void
172 t = new ts.NameType('void');
173 } else if (isName(node)) { // string, Object, MyClass, etc.
174 t = new ts.NameType(renameMap.get(node.name) || node.name);
175 } else {
176 console.error('Unknown syntax.');
177 return ts.anyType;
178 }
179
180 if (nullable) {
181 t = new ts.UnionType([t, ts.nullType]);
182 }
183
184 return t;
185}
186
187/**
188 * Special cases where a named type in Closure maps to something different in
189 * TypeScript.
190 */
191const renameMap = new Map<string, string>([
192 // Closure's `Object` type excludes primitives, so it is closest to
193 // TypeScript's `object`. (Technically this should be `object|Symbol`, but we
194 // will concede that technicality.)
195 ['Object', 'object'],
196 // The tagged template literal function argument.
197 ['ITemplateArray', 'TemplateStringsArray'],
198]);
199
200/*
201 * As above but only applicable when parameterized (`Foo<T>`)
202 */
203const parameterizedRenameMap = new Map<string, string>([
204 ['HTMLCollection', 'HTMLCollectionOf'],
205 ['NodeList', 'NodeListOf'],
206]);
207
208/**
209 * Return whether the given AST node is an expression that is nullable by
210 * default in the Closure type system.
211 */
212function nullableByDefault(node: doctrine.Type): boolean {
213 if (isName(node)) {
214 switch (node.name) {
215 case 'string':
216 case 'number':
217 case 'boolean':
218 case 'void':
219 return false;
220 }
221 return true;
222 }
223 return isParameterizedArray(node);
224}
225
226function convertArray(
227 node: doctrine.type.TypeApplication, templateTypes: string[]): ts.Type {
228 const applications = node.applications;
229 return new ts.ArrayType(
230 applications.length === 1 ? convert(applications[0], templateTypes) :
231 ts.anyType);
232}
233
234function convertIndexableObject(
235 node: doctrine.type.TypeApplication,
236 templateTypes: string[]): ts.IndexableObjectType|ts.NameType {
237 if (node.applications.length !== 2) {
238 console.error('Parameterized Object must have two parameters.');
239 return ts.anyType;
240 }
241 return new ts.IndexableObjectType(
242 convert(node.applications[0], templateTypes),
243 convert(node.applications[1], templateTypes));
244}
245
246function convertParameterizedType(
247 node: doctrine.type.TypeApplication,
248 templateTypes: string[]): ts.ParameterizedType|ts.NameType {
249 if (!isName(node.expression)) {
250 console.error('Could not find name of parameterized type');
251 return ts.anyType;
252 }
253 const types = node.applications.map(
254 (application) => convert(application, templateTypes));
255 const name = renameMap.get(node.expression.name) ||
256 parameterizedRenameMap.get(node.expression.name) || node.expression.name;
257 return new ts.ParameterizedType(name, types);
258}
259
260function convertUnion(
261 node: doctrine.type.UnionType, templateTypes: string[]): ts.Type {
262 return new ts.UnionType(
263 node.elements.map((element) => convert(element, templateTypes)));
264}
265
266function convertFunction(
267 node: doctrine.type.FunctionType,
268 templateTypes: string[]): ts.FunctionType|ts.ConstructorType {
269 const params = node.params.map(
270 (param, idx) => convertParam(
271 // TypeScript wants named parameters, but we don't have names.
272 'p' + idx,
273 param,
274 templateTypes));
275 if (node.new) {
276 return new ts.ConstructorType(
277 params,
278 // It doesn't make sense for a constructor to return something other
279 // than a named type. Also, in this context the name type is not
280 // nullable by default, so it's simpler to just directly convert here.
281 isName(node.this) ? new ts.NameType(node.this.name) : ts.anyType);
282 } else {
283 return new ts.FunctionType(
284 params,
285 // Cast because type is wrong: `FunctionType.result` is not an array.
286 node.result ?
287 convert(node.result as {} as doctrine.Type, templateTypes) :
288 ts.anyType);
289 }
290}
291
292function convertRecord(node: doctrine.type.RecordType, templateTypes: string[]):
293 ts.RecordType|ts.NameType {
294 const fields = [];
295 for (const field of node.fields) {
296 if (field.type !== 'FieldType') {
297 return ts.anyType;
298 }
299 const fieldType =
300 field.value ? convert(field.value, templateTypes) : ts.anyType;
301
302 // In Closure you can't declare a record field optional, instead you
303 // declare `foo: bar|undefined`. In TypeScript we can represent this as
304 // `foo?: bar`. This also matches the semantics better, since Closure would
305 // allow the field to be omitted when it is `|undefined`, but TypeScript
306 // would require it to be explicitly set to `undefined`.
307 let optional = false;
308 if (fieldType.kind === 'union') {
309 fieldType.members = fieldType.members.filter((member) => {
310 if (member.kind === 'name' && member.name === 'undefined') {
311 optional = true;
312 return false;
313 }
314 return true;
315 });
316
317 // No need for a union if we collapsed it to one member.
318 fieldType.simplify();
319 }
320
321 fields.push(new ts.ParamType({name: field.key, type: fieldType, optional}));
322 }
323 return new ts.RecordType(fields);
324}
325
326function isParameterizedArray(node: doctrine.Type):
327 node is doctrine.type.TypeApplication {
328 return node.type === 'TypeApplication' &&
329 node.expression.type === 'NameExpression' &&
330 node.expression.name === 'Array';
331}
332
333function isParameterizedType(node: doctrine.Type):
334 node is doctrine.type.TypeApplication {
335 return node.type === 'TypeApplication';
336}
337
338function isBareArray(node: doctrine.Type):
339 node is doctrine.type.NameExpression {
340 return node.type === 'NameExpression' && node.name === 'Array';
341}
342
343function isBarePromise(node: doctrine.Type):
344 node is doctrine.type.NameExpression {
345 return node.type === 'NameExpression' && node.name === 'Promise';
346}
347
348/**
349 * Matches `Object<foo, bar>` but not `Object` (which is a NameExpression).
350 */
351function isParameterizedObject(node: doctrine.Type):
352 node is doctrine.type.TypeApplication {
353 return node.type === 'TypeApplication' &&
354 node.expression.type === 'NameExpression' &&
355 node.expression.name === 'Object';
356}
357
358function isUnion(node: doctrine.Type): node is doctrine.type.UnionType {
359 return node.type === 'UnionType';
360}
361
362function isFunction(node: doctrine.Type): node is doctrine.type.FunctionType {
363 return node.type === 'FunctionType';
364}
365
366function isRecordType(node: doctrine.Type): node is doctrine.type.RecordType {
367 return node.type === 'RecordType';
368}
369
370function isNullable(node: doctrine.Type): node is doctrine.type.NullableType {
371 return node.type === 'NullableType';
372}
373
374function isNonNullable(node: doctrine.Type):
375 node is doctrine.type.NonNullableType {
376 return node.type === 'NonNullableType';
377}
378
379function isAllLiteral(node: doctrine.Type): node is doctrine.type.AllLiteral {
380 return node.type === 'AllLiteral';
381}
382
383function isNullLiteral(node: doctrine.Type): node is doctrine.type.NullLiteral {
384 return node.type === 'NullLiteral';
385}
386
387function isNullableLiteral(node: doctrine.Type):
388 node is doctrine.type.NullableLiteral {
389 return node.type === 'NullableLiteral';
390}
391
392function isUndefinedLiteral(node: doctrine.Type):
393 node is doctrine.type.UndefinedLiteral {
394 return node.type === 'UndefinedLiteral';
395}
396
397function isVoidLiteral(node: doctrine.Type): node is doctrine.type.VoidLiteral {
398 return node.type === 'VoidLiteral';
399}
400
401function isName(node: doctrine.Type): node is doctrine.type.NameExpression {
402 return node.type === 'NameExpression';
403}