UNPKG

12 kBJavaScriptView Raw
1"use strict";
2/**
3 * @license
4 * Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
5 * This code may only be used under the BSD style license found at
6 * http://polymer.github.io/LICENSE.txt The complete set of authors may be found
7 * at http://polymer.github.io/AUTHORS.txt The complete set of contributors may
8 * be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by
9 * Google as part of the polymer project is also subject to an additional IP
10 * rights grant found at http://polymer.github.io/PATENTS.txt
11 */
12Object.defineProperty(exports, "__esModule", { value: true });
13const ts = require("./ts-ast");
14const { parseType, parseParamType } = require('doctrine/lib/typed.js');
15/**
16 * Convert a Closure type expression string to its equivalent TypeScript AST
17 * node.
18 *
19 * Note that function and method parameters should instead use
20 * `closureParamToTypeScript`.
21 */
22function closureTypeToTypeScript(closureType, templateTypes = []) {
23 if (!closureType) {
24 return ts.anyType;
25 }
26 let ast;
27 try {
28 ast = parseType(closureType);
29 }
30 catch (_a) {
31 return ts.anyType;
32 }
33 return convert(ast, templateTypes);
34}
35exports.closureTypeToTypeScript = closureTypeToTypeScript;
36/**
37 * Convert a Closure function or method parameter type expression string to its
38 * equivalent TypeScript AST node.
39 *
40 * This differs from `closureTypeToTypeScript` in that it always returns a
41 * `ParamType`, and can parse the optional (`foo=`) and rest (`...foo`)
42 * syntaxes, which only apply when parsing an expression in the context of a
43 * parameter.
44 */
45function closureParamToTypeScript(name, closureType, templateTypes = []) {
46 if (!closureType) {
47 return new ts.ParamType({
48 name,
49 type: ts.anyType,
50 optional: false,
51 rest: false,
52 });
53 }
54 let ast;
55 try {
56 ast = parseParamType(closureType);
57 }
58 catch (_a) {
59 return new ts.ParamType({
60 name,
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 return convertParam(name, ast, templateTypes);
70}
71exports.closureParamToTypeScript = closureParamToTypeScript;
72/**
73 * Convert a doctrine function or method parameter AST node to its equivalent
74 * TypeScript parameter AST node.
75 */
76function convertParam(name, node, templateTypes) {
77 switch (node.type) {
78 case 'OptionalType':
79 return new ts.ParamType({
80 name,
81 type: convert(node.expression, templateTypes),
82 optional: true,
83 rest: false,
84 });
85 case 'RestType':
86 return new ts.ParamType({
87 name,
88 // The Closure type annotation for a rest parameter looks like
89 // `...foo`, where `foo` is implicitly an array. The TypeScript
90 // equivalent is explicitly an array, so we wrap it in one here.
91 type: new ts.ArrayType(node.expression !== undefined ?
92 convert(node.expression, templateTypes) :
93 ts.anyType),
94 optional: false,
95 rest: true,
96 });
97 default:
98 return new ts.ParamType({
99 name,
100 type: convert(node, templateTypes),
101 optional: false,
102 rest: false,
103 });
104 }
105}
106/**
107 * Convert a doctrine AST node to its equivalent TypeScript AST node.
108 */
109function convert(node, templateTypes) {
110 let nullable;
111 if (isNullable(node)) { // ?foo
112 nullable = true;
113 node = node.expression;
114 }
115 else if (isNonNullable(node)) { // !foo
116 nullable = false;
117 node = node.expression;
118 }
119 else if (isName(node) && templateTypes.includes(node.name)) {
120 // A template type "T" looks naively like a regular name type to doctrine
121 // (e.g. a class called "T"), which would be nullable by default. However,
122 // template types are not nullable by default.
123 nullable = false;
124 }
125 else {
126 nullable = nullableByDefault(node);
127 }
128 let t;
129 if (isParameterizedArray(node)) { // Array<foo>
130 t = convertArray(node, templateTypes);
131 }
132 else if (isParameterizedObject(node)) { // Object<foo, bar>
133 t = convertIndexableObject(node, templateTypes);
134 }
135 else if (isBarePromise(node)) { // Promise
136 // In Closure, `Promise` is ok, but in TypeScript this is invalid and must
137 // be explicitly parameterized as `Promise<any>`.
138 t = new ts.ParameterizedType('Promise', [ts.anyType]);
139 }
140 else if (isParameterizedType(node)) { // foo<T>
141 t = convertParameterizedType(node, templateTypes);
142 }
143 else if (isUnion(node)) { // foo|bar
144 t = convertUnion(node, templateTypes);
145 }
146 else if (isFunction(node)) { // function(foo): bar
147 t = convertFunction(node, templateTypes);
148 }
149 else if (isBareArray(node)) { // Array
150 t = new ts.ArrayType(ts.anyType);
151 }
152 else if (isRecordType(node)) { // {foo:bar}
153 t = convertRecord(node, templateTypes);
154 }
155 else if (isAllLiteral(node)) { // *
156 t = ts.anyType;
157 }
158 else if (isNullableLiteral(node)) { // ?
159 t = ts.anyType;
160 }
161 else if (isNullLiteral(node)) { // null
162 t = ts.nullType;
163 }
164 else if (isUndefinedLiteral(node)) { // undefined
165 t = ts.undefinedType;
166 }
167 else if (isVoidLiteral(node)) { // void
168 t = new ts.NameType('void');
169 }
170 else if (isName(node)) { // string, Object, MyClass, etc.
171 t = new ts.NameType(renameMap.get(node.name) || node.name);
172 }
173 else {
174 console.error('Unknown syntax.');
175 return ts.anyType;
176 }
177 if (nullable) {
178 t = new ts.UnionType([t, ts.nullType]);
179 }
180 return t;
181}
182/**
183 * Special cases where a named type in Closure maps to something different in
184 * TypeScript.
185 */
186const renameMap = new Map([
187 // Closure's `Object` type excludes primitives, so it is closest to
188 // TypeScript's `object`. (Technically this should be `object|Symbol`, but we
189 // will concede that technicality.)
190 ['Object', 'object'],
191 // The tagged template literal function argument.
192 ['ITemplateArray', 'TemplateStringsArray'],
193]);
194/*
195 * As above but only applicable when parameterized (`Foo<T>`)
196 */
197const parameterizedRenameMap = new Map([
198 ['HTMLCollection', 'HTMLCollectionOf'],
199 ['NodeList', 'NodeListOf'],
200]);
201/**
202 * Return whether the given AST node is an expression that is nullable by
203 * default in the Closure type system.
204 */
205function nullableByDefault(node) {
206 if (isName(node)) {
207 switch (node.name) {
208 case 'string':
209 case 'number':
210 case 'boolean':
211 case 'void':
212 return false;
213 }
214 return true;
215 }
216 return isParameterizedArray(node);
217}
218function convertArray(node, templateTypes) {
219 const applications = node.applications;
220 return new ts.ArrayType(applications.length === 1 ? convert(applications[0], templateTypes) :
221 ts.anyType);
222}
223function convertIndexableObject(node, templateTypes) {
224 if (node.applications.length !== 2) {
225 console.error('Parameterized Object must have two parameters.');
226 return ts.anyType;
227 }
228 return new ts.IndexableObjectType(convert(node.applications[0], templateTypes), convert(node.applications[1], templateTypes));
229}
230function convertParameterizedType(node, templateTypes) {
231 if (!isName(node.expression)) {
232 console.error('Could not find name of parameterized type');
233 return ts.anyType;
234 }
235 const types = node.applications.map((application) => convert(application, templateTypes));
236 const name = renameMap.get(node.expression.name) ||
237 parameterizedRenameMap.get(node.expression.name) || node.expression.name;
238 return new ts.ParameterizedType(name, types);
239}
240function convertUnion(node, templateTypes) {
241 return new ts.UnionType(node.elements.map((element) => convert(element, templateTypes)));
242}
243function convertFunction(node, templateTypes) {
244 const params = node.params.map((param, idx) => convertParam(
245 // TypeScript wants named parameters, but we don't have names.
246 'p' + idx, param, templateTypes));
247 if (node.new) {
248 return new ts.ConstructorType(params,
249 // It doesn't make sense for a constructor to return something other
250 // than a named type. Also, in this context the name type is not
251 // nullable by default, so it's simpler to just directly convert here.
252 isName(node.this) ? new ts.NameType(node.this.name) : ts.anyType);
253 }
254 else {
255 return new ts.FunctionType(params,
256 // Cast because type is wrong: `FunctionType.result` is not an array.
257 node.result ?
258 convert(node.result, templateTypes) :
259 ts.anyType);
260 }
261}
262function convertRecord(node, templateTypes) {
263 const fields = [];
264 for (const field of node.fields) {
265 if (field.type !== 'FieldType') {
266 return ts.anyType;
267 }
268 const fieldType = field.value ? convert(field.value, templateTypes) : ts.anyType;
269 // In Closure you can't declare a record field optional, instead you
270 // declare `foo: bar|undefined`. In TypeScript we can represent this as
271 // `foo?: bar`. This also matches the semantics better, since Closure would
272 // allow the field to be omitted when it is `|undefined`, but TypeScript
273 // would require it to be explicitly set to `undefined`.
274 let optional = false;
275 if (fieldType.kind === 'union') {
276 fieldType.members = fieldType.members.filter((member) => {
277 if (member.kind === 'name' && member.name === 'undefined') {
278 optional = true;
279 return false;
280 }
281 return true;
282 });
283 // No need for a union if we collapsed it to one member.
284 fieldType.simplify();
285 }
286 fields.push(new ts.ParamType({ name: field.key, type: fieldType, optional }));
287 }
288 return new ts.RecordType(fields);
289}
290function isParameterizedArray(node) {
291 return node.type === 'TypeApplication' &&
292 node.expression.type === 'NameExpression' &&
293 node.expression.name === 'Array';
294}
295function isParameterizedType(node) {
296 return node.type === 'TypeApplication';
297}
298function isBareArray(node) {
299 return node.type === 'NameExpression' && node.name === 'Array';
300}
301function isBarePromise(node) {
302 return node.type === 'NameExpression' && node.name === 'Promise';
303}
304/**
305 * Matches `Object<foo, bar>` but not `Object` (which is a NameExpression).
306 */
307function isParameterizedObject(node) {
308 return node.type === 'TypeApplication' &&
309 node.expression.type === 'NameExpression' &&
310 node.expression.name === 'Object';
311}
312function isUnion(node) {
313 return node.type === 'UnionType';
314}
315function isFunction(node) {
316 return node.type === 'FunctionType';
317}
318function isRecordType(node) {
319 return node.type === 'RecordType';
320}
321function isNullable(node) {
322 return node.type === 'NullableType';
323}
324function isNonNullable(node) {
325 return node.type === 'NonNullableType';
326}
327function isAllLiteral(node) {
328 return node.type === 'AllLiteral';
329}
330function isNullLiteral(node) {
331 return node.type === 'NullLiteral';
332}
333function isNullableLiteral(node) {
334 return node.type === 'NullableLiteral';
335}
336function isUndefinedLiteral(node) {
337 return node.type === 'UndefinedLiteral';
338}
339function isVoidLiteral(node) {
340 return node.type === 'VoidLiteral';
341}
342function isName(node) {
343 return node.type === 'NameExpression';
344}
345//# sourceMappingURL=closure-types.js.map
\No newline at end of file