UNPKG

19.7 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3const ts_morph_1 = require("ts-morph");
4const errors_1 = require("../errors");
5const locations_1 = require("../locations");
6const types_1 = require("../types");
7const util_1 = require("../util");
8const parser_helpers_1 = require("./parser-helpers");
9function parseType(typeNode, typeTable, lociTable) {
10 // Type references must be parsed first to ensure internal type aliases are handled
11 if (ts_morph_1.TypeGuards.isTypeReferenceNode(typeNode)) {
12 if (typeNode.getType().isArray() &&
13 typeNode.getTypeArguments().length > 0) {
14 // TypeScript forbids use of Array constructor without at least one type argument
15 return parseArrayConstructorType(typeNode, typeTable, lociTable);
16 }
17 return parseTypeReference(typeNode, typeTable, lociTable);
18 }
19 else if (ts_morph_1.TypeGuards.isNullLiteral(typeNode)) {
20 return util_1.ok(types_1.nullType());
21 // TODO: discourage native boolean keyword?
22 }
23 else if (ts_morph_1.TypeGuards.isBooleanKeyword(typeNode)) {
24 return util_1.ok(types_1.booleanType());
25 // TODO: discourage native string keyword?
26 }
27 else if (ts_morph_1.TypeGuards.isStringKeyword(typeNode)) {
28 return util_1.ok(types_1.stringType());
29 // TODO: discourage native number keyword?
30 }
31 else if (ts_morph_1.TypeGuards.isNumberKeyword(typeNode)) {
32 return util_1.ok(types_1.floatType());
33 }
34 else if (ts_morph_1.TypeGuards.isLiteralTypeNode(typeNode)) {
35 return parseLiteralType(typeNode);
36 }
37 else if (ts_morph_1.TypeGuards.isArrayTypeNode(typeNode)) {
38 return parseArrayType(typeNode, typeTable, lociTable);
39 }
40 else if (ts_morph_1.TypeGuards.isTypeLiteralNode(typeNode)) {
41 return parseObjectLiteralType(typeNode, typeTable, lociTable);
42 }
43 else if (ts_morph_1.TypeGuards.isUnionTypeNode(typeNode)) {
44 return parseUnionType(typeNode, typeTable, lociTable);
45 }
46 else if (ts_morph_1.TypeGuards.isIndexedAccessTypeNode(typeNode)) {
47 return parseIndexedAccessType(typeNode, typeTable, lociTable);
48 }
49 else {
50 throw new errors_1.TypeNotAllowedError("unknown type", {
51 file: typeNode.getSourceFile().getFilePath(),
52 position: typeNode.getPos()
53 });
54 }
55}
56exports.parseType = parseType;
57// TODO: store this somewhere else to be more typesafe
58const SPOT_TYPE_ALIASES = [
59 "Date",
60 "DateTime",
61 "Number",
62 "Double",
63 "Float",
64 "Integer",
65 "Int32",
66 "Int64",
67 "String"
68];
69/**
70 * Parse an reference node. Reference nodes refer to type aliases and interfaces.
71 *
72 * @param typeNode AST type node
73 * @param typeTable a TypeTable
74 * @param lociTable a LociTable
75 */
76function parseTypeReference(typeNode, typeTable, lociTable) {
77 var _a;
78 const declarationResult = getTargetDeclarationFromTypeReference(typeNode);
79 if (declarationResult.isErr())
80 return declarationResult;
81 const declaration = declarationResult.unwrap();
82 const name = declaration.getName();
83 const description = (_a = parser_helpers_1.getJsDoc(declaration)) === null || _a === void 0 ? void 0 : _a.getDescription().trim();
84 if (ts_morph_1.TypeGuards.isTypeAliasDeclaration(declaration)) {
85 const decTypeNode = declaration.getTypeNodeOrThrow();
86 // if the type name is one of of the internal ones ensure they have not been redefined
87 // TODO: introduce some more type safety
88 if (SPOT_TYPE_ALIASES.includes(name)) {
89 if (ts_morph_1.TypeGuards.isTypeReferenceNode(decTypeNode)) {
90 throw new Error(`Internal type ${name} must not be redefined`);
91 }
92 else if (declaration.getType().isString()) {
93 switch (name) {
94 case "String":
95 return util_1.ok(types_1.stringType());
96 case "Date":
97 return util_1.ok(types_1.dateType());
98 case "DateTime":
99 return util_1.ok(types_1.dateTimeType());
100 default:
101 throw new Error(`Internal type ${name} must not be redefined`);
102 }
103 }
104 else if (declaration.getType().isNumber()) {
105 switch (name) {
106 case "Number":
107 case "Float":
108 return util_1.ok(types_1.floatType());
109 case "Double":
110 return util_1.ok(types_1.doubleType());
111 case "Integer":
112 case "Int32":
113 return util_1.ok(types_1.int32Type());
114 case "Int64":
115 return util_1.ok(types_1.int64Type());
116 default:
117 throw new Error(`Internal type ${name} must not be redefined`);
118 }
119 }
120 else {
121 throw new Error(`Internal type ${name} must not be redefined`);
122 }
123 }
124 else {
125 if (typeTable.exists(name)) {
126 if (!lociTable.equalsMorphNode(locations_1.LociTable.typeKey(name), decTypeNode)) {
127 throw new Error(`Type ${name} defined multiple times`);
128 }
129 }
130 else {
131 const targetTypeResult = parseType(decTypeNode, typeTable, lociTable);
132 if (targetTypeResult.isErr())
133 return targetTypeResult;
134 typeTable.add(name, { type: targetTypeResult.unwrap(), description });
135 lociTable.addMorphNode(locations_1.LociTable.typeKey(name), decTypeNode);
136 }
137 return util_1.ok(types_1.referenceType(name));
138 }
139 }
140 else {
141 if (SPOT_TYPE_ALIASES.includes(name)) {
142 throw new Error(`Internal type ${name} must not be redefined`);
143 }
144 else {
145 if (typeTable.exists(name)) {
146 if (!lociTable.equalsMorphNode(locations_1.LociTable.typeKey(name), declaration)) {
147 throw new Error(`Type ${name} defined multiple times`);
148 }
149 }
150 else {
151 const targetTypeResult = parseInterfaceDeclaration(declaration, typeTable, lociTable);
152 if (targetTypeResult.isErr())
153 return targetTypeResult;
154 typeTable.add(name, { type: targetTypeResult.unwrap(), description });
155 lociTable.addMorphNode(locations_1.LociTable.typeKey(name), declaration);
156 }
157 return util_1.ok(types_1.referenceType(name));
158 }
159 }
160}
161/**
162 * AST literal types include literal booleans, strings and numbers.
163 *
164 * @param typeNode AST type node
165 */
166function parseLiteralType(typeNode) {
167 const literal = typeNode.getLiteral();
168 if (ts_morph_1.TypeGuards.isBooleanLiteral(literal)) {
169 return util_1.ok(types_1.booleanLiteralType(literal.getLiteralValue()));
170 }
171 else if (ts_morph_1.TypeGuards.isStringLiteral(literal)) {
172 return util_1.ok(types_1.stringLiteralType(literal.getLiteralText()));
173 }
174 else if (ts_morph_1.TypeGuards.isNumericLiteral(literal)) {
175 const numericValue = literal.getLiteralValue();
176 return util_1.ok(Number.isInteger(numericValue)
177 ? types_1.intLiteralType(numericValue)
178 : types_1.floatLiteralType(numericValue));
179 }
180 else {
181 return util_1.err(new errors_1.TypeNotAllowedError("unexpected literal type", {
182 file: typeNode.getSourceFile().getFilePath(),
183 position: typeNode.getPos()
184 }));
185 }
186}
187/**
188 * Parse an array node.
189 *
190 * @param typeNode AST type node
191 * @param typeTable a TypeTable
192 * @param lociTable a LociTable
193 *
194 * @example
195 * ```ts
196 * let array: string[];
197 * ```
198 */
199function parseArrayType(typeNode, typeTable, lociTable) {
200 const elementDataTypeResult = parseType(typeNode.getElementTypeNode(), typeTable, lociTable);
201 if (elementDataTypeResult.isErr())
202 return elementDataTypeResult;
203 return util_1.ok(types_1.arrayType(elementDataTypeResult.unwrap()));
204}
205/**
206 * Parse an array constructor type.
207 *
208 * @param typeNode AST type node
209 * @param typeTable a TypeTable
210 * @param lociTable a LociTable
211 */
212function parseArrayConstructorType(typeNode, typeTable, lociTable) {
213 const typeArguments = typeNode.getTypeArguments();
214 if (typeArguments.length !== 1) {
215 return util_1.err(new errors_1.ParserError("Array types must declare exactly one argument", {
216 file: typeNode.getSourceFile().getFilePath(),
217 position: typeNode.getPos()
218 }));
219 }
220 const elementDataTypeResult = parseType(typeArguments[0], typeTable, lociTable);
221 if (elementDataTypeResult.isErr())
222 return elementDataTypeResult;
223 return util_1.ok(types_1.arrayType(elementDataTypeResult.unwrap()));
224}
225/**
226 * Parse an object literal type.
227 *
228 * NOTE: this parser is limited to `TypeLiteralNode`s. Although `InterfaceDeclaration`s have
229 * a very similar structure (both extend `TypeElementMemberedNode`), `InterfaceDeclaration`s
230 * may additionally extend other `InterfaceDeclaration`s which should be considered separately.
231 *
232 * @param typeNode AST type node
233 * @param typeTable a TypeTable
234 * @param lociTable a LociTable
235 */
236function parseObjectLiteralType(typeNode, typeTable, lociTable) {
237 var _a;
238 const indexSignatures = typeNode.getIndexSignatures();
239 if (indexSignatures.length > 0) {
240 return util_1.err(new errors_1.TypeNotAllowedError("indexed types are not supported", {
241 file: indexSignatures[0].getSourceFile().getFilePath(),
242 position: indexSignatures[0].getPos()
243 }));
244 }
245 const objectProperties = [];
246 for (const ps of typeNode.getProperties()) {
247 const propTypeResult = parseType(ps.getTypeNodeOrThrow(), typeTable, lociTable);
248 if (propTypeResult.isErr())
249 return propTypeResult;
250 const prop = {
251 name: parser_helpers_1.getPropertyName(ps),
252 description: (_a = parser_helpers_1.getJsDoc(ps)) === null || _a === void 0 ? void 0 : _a.getDescription().trim(),
253 type: propTypeResult.unwrap(),
254 optional: ps.hasQuestionToken()
255 };
256 objectProperties.push(prop);
257 }
258 return util_1.ok(types_1.objectType(objectProperties));
259}
260/**
261 * Parse an interface declaration. Resulting object properties will
262 * include those from the extended interface hierarchy.
263 *
264 * @param interfaceDeclaration interface declaration
265 * @param typeTable a TypeTable
266 * @param lociTable a LociTable
267 */
268function parseInterfaceDeclaration(interfaceDeclaration, typeTable, lociTable) {
269 var _a;
270 const indexSignatures = interfaceDeclaration.getIndexSignatures();
271 if (indexSignatures.length > 0) {
272 return util_1.err(new errors_1.TypeNotAllowedError("indexed types are not supported", {
273 file: indexSignatures[0].getSourceFile().getFilePath(),
274 position: indexSignatures[0].getPos()
275 }));
276 }
277 const propertySignatures = interfaceDeclaration
278 .getType()
279 .getProperties()
280 .map(propertySymbol => {
281 const vd = propertySymbol.getValueDeclarationOrThrow();
282 if (!ts_morph_1.TypeGuards.isPropertySignature(vd)) {
283 throw new Error("expected property signature");
284 }
285 return vd;
286 });
287 const objectProperties = [];
288 for (const ps of propertySignatures) {
289 const propTypeResult = parseType(ps.getTypeNodeOrThrow(), typeTable, lociTable);
290 if (propTypeResult.isErr())
291 return propTypeResult;
292 const prop = {
293 name: parser_helpers_1.getPropertyName(ps),
294 description: (_a = parser_helpers_1.getJsDoc(ps)) === null || _a === void 0 ? void 0 : _a.getDescription().trim(),
295 type: propTypeResult.unwrap(),
296 optional: ps.hasQuestionToken()
297 };
298 objectProperties.push(prop);
299 }
300 return util_1.ok(types_1.objectType(objectProperties));
301}
302/**
303 * Parse a union type node.
304 *
305 * @param typeNode union type node
306 * @param typeTable a TypeTable
307 * @param lociTable a LociTable
308 */
309function parseUnionType(typeNode, typeTable, lociTable) {
310 const allowedTargetTypes = typeNode
311 .getTypeNodes()
312 .filter(type => !type.getType().isUndefined());
313 switch (allowedTargetTypes.length) {
314 case 0:
315 return util_1.err(new errors_1.TypeNotAllowedError("malformed union type", {
316 file: typeNode.getSourceFile().getFilePath(),
317 position: typeNode.getPos()
318 }));
319 case 1:
320 // not a union
321 return parseType(allowedTargetTypes[0], typeTable, lociTable);
322 default: {
323 const types = [];
324 for (const tn of allowedTargetTypes) {
325 const typeResult = parseType(tn, typeTable, lociTable);
326 if (typeResult.isErr())
327 return typeResult;
328 types.push(typeResult.unwrap());
329 }
330 return util_1.ok(types_1.unionType(types, types_1.inferDiscriminator(types, typeTable)));
331 }
332 }
333}
334/**
335 * Parse a indexed access type node.
336 *
337 * @param typeNode AST type node
338 * @param typeTable a TypeTable
339 * @param lociTable a LociTable
340 */
341function parseIndexedAccessType(typeNode, typeTable, lociTable) {
342 const propertyAccessChainResult = resolveIndexAccessPropertyAccessChain(typeNode);
343 if (propertyAccessChainResult.isErr())
344 return propertyAccessChainResult;
345 const rootReferenceResult = resolveIndexedAccessRootReference(typeNode);
346 if (rootReferenceResult.isErr())
347 return rootReferenceResult;
348 const refTypeResult = parseTypeReference(rootReferenceResult.unwrap(), typeTable, lociTable);
349 if (refTypeResult.isErr())
350 return refTypeResult;
351 const refType = refTypeResult.unwrap();
352 if (refType.kind !== types_1.TypeKind.REFERENCE) {
353 return util_1.err(new errors_1.TypeNotAllowedError("Indexed access type must be reference", {
354 file: typeNode.getSourceFile().getFilePath(),
355 position: typeNode.getPos()
356 }));
357 }
358 const resolvedType = resolveIndexedAccessType(propertyAccessChainResult.unwrap(), refType, typeTable);
359 return util_1.ok(resolvedType);
360}
361/**
362 * Resolve the target type for an indexed access type.
363 *
364 * @param propertyChain properties to traverse
365 * @param currentType type to inspect
366 * @param typeTable a TypeTable
367 */
368function resolveIndexedAccessType(propertyChain, currentType, typeTable) {
369 if (propertyChain.length === 0)
370 return currentType;
371 if (currentType.kind === types_1.TypeKind.OBJECT) {
372 const property = currentType.properties.find(p => p.name === propertyChain[0]);
373 if (property === undefined) {
374 throw new Error("Indexed type property not found");
375 }
376 return resolveIndexedAccessType(propertyChain.slice(1), property.type, typeTable);
377 }
378 if (currentType.kind === types_1.TypeKind.REFERENCE) {
379 const referencedType = typeTable.getOrError(currentType.name).type;
380 return resolveIndexedAccessType(propertyChain, referencedType, typeTable);
381 }
382 throw new Error("Indexed type error");
383}
384/**
385 * Resolve the root reference type of an indexed access type.
386 *
387 * @param typeNode an indexed access type node
388 */
389function resolveIndexedAccessRootReference(typeNode) {
390 const objectType = typeNode.getObjectTypeNode();
391 if (ts_morph_1.TypeGuards.isIndexedAccessTypeNode(objectType)) {
392 return resolveIndexedAccessRootReference(objectType);
393 }
394 if (!ts_morph_1.TypeGuards.isTypeReferenceNode(objectType)) {
395 return util_1.err(new errors_1.TypeNotAllowedError("Indexed access type must be reference", {
396 file: objectType.getSourceFile().getFilePath(),
397 position: objectType.getPos()
398 }));
399 }
400 return util_1.ok(objectType);
401}
402/**
403 * Resolve the property access chain of an indexed access type.
404 *
405 * @param typeNode an indexed access type node
406 * @param accResult property chain result accumulator
407 */
408function resolveIndexAccessPropertyAccessChain(typeNode, accResult = util_1.ok([])) {
409 if (accResult.isErr())
410 return accResult;
411 const acc = accResult.unwrap();
412 const literalTypeNode = typeNode.getIndexTypeNode();
413 if (!ts_morph_1.TypeGuards.isLiteralTypeNode(literalTypeNode)) {
414 throw new Error("expected type literal");
415 }
416 const literalTypeResult = parseLiteralType(literalTypeNode);
417 if (literalTypeResult.isErr())
418 return literalTypeResult;
419 const literalType = literalTypeResult.unwrap();
420 if (literalType.kind !== types_1.TypeKind.STRING_LITERAL) {
421 throw new Error("expected string literal");
422 }
423 const chainParent = typeNode.getObjectTypeNode();
424 if (ts_morph_1.TypeGuards.isIndexedAccessTypeNode(chainParent)) {
425 return resolveIndexAccessPropertyAccessChain(chainParent, util_1.ok(acc.concat(literalType.value)));
426 }
427 return util_1.ok(acc.concat(literalType.value).reverse());
428}
429/**
430 * Extract the target type alias declaration or interface declaration
431 * of a type reference.
432 *
433 * @param typeReference AST type reference node
434 */
435function getTargetDeclarationFromTypeReference(typeReference) {
436 // TODO: check logic
437 const symbol = typeReference.getTypeName().getSymbolOrThrow();
438 // if the symbol is an alias, it means it the reference is declared from an import
439 const targetSymbol = symbol.isAlias()
440 ? symbol.getAliasedSymbolOrThrow()
441 : symbol;
442 const declarations = targetSymbol.getDeclarations();
443 const location = typeReference.getSourceFile().getFilePath();
444 const line = typeReference.getStartLineNumber();
445 const typeName = symbol.getName();
446 if (typeName === "Map") {
447 return util_1.err(new errors_1.TypeNotAllowedError("Map type is not supported", {
448 file: location,
449 position: typeReference.getPos()
450 }));
451 }
452 if (declarations.length !== 1) {
453 // String interface must not be redefined and must be imported from the Spot native types
454 const errorMsg = `${location}#${line}: expected exactly one declaration for ${typeName}`;
455 if (typeName === "String") {
456 throw new Error(`${errorMsg}\nDid you forget to import String? => import { String } from "@airtasker/spot"`);
457 }
458 else {
459 throw new Error(errorMsg);
460 }
461 // TODO: same for other internal custom types e.g. Number
462 }
463 const targetDeclaration = declarations[0];
464 // Enums are not supported:
465 // enum SomeEnum { A, B, C }
466 if (ts_morph_1.TypeGuards.isEnumDeclaration(targetDeclaration)) {
467 return util_1.err(new errors_1.TypeNotAllowedError("Enums are not supported", {
468 file: targetDeclaration.getSourceFile().getFilePath(),
469 position: targetDeclaration.getPos()
470 }));
471 }
472 // References to enum constants (e.g SomeEnum.A) are not supported either.
473 if (ts_morph_1.TypeGuards.isEnumMember(targetDeclaration)) {
474 return util_1.err(new errors_1.TypeNotAllowedError("Enums are not supported", {
475 file: targetDeclaration.getSourceFile().getFilePath(),
476 position: targetDeclaration.getPos()
477 }));
478 }
479 if (ts_morph_1.TypeGuards.isInterfaceDeclaration(targetDeclaration) ||
480 ts_morph_1.TypeGuards.isTypeAliasDeclaration(targetDeclaration)) {
481 return util_1.ok(targetDeclaration);
482 }
483 throw new Error("expected a type alias or interface declaration");
484}