UNPKG

49.3 kBPlain TextView Raw
1import * as glob from "glob";
2import * as stringify from "json-stable-stringify";
3import * as path from "path";
4import { createHash } from "crypto";
5import * as ts from "typescript";
6export { Program, CompilerOptions, Symbol } from "typescript";
7
8
9const vm = require("vm");
10
11const REGEX_FILE_NAME_OR_SPACE = /(\bimport\(".*?"\)|".*?")\.| /g;
12const REGEX_TSCONFIG_NAME = /^.*\.json$/;
13const REGEX_TJS_JSDOC = /^-([\w]+)\s+(\S|\S[\s\S]*\S)\s*$/g;
14const NUMERIC_INDEX_PATTERN = "^[0-9]+$";
15
16export function getDefaultArgs(): Args {
17 return {
18 ref: true,
19 aliasRef: false,
20 topRef: false,
21 titles: false,
22 defaultProps: false,
23 noExtraProps: false,
24 propOrder: false,
25 typeOfKeyword: false,
26 required: false,
27 strictNullChecks: false,
28 ignoreErrors: false,
29 out: "",
30 validationKeywords: [],
31 include: [],
32 excludePrivate: false,
33 uniqueNames: false,
34 rejectDateType: false,
35 id: "",
36 };
37}
38
39export type ValidationKeywords = {
40 [prop: string]: boolean
41};
42
43export type Args = {
44 ref: boolean;
45 aliasRef: boolean;
46 topRef: boolean;
47 titles: boolean;
48 defaultProps: boolean;
49 noExtraProps: boolean;
50 propOrder: boolean;
51 typeOfKeyword: boolean;
52 required: boolean;
53 strictNullChecks: boolean;
54 ignoreErrors: boolean;
55 out: string;
56 validationKeywords: string[];
57 include: string[];
58 excludePrivate: boolean;
59 uniqueNames: boolean;
60 rejectDateType: boolean;
61 id: string;
62};
63
64export type PartialArgs = Partial<Args>;
65
66export type PrimitiveType = number | boolean | string | null;
67
68export type Definition = {
69 $ref?: string,
70 $schema?: string,
71 $id?: string,
72 description?: string,
73 examples?: any[],
74 allOf?: Definition[],
75 oneOf?: Definition[],
76 anyOf?: Definition[],
77 title?: string,
78 type?: string | string[],
79 definitions?: {[key: string]: any},
80 format?: string,
81 items?: Definition | Definition[],
82 minItems?: number,
83 additionalItems?: {
84 anyOf: Definition[]
85 } | Definition,
86 enum?: PrimitiveType[] | Definition[],
87 default?: PrimitiveType | Object,
88 additionalProperties?: Definition | boolean,
89 required?: string[],
90 propertyOrder?: string[],
91 properties?: {[key: string]: any},
92 defaultProperties?: string[],
93 patternProperties?: {[pattern: string]: Definition},
94 typeof?: "function"
95};
96
97export type SymbolRef = {
98 name: string;
99 typeName: string;
100 fullyQualifiedName: string;
101 symbol: ts.Symbol;
102};
103
104function extend(target: any, ..._: any[]) {
105 if (target == null) { // TypeError if undefined or null
106 throw new TypeError("Cannot convert undefined or null to object");
107 }
108
109 const to = Object(target);
110
111 for (var index = 1; index < arguments.length; index++) {
112 const nextSource = arguments[index];
113
114 if (nextSource != null) { // Skip over if undefined or null
115 for (const nextKey in nextSource) {
116 // Avoid bugs when hasOwnProperty is shadowed
117 if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
118 to[nextKey] = nextSource[nextKey];
119 }
120 }
121 }
122 }
123 return to;
124}
125
126function unique(arr: string[]): string[] {
127 const temp = {};
128 for (const e of arr) {
129 temp[e] = true;
130 }
131 const r: string[] = [];
132 for (const k in temp) {
133 // Avoid bugs when hasOwnProperty is shadowed
134 if (Object.prototype.hasOwnProperty.call(temp, k)) {
135 r.push(k);
136 }
137 }
138 return r;
139}
140
141/**
142 * Try to parse a value and returns the string if it fails.
143 */
144function parseValue(value: string) {
145 try {
146 return JSON.parse(value);
147 } catch (error) {
148 return value;
149 }
150}
151
152function extractLiteralValue(typ: ts.Type): PrimitiveType | undefined {
153 let str = (<ts.LiteralType>typ).value;
154 if (str === undefined) {
155 str = (typ as any).text;
156 }
157 if (typ.flags & ts.TypeFlags.StringLiteral) {
158 return str as string;
159 } else if (typ.flags & ts.TypeFlags.BooleanLiteral) {
160 return (typ as any).intrinsicName === "true";
161 } else if (typ.flags & ts.TypeFlags.EnumLiteral) {
162 // or .text for old TS
163 const num = parseFloat(str as string);
164 return isNaN(num) ? str as string : num;
165 } else if (typ.flags & ts.TypeFlags.NumberLiteral) {
166 return parseFloat(str as string);
167 }
168 return undefined;
169}
170
171/**
172 * Checks whether a type is a tuple type.
173 */
174function resolveTupleType(propertyType: ts.Type): ts.TupleTypeNode|null {
175 if (!propertyType.getSymbol() && (propertyType.getFlags() & ts.TypeFlags.Object && (<ts.ObjectType>propertyType).objectFlags & ts.ObjectFlags.Reference)) {
176 return (propertyType as ts.TypeReference).target as any;
177 }
178 if (!(propertyType.getFlags() & ts.TypeFlags.Object && (<ts.ObjectType>propertyType).objectFlags & ts.ObjectFlags.Tuple)) {
179 return null;
180 }
181 return propertyType as any;
182}
183
184const simpleTypesAllowedProperties = {
185 type: true,
186 description: true
187};
188
189function addSimpleType(def: Definition, type: string) {
190 for (const k in def) {
191 if (!simpleTypesAllowedProperties[k]) {
192 return false;
193 }
194 }
195
196 if (!def.type) {
197 def.type = type;
198 } else if (typeof def.type !== "string") {
199 if (!(<Object[]>def.type).every((val) => { return typeof val === "string"; })) {
200 return false;
201 }
202
203 if (def.type.indexOf("null") === -1) {
204 def.type.push("null");
205 }
206 } else {
207 if (typeof def.type !== "string") {
208 return false;
209 }
210
211 if (def.type !== "null") {
212 def.type = [ def.type, "null" ];
213 }
214 }
215 return true;
216}
217
218function makeNullable(def: Definition) {
219 if (!addSimpleType(def, "null")) {
220 const union = def.oneOf || def.anyOf;
221 if (union) {
222 union.push({ type: "null" });
223 } else {
224 const subdef = {};
225 for (var k in def) {
226 if (def.hasOwnProperty(k)) {
227 subdef[k] = def[k];
228 delete def[k];
229 }
230 }
231 def.anyOf = [ subdef, { type: "null" } ];
232 }
233 }
234 return def;
235}
236
237/**
238 * JSDoc keywords that should be used to annotate the JSON schema.
239 *
240 * Many of these validation keywords are defined here: http://json-schema.org/latest/json-schema-validation.html
241 */
242const validationKeywords = {
243 multipleOf: true, // 6.1.
244 maximum: true, // 6.2.
245 exclusiveMaximum: true, // 6.3.
246 minimum: true, // 6.4.
247 exclusiveMinimum: true, // 6.5.
248 maxLength: true, // 6.6.
249 minLength: true, // 6.7.
250 pattern: true, // 6.8.
251 // items: true, // 6.9.
252 // additionalItems: true, // 6.10.
253 maxItems: true, // 6.11.
254 minItems: true, // 6.12.
255 uniqueItems: true, // 6.13.
256 // contains: true, // 6.14.
257 maxProperties: true, // 6.15.
258 minProperties: true, // 6.16.
259 // required: true, // 6.17. This is not required. It is auto-generated.
260 // properties: true, // 6.18. This is not required. It is auto-generated.
261 // patternProperties: true, // 6.19.
262 additionalProperties: true, // 6.20.
263 // dependencies: true, // 6.21.
264 // propertyNames: true, // 6.22.
265 enum: true, // 6.23.
266 // const: true, // 6.24.
267 type: true, // 6.25.
268 // allOf: true, // 6.26.
269 // anyOf: true, // 6.27.
270 // oneOf: true, // 6.28.
271 // not: true, // 6.29.
272 examples: true, // Draft 6 (draft-handrews-json-schema-validation-01)
273
274 ignore: true,
275 description: true,
276 format: true,
277 default: true,
278 $ref: true,
279 id: true
280};
281
282export class JsonSchemaGenerator {
283 private tc: ts.TypeChecker;
284
285 /**
286 * Holds all symbols within a custom SymbolRef object, containing useful
287 * information.
288 */
289 private symbols: SymbolRef[];
290 /**
291 * All types for declarations of classes, interfaces, enums, and type aliases
292 * defined in all TS files.
293 */
294 private allSymbols: { [name: string]: ts.Type };
295 /**
296 * All symbols for declarations of classes, interfaces, enums, and type aliases
297 * defined in non-default-lib TS files.
298 */
299 private userSymbols: { [name: string]: ts.Symbol };
300 /**
301 * Maps from the names of base types to the names of the types that inherit from
302 * them.
303 */
304 private inheritingTypes: { [baseName: string]: string[] };
305
306 private reffedDefinitions: { [key: string]: Definition } = {};
307
308 /**
309 * This is a set of all the user-defined validation keywords.
310 */
311 private userValidationKeywords: ValidationKeywords;
312
313 /**
314 * Types are assigned names which are looked up by their IDs. This is the
315 * map from type IDs to type names.
316 */
317 private typeNamesById: { [id: number]: string } = {};
318 /**
319 * Whenever a type is assigned its name, its entry in this dictionary is set,
320 * so that we don't give the same name to two separate types.
321 */
322 private typeIdsByName: { [name: string]: number } = {};
323
324 constructor(
325 symbols: SymbolRef[],
326 allSymbols: { [name: string]: ts.Type },
327 userSymbols: { [name: string]: ts.Symbol },
328 inheritingTypes: { [baseName: string]: string[] },
329 tc: ts.TypeChecker,
330 private args = getDefaultArgs(),
331 ) {
332 this.symbols = symbols;
333 this.allSymbols = allSymbols;
334 this.userSymbols = userSymbols;
335 this.inheritingTypes = inheritingTypes;
336 this.tc = tc;
337 this.userValidationKeywords = args.validationKeywords.reduce(
338 (acc, word) => ({ ...acc, [word]: true }),
339 {}
340 );
341 }
342
343 public get ReffedDefinitions(): { [key: string]: Definition } {
344 return this.reffedDefinitions;
345 }
346
347 /**
348 * Parse the comments of a symbol into the definition and other annotations.
349 */
350 private parseCommentsIntoDefinition(symbol: ts.Symbol, definition: {description?: string}, otherAnnotations: {}): void {
351 if (!symbol) {
352 return;
353 }
354
355 // the comments for a symbol
356 const comments = symbol.getDocumentationComment(this.tc);
357
358 if (comments.length) {
359 definition.description = comments.map(comment => comment.kind === "lineBreak" ? comment.text : comment.text.trim().replace(/\r\n/g, "\n")).join("");
360 }
361
362 // jsdocs are separate from comments
363 const jsdocs = symbol.getJsDocTags();
364 jsdocs.forEach(doc => {
365 // if we have @TJS-... annotations, we have to parse them
366 const [name, text] = (doc.name === "TJS" ? new RegExp(REGEX_TJS_JSDOC).exec(doc.text!)!.slice(1,3) : [doc.name, doc.text]) as string[];
367 if (validationKeywords[name] || this.userValidationKeywords[name]) {
368 definition[name] = text === undefined ? "" : parseValue(text);
369 } else {
370 // special annotations
371 otherAnnotations[doc.name] = true;
372 }
373 });
374 }
375
376 private getDefinitionForRootType(propertyType: ts.Type, reffedType: ts.Symbol, definition: Definition) {
377 const tupleType = resolveTupleType(propertyType);
378
379 if (tupleType) { // tuple
380 const elemTypes: ts.NodeArray<ts.TypeNode> = tupleType.elementTypes || (propertyType as any).typeArguments;
381 const fixedTypes = elemTypes.map(elType => this.getTypeDefinition(elType as any));
382 definition.type = "array";
383 definition.items = fixedTypes;
384 const targetTupleType = (propertyType as ts.TupleTypeReference).target;
385 definition.minItems = targetTupleType.minLength;
386 if (targetTupleType.hasRestElement) {
387 definition.additionalItems = fixedTypes[fixedTypes.length - 1];
388 fixedTypes.splice(fixedTypes.length - 1, 1);
389 } else {
390 definition.additionalItems = {
391 anyOf: fixedTypes
392 };
393 }
394 } else {
395 const propertyTypeString = this.tc.typeToString(propertyType, undefined, ts.TypeFormatFlags.UseFullyQualifiedType);
396 const flags = propertyType.flags;
397 const arrayType = this.tc.getIndexTypeOfType(propertyType, ts.IndexKind.Number);
398
399 if (flags & ts.TypeFlags.String) {
400 definition.type = "string";
401 } else if (flags & ts.TypeFlags.Number) {
402 const isInteger = (definition.type === "integer" || (reffedType && reffedType.getName() === "integer"));
403 definition.type = isInteger ? "integer" : "number";
404 } else if (flags & ts.TypeFlags.Boolean) {
405 definition.type = "boolean";
406 } else if (flags & ts.TypeFlags.Null) {
407 definition.type = "null";
408 } else if (flags & ts.TypeFlags.Undefined) {
409 definition.type = "undefined";
410 } else if ((flags & ts.TypeFlags.Any) || (flags & ts.TypeFlags.Unknown)) {
411 // no type restriction, so that anything will match
412 } else if (propertyTypeString === "Date" && !this.args.rejectDateType) {
413 definition.type = "string";
414 definition.format = "date-time";
415 } else if (propertyTypeString === "object") {
416 definition.type = "object";
417 definition.properties = {};
418 definition.additionalProperties = true;
419 } else {
420 const value = extractLiteralValue(propertyType);
421 if (value !== undefined) {
422 definition.type = typeof value;
423 definition.enum = [ value ];
424 } else if (arrayType !== undefined) {
425 if ((propertyType.flags & ts.TypeFlags.Object) &&
426 ((propertyType as ts.ObjectType).objectFlags & (ts.ObjectFlags.Anonymous | ts.ObjectFlags.Interface | ts.ObjectFlags.Mapped))) {
427 definition.type = "object";
428 definition.additionalProperties = false;
429 definition.patternProperties = {
430 [NUMERIC_INDEX_PATTERN]: this.getTypeDefinition(arrayType)
431 };
432 } else {
433 definition.type = "array";
434 definition.items = this.getTypeDefinition(arrayType);
435 }
436 } else {
437 // Report that type could not be processed
438 const error = new TypeError("Unsupported type: " + propertyTypeString);
439 (error as any).type = propertyType;
440 throw error;
441 // definition = this.getTypeDefinition(propertyType, tc);
442 }
443 }
444 }
445
446 return definition;
447 }
448
449 private getReferencedTypeSymbol(prop: ts.Symbol): ts.Symbol|undefined {
450 const decl = prop.getDeclarations();
451 if (decl && decl.length) {
452 const type = (<ts.TypeReferenceNode> (<any> decl[0]).type);
453 if (type && (type.kind & ts.SyntaxKind.TypeReference) && type.typeName) {
454 const symbol = this.tc.getSymbolAtLocation(type.typeName);
455 if (symbol && (symbol.flags & ts.SymbolFlags.Alias)) {
456 return this.tc.getAliasedSymbol(symbol);
457 }
458 return symbol;
459 }
460 }
461 return undefined;
462 }
463
464 private getDefinitionForProperty(prop: ts.Symbol, node: ts.Node) {
465 if (prop.flags & ts.SymbolFlags.Method) {
466 return null;
467 }
468 const propertyName = prop.getName();
469 const propertyType = this.tc.getTypeOfSymbolAtLocation(prop, node);
470
471 const reffedType = this.getReferencedTypeSymbol(prop);
472
473 const definition = this.getTypeDefinition(propertyType, undefined, undefined, prop, reffedType);
474
475 if (this.args.titles) {
476 definition.title = propertyName;
477 }
478
479 if (definition.hasOwnProperty("ignore")) {
480 return null;
481 }
482
483 // try to get default value
484 const valDecl = prop.valueDeclaration as ts.VariableDeclaration;
485 if (valDecl && valDecl.initializer) {
486 let initial = valDecl.initializer;
487
488 while (ts.isTypeAssertion(initial)) {
489 initial = initial.expression;
490 }
491
492 if ((<any>initial).expression) { // node
493 console.warn("initializer is expression for property " + propertyName);
494 } else if ((<any>initial).kind && (<any>initial).kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral) {
495 definition.default = initial.getText();
496 } else {
497 try {
498 const sandbox = { sandboxvar: null as any };
499 vm.runInNewContext("sandboxvar=" + initial.getText(), sandbox);
500
501 const val = sandbox.sandboxvar;
502 if (val === null || typeof val === "string" || typeof val === "number" || typeof val === "boolean" || Object.prototype.toString.call(val) === "[object Array]") {
503 definition.default = val;
504 } else if (val) {
505 console.warn("unknown initializer for property " + propertyName + ": " + val);
506 }
507 } catch (e) {
508 console.warn("exception evaluating initializer for property " + propertyName);
509 }
510 }
511 }
512
513 return definition;
514 }
515
516 private getEnumDefinition(clazzType: ts.Type, definition: Definition): Definition {
517 const node = clazzType.getSymbol()!.getDeclarations()![0];
518 const fullName = this.tc.typeToString(clazzType, undefined, ts.TypeFormatFlags.UseFullyQualifiedType);
519 const members: ts.NodeArray<ts.EnumMember> = node.kind === ts.SyntaxKind.EnumDeclaration ?
520 (node as ts.EnumDeclaration).members :
521 ts.createNodeArray([node as ts.EnumMember]);
522 var enumValues: (number|boolean|string|null)[] = [];
523 const enumTypes: string[] = [];
524
525 const addType = (type: string) => {
526 if (enumTypes.indexOf(type) === -1) {
527 enumTypes.push(type);
528 }
529 };
530
531 members.forEach(member => {
532 const caseLabel = (<ts.Identifier>member.name).text;
533 const constantValue = this.tc.getConstantValue(member);
534 if (constantValue !== undefined) {
535 enumValues.push(constantValue);
536 addType(typeof constantValue);
537 } else {
538 // try to extract the enums value; it will probably by a cast expression
539 const initial: ts.Expression|undefined = member.initializer;
540 if (initial) {
541 if ((<any>initial).expression) { // node
542 const exp = (<any>initial).expression;
543 const text = (<any>exp).text;
544 // if it is an expression with a text literal, chances are it is the enum convension:
545 // CASELABEL = 'literal' as any
546 if (text) {
547 enumValues.push(text);
548 addType("string");
549 } else if (exp.kind === ts.SyntaxKind.TrueKeyword || exp.kind === ts.SyntaxKind.FalseKeyword) {
550 enumValues.push((exp.kind === ts.SyntaxKind.TrueKeyword));
551 addType("boolean");
552 } else {
553 console.warn("initializer is expression for enum: " + fullName + "." + caseLabel);
554 }
555 } else if (initial.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral) {
556 enumValues.push(initial.getText());
557 addType("string");
558 } else if (initial.kind === ts.SyntaxKind.NullKeyword) {
559 enumValues.push(null);
560 addType("null");
561 }
562 }
563 }
564 });
565
566 if (enumTypes.length) {
567 definition.type = (enumTypes.length === 1) ? enumTypes[0] : enumTypes;
568 }
569
570 if (enumValues.length > 0) {
571 definition.enum = enumValues.sort();
572 }
573
574 return definition;
575 }
576
577 private getUnionDefinition(unionType: ts.UnionType, prop: ts.Symbol, unionModifier: string, definition: Definition) {
578 const enumValues: PrimitiveType[] = [];
579 const simpleTypes: string[] = [];
580 const schemas: Definition[] = [];
581
582 const pushSimpleType = (type: string) => {
583 if (simpleTypes.indexOf(type) === -1) {
584 simpleTypes.push(type);
585 }
586 };
587
588 const pushEnumValue = (val: PrimitiveType) => {
589 if (enumValues.indexOf(val) === -1) {
590 enumValues.push(val);
591 }
592 };
593
594 for (const valueType of unionType.types) {
595 const value = extractLiteralValue(valueType);
596 if (value !== undefined) {
597 pushEnumValue(value);
598 } else {
599 const def = this.getTypeDefinition(valueType);
600 if (def.type === "undefined") {
601 if (prop) {
602 (<any>prop).mayBeUndefined = true;
603 }
604 } else {
605 const keys = Object.keys(def);
606 if (keys.length === 1 && keys[0] === "type") {
607 if (typeof def.type !== "string") {
608 console.error("Expected only a simple type.");
609 } else {
610 pushSimpleType(def.type);
611 }
612 } else {
613 schemas.push(def);
614 }
615 }
616 }
617 }
618
619 if (enumValues.length > 0) {
620 // If the values are true and false, just add "boolean" as simple type
621 const isOnlyBooleans = enumValues.length === 2 &&
622 typeof enumValues[0] === "boolean" &&
623 typeof enumValues[1] === "boolean" &&
624 enumValues[0] !== enumValues[1];
625
626 if (isOnlyBooleans) {
627 pushSimpleType("boolean");
628 } else {
629 const enumSchema: Definition = { enum: enumValues.sort() };
630
631 // If all values are of the same primitive type, add a "type" field to the schema
632 if (enumValues.every((x) => { return typeof x === "string"; })) {
633 enumSchema.type = "string";
634 } else if (enumValues.every((x) => { return typeof x === "number"; })) {
635 enumSchema.type = "number";
636 } else if (enumValues.every((x) => { return typeof x === "boolean"; })) {
637 enumSchema.type = "boolean";
638 }
639
640 schemas.push(enumSchema);
641 }
642 }
643
644 if (simpleTypes.length > 0) {
645 schemas.push({ type: simpleTypes.length === 1 ? simpleTypes[0] : simpleTypes });
646 }
647
648 if (schemas.length === 1) {
649 for (const k in schemas[0]) {
650 if (schemas[0].hasOwnProperty(k)) {
651 definition[k] = schemas[0][k];
652 }
653 }
654 } else {
655 definition[unionModifier] = schemas;
656 }
657 return definition;
658 }
659
660 private getIntersectionDefinition(intersectionType: ts.IntersectionType, definition: Definition) {
661 const simpleTypes: string[] = [];
662 const schemas: Definition[] = [];
663
664 const pushSimpleType = (type: string) => {
665 if (simpleTypes.indexOf(type) === -1) {
666 simpleTypes.push(type);
667 }
668 };
669
670 for (const intersectionMember of intersectionType.types) {
671 const def = this.getTypeDefinition(intersectionMember);
672 if (def.type === "undefined") {
673 console.error("Undefined in intersection makes no sense.");
674 } else {
675 const keys = Object.keys(def);
676 if (keys.length === 1 && keys[0] === "type") {
677 if (typeof def.type !== "string") {
678 console.error("Expected only a simple type.");
679 } else {
680 pushSimpleType(def.type);
681 }
682 } else {
683 schemas.push(def);
684 }
685 }
686 }
687
688 if (simpleTypes.length > 0) {
689 schemas.push({ type: simpleTypes.length === 1 ? simpleTypes[0] : simpleTypes });
690 }
691
692 if (schemas.length === 1) {
693 for (const k in schemas[0]) {
694 if (schemas[0].hasOwnProperty(k)) {
695 definition[k] = schemas[0][k];
696 }
697 }
698 } else {
699 definition.allOf = schemas;
700 }
701 return definition;
702 }
703
704
705 private getClassDefinition(clazzType: ts.Type, definition: Definition): Definition {
706 const node = clazzType.getSymbol()!.getDeclarations()![0];
707 if (this.args.typeOfKeyword && node.kind === ts.SyntaxKind.FunctionType) {
708 definition.typeof = "function";
709 return definition;
710 }
711
712 const clazz = <ts.ClassDeclaration>node;
713 const props = this.tc.getPropertiesOfType(clazzType).filter(prop => {
714 // filter never
715 const propertyType = this.tc.getTypeOfSymbolAtLocation(prop, node);
716 if (ts.TypeFlags.Never === propertyType.getFlags()) {
717 return false;
718 }
719 if (!this.args.excludePrivate) {
720 return true;
721 }
722
723 const decls = prop.declarations;
724 return !(decls && decls.filter(decl => {
725 const mods = decl.modifiers;
726 return mods && mods.filter(mod => mod.kind === ts.SyntaxKind.PrivateKeyword).length > 0;
727 }).length > 0);
728 });
729 const fullName = this.tc.typeToString(clazzType, undefined, ts.TypeFormatFlags.UseFullyQualifiedType);
730
731 const modifierFlags = ts.getCombinedModifierFlags(node);
732
733 if (modifierFlags & ts.ModifierFlags.Abstract && this.inheritingTypes[fullName]) {
734 const oneOf = this.inheritingTypes[fullName].map((typename) => {
735 return this.getTypeDefinition(this.allSymbols[typename]);
736 });
737
738 definition.oneOf = oneOf;
739 } else {
740 if (clazz.members) {
741 const indexSignatures = clazz.members == null ? [] : clazz.members.filter(x => x.kind === ts.SyntaxKind.IndexSignature);
742 if (indexSignatures.length === 1) {
743 // for case "array-types"
744 const indexSignature = indexSignatures[0] as ts.IndexSignatureDeclaration;
745 if (indexSignature.parameters.length !== 1) {
746 throw new Error("Not supported: IndexSignatureDeclaration parameters.length != 1");
747 }
748 const indexSymbol: ts.Symbol = (<any>indexSignature.parameters[0]).symbol;
749 const indexType = this.tc.getTypeOfSymbolAtLocation(indexSymbol, node);
750 const isStringIndexed = (indexType.flags === ts.TypeFlags.String);
751 if (indexType.flags !== ts.TypeFlags.Number && !isStringIndexed) {
752 throw new Error("Not supported: IndexSignatureDeclaration with index symbol other than a number or a string");
753 }
754
755 const typ = this.tc.getTypeAtLocation(indexSignature.type!);
756 const def = this.getTypeDefinition(typ, undefined, "anyOf");
757
758 if (isStringIndexed) {
759 definition.type = "object";
760 definition.additionalProperties = def;
761 } else {
762 definition.type = "array";
763 definition.items = def;
764 }
765 }
766 }
767
768 const propertyDefinitions = props.reduce((all, prop) => {
769 const propertyName = prop.getName();
770 const propDef = this.getDefinitionForProperty(prop, node);
771 if (propDef != null) {
772 all[propertyName] = propDef;
773 }
774 return all;
775 }, {});
776
777 if (definition.type === undefined) {
778 definition.type = "object";
779 }
780
781 if (definition.type === "object" && Object.keys(propertyDefinitions).length > 0) {
782 definition.properties = propertyDefinitions;
783 }
784
785 if (this.args.defaultProps) {
786 definition.defaultProperties = [];
787 }
788 if (this.args.noExtraProps && definition.additionalProperties === undefined) {
789 definition.additionalProperties = false;
790 }
791 if (this.args.propOrder) {
792 // propertyOrder is non-standard, but useful:
793 // https://github.com/json-schema/json-schema/issues/87
794 const propertyOrder = props.reduce((order: string[], prop: ts.Symbol) => {
795 order.push(prop.getName());
796 return order;
797 }, []);
798
799 definition.propertyOrder = propertyOrder;
800 }
801 if (this.args.required) {
802 const requiredProps = props.reduce((required: string[], prop: ts.Symbol) => {
803 const def = {};
804 this.parseCommentsIntoDefinition(prop, def, {});
805 if (!(prop.flags & ts.SymbolFlags.Optional) && !(prop.flags & ts.SymbolFlags.Method) && !(<any>prop).mayBeUndefined && !def.hasOwnProperty("ignore")) {
806 required.push(prop.getName());
807 }
808 return required;
809 }, []);
810
811 if (requiredProps.length > 0) {
812 definition.required = unique(requiredProps).sort();
813 }
814 }
815 }
816 return definition;
817 }
818
819 /**
820 * Gets/generates a globally unique type name for the given type
821 */
822 private getTypeName(typ: ts.Type) {
823 const id = (typ as any).id as number;
824 if (this.typeNamesById[id]) { // Name already assigned?
825 return this.typeNamesById[id];
826 }
827 return this.makeTypeNameUnique(
828 typ,
829 this.tc.typeToString(typ, undefined, ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.UseFullyQualifiedType).replace(REGEX_FILE_NAME_OR_SPACE, "")
830 );
831 }
832
833 private makeTypeNameUnique(typ: ts.Type, baseName: string) {
834 const id = (typ as any).id as number;
835
836 let name = baseName;
837 // If a type with same name exists
838 // Try appending "_1", "_2", etc.
839 for (let i = 1; this.typeIdsByName[name] !== undefined && this.typeIdsByName[name] !== id; ++i) {
840 name = baseName + "_" + i;
841 }
842
843 this.typeNamesById[id] = name;
844 this.typeIdsByName[name] = id;
845 return name;
846 }
847
848 private getTypeDefinition(typ: ts.Type, asRef = this.args.ref, unionModifier: string = "anyOf", prop?: ts.Symbol, reffedType?: ts.Symbol, pairedSymbol?: ts.Symbol): Definition {
849 const definition: Definition = {}; // real definition
850
851 // Ignore any number of Readonly and Mutable type wrappings, since they only add and remove readonly modifiers on fields and JSON Schema is not concerned with mutability
852 while (typ.aliasSymbol && (typ.aliasSymbol.escapedName === "Readonly" || typ.aliasSymbol.escapedName === "Mutable") && typ.aliasTypeArguments && typ.aliasTypeArguments[0]) {
853 typ = typ.aliasTypeArguments[0];
854 reffedType = undefined;
855 }
856
857 if (this.args.typeOfKeyword && (typ.flags & ts.TypeFlags.Object) && ((<ts.ObjectType>typ).objectFlags & ts.ObjectFlags.Anonymous)) {
858 definition.typeof = "function";
859 return definition;
860 }
861
862 let returnedDefinition = definition; // returned definition, may be a $ref
863
864 const symbol = typ.getSymbol();
865 // FIXME: We can't just compare the name of the symbol - it ignores the namespace
866 const isRawType = (!symbol || this.tc.getFullyQualifiedName(symbol) === "Date" || symbol.name === "integer" || this.tc.getIndexInfoOfType(typ, ts.IndexKind.Number) !== undefined);
867
868 // special case: an union where all child are string literals -> make an enum instead
869 let isStringEnum = false;
870 if (typ.flags & ts.TypeFlags.Union) {
871 const unionType = <ts.UnionType>typ;
872 isStringEnum = (unionType.types.every(propType => {
873 return (propType.getFlags() & ts.TypeFlags.StringLiteral) !== 0;
874 }));
875 }
876
877 // aliased types must be handled slightly different
878 const asTypeAliasRef = asRef && reffedType && (this.args.aliasRef || isStringEnum);
879 if (!asTypeAliasRef) {
880 if (isRawType || typ.getFlags() & ts.TypeFlags.Object && (<ts.ObjectType>typ).objectFlags & ts.ObjectFlags.Anonymous) {
881 asRef = false; // raw types and inline types cannot be reffed,
882 // unless we are handling a type alias
883 }
884 }
885
886 let fullTypeName = "";
887 if (asTypeAliasRef) {
888 fullTypeName = this.makeTypeNameUnique(
889 typ,
890 this.tc.getFullyQualifiedName(
891 reffedType!.getFlags() & ts.SymbolFlags.Alias ?
892 this.tc.getAliasedSymbol(reffedType!) :
893 reffedType!
894 ).replace(REGEX_FILE_NAME_OR_SPACE, "")
895 );
896 } else if (asRef) {
897 fullTypeName = this.getTypeName(typ);
898 }
899
900 if (asRef) {
901 // We don't return the full definition, but we put it into
902 // reffedDefinitions below.
903 returnedDefinition = {
904 $ref: `${this.args.id}#/definitions/` + fullTypeName
905 };
906 }
907
908 // Parse comments
909 const otherAnnotations = {};
910 this.parseCommentsIntoDefinition(reffedType!, definition, otherAnnotations); // handle comments in the type alias declaration
911 this.parseCommentsIntoDefinition(symbol!, definition, otherAnnotations);
912 if (prop) {
913 this.parseCommentsIntoDefinition(prop, returnedDefinition, otherAnnotations);
914 }
915
916 // Create the actual definition only if is an inline definition, or
917 // if it will be a $ref and it is not yet created
918 if (!asRef || !this.reffedDefinitions[fullTypeName]) {
919 if (asRef) { // must be here to prevent recursivity problems
920 let reffedDefinition: Definition;
921 if (asTypeAliasRef && reffedType && typ.symbol !== reffedType && symbol) {
922 reffedDefinition = this.getTypeDefinition(typ, true, undefined, symbol, symbol);
923 } else {
924 reffedDefinition = definition;
925 }
926 this.reffedDefinitions[fullTypeName] = reffedDefinition;
927 if (this.args.titles && fullTypeName) {
928 definition.title = fullTypeName;
929 }
930 }
931 const node = symbol && symbol.getDeclarations() !== undefined ? symbol.getDeclarations()![0] : null;
932
933 if (definition.type === undefined) { // if users override the type, do not try to infer it
934 if (typ.flags & ts.TypeFlags.Union) {
935 this.getUnionDefinition(typ as ts.UnionType, prop!, unionModifier, definition);
936 } else if (typ.flags & ts.TypeFlags.Intersection) {
937 if (this.args.noExtraProps) {
938 // extend object instead of using allOf because allOf does not work well with additional properties. See #107
939 if (this.args.noExtraProps) {
940 definition.additionalProperties = false;
941 }
942
943 const types = (<ts.IntersectionType> typ).types;
944 for (const member of types) {
945 const other = this.getTypeDefinition(member, false);
946 definition.type = other.type; // should always be object
947 definition.properties = extend(definition.properties || {}, other.properties);
948 if (Object.keys(other.default || {}).length > 0) {
949 definition.default = extend(definition.default || {}, other.default);
950 }
951 if (other.required) {
952 definition.required = unique((definition.required || []).concat(other.required)).sort();
953 }
954 }
955 } else {
956 this.getIntersectionDefinition(typ as ts.IntersectionType, definition);
957 }
958 } else if (isRawType) {
959 if (pairedSymbol) {
960 this.parseCommentsIntoDefinition(pairedSymbol, definition, {});
961 }
962 this.getDefinitionForRootType(typ, reffedType!, definition);
963 } else if (node && (node.kind === ts.SyntaxKind.EnumDeclaration || node.kind === ts.SyntaxKind.EnumMember)) {
964 this.getEnumDefinition(typ, definition);
965 } else if (symbol && symbol.flags & ts.SymbolFlags.TypeLiteral && symbol.members!.size === 0 && !(node && (node.kind === ts.SyntaxKind.MappedType))) {
966 // {} is TypeLiteral with no members. Need special case because it doesn't have declarations.
967 definition.type = "object";
968 definition.properties = {};
969 } else {
970 this.getClassDefinition(typ, definition);
971 }
972 }
973 }
974
975 if (otherAnnotations["nullable"]) {
976 makeNullable(returnedDefinition);
977 }
978
979 return returnedDefinition;
980 }
981
982 public setSchemaOverride(symbolName: string, schema: Definition) {
983 this.reffedDefinitions[symbolName] = schema;
984 }
985
986 public getSchemaForSymbol(symbolName: string, includeReffedDefinitions: boolean = true): Definition {
987 if(!this.allSymbols[symbolName]) {
988 throw new Error(`type ${symbolName} not found`);
989 }
990 const def = this.getTypeDefinition(this.allSymbols[symbolName], this.args.topRef, undefined, undefined, undefined, this.userSymbols[symbolName] || undefined);
991
992 if (this.args.ref && includeReffedDefinitions && Object.keys(this.reffedDefinitions).length > 0) {
993 def.definitions = this.reffedDefinitions;
994 }
995 def["$schema"] = "http://json-schema.org/draft-07/schema#";
996 const id = this.args.id;
997 if(id) {
998 def["$id"] = this.args.id;
999 }
1000 return def;
1001 }
1002
1003 public getSchemaForSymbols(symbolNames: string[], includeReffedDefinitions: boolean = true): Definition {
1004 const root = {
1005 $schema: "http://json-schema.org/draft-07/schema#",
1006 definitions: {}
1007 };
1008 const id = this.args.id;
1009
1010 if(id) {
1011 root["$id"] = id;
1012 }
1013
1014 for (const symbolName of symbolNames) {
1015 root.definitions[symbolName] = this.getTypeDefinition(this.allSymbols[symbolName], this.args.topRef, undefined, undefined, undefined, this.userSymbols[symbolName]);
1016 }
1017 if (this.args.ref && includeReffedDefinitions && Object.keys(this.reffedDefinitions).length > 0) {
1018 root.definitions = {...root.definitions, ... this.reffedDefinitions};
1019 }
1020 return root;
1021 }
1022
1023 public getSymbols(name?: string): SymbolRef[] {
1024 if (name === void 0) {
1025 return this.symbols;
1026 }
1027
1028 return this.symbols.filter(symbol => symbol.typeName === name);
1029 }
1030
1031 public getUserSymbols(): string[] {
1032 return Object.keys(this.userSymbols);
1033 }
1034
1035 public getMainFileSymbols(program: ts.Program, onlyIncludeFiles?: string[]): string[] {
1036 function includeFile(file: ts.SourceFile): boolean {
1037 if (onlyIncludeFiles === undefined) {
1038 return !file.isDeclarationFile;
1039 }
1040 return onlyIncludeFiles.indexOf(file.fileName) >= 0;
1041 }
1042 const files = program.getSourceFiles().filter(includeFile);
1043 if (files.length) {
1044 return Object.keys(this.userSymbols).filter((key) => {
1045 const symbol = this.userSymbols[key];
1046 if (!symbol || !symbol.declarations || !symbol.declarations.length) {
1047 return false;
1048 }
1049 let node: ts.Node = symbol.declarations[0];
1050 while (node && node.parent) {
1051 node = node.parent;
1052 }
1053 return files.indexOf(node.getSourceFile()) > -1;
1054 });
1055 }
1056 return [];
1057 }
1058}
1059
1060export function getProgramFromFiles(files: string[], jsonCompilerOptions: any = {}, basePath: string = "./"): ts.Program {
1061 // use built-in default options
1062 const compilerOptions = ts.convertCompilerOptionsFromJson(jsonCompilerOptions, basePath).options;
1063 const options: ts.CompilerOptions = {
1064 noEmit: true, emitDecoratorMetadata: true, experimentalDecorators: true, target: ts.ScriptTarget.ES5, module: ts.ModuleKind.CommonJS, allowUnusedLabels: true,
1065 };
1066 for (const k in compilerOptions) {
1067 if (compilerOptions.hasOwnProperty(k)) {
1068 options[k] = compilerOptions[k];
1069 }
1070 }
1071 return ts.createProgram(files, options);
1072}
1073
1074function generateHashOfNode(node: ts.Node, relativePath: string) {
1075 return createHash("md5").update(relativePath).update(node.pos.toString()).digest("hex").substring(0, 8);
1076}
1077
1078export function buildGenerator(program: ts.Program, args: PartialArgs = {}, onlyIncludeFiles?: string[]): JsonSchemaGenerator|null {
1079 function isUserFile(file: ts.SourceFile): boolean {
1080 if (onlyIncludeFiles === undefined) {
1081 return !file.hasNoDefaultLib;
1082 }
1083 return onlyIncludeFiles.indexOf(file.fileName) >= 0;
1084 }
1085 // Use defaults unles otherwise specified
1086 const settings = getDefaultArgs();
1087
1088 for (const pref in args) {
1089 if (args.hasOwnProperty(pref)) {
1090 settings[pref] = args[pref];
1091 }
1092 }
1093
1094 let diagnostics: ReadonlyArray<ts.Diagnostic> = [];
1095
1096 if (!args.ignoreErrors) {
1097 diagnostics = ts.getPreEmitDiagnostics(program);
1098 }
1099
1100 if (diagnostics.length === 0) {
1101 const typeChecker = program.getTypeChecker();
1102
1103 const symbols: SymbolRef[] = [];
1104 const allSymbols: { [name: string]: ts.Type } = {};
1105 const userSymbols: { [name: string]: ts.Symbol } = {};
1106 const inheritingTypes: { [baseName: string]: string[] } = {};
1107 const workingDir = program.getCurrentDirectory();
1108
1109 program.getSourceFiles().forEach((sourceFile, _sourceFileIdx) => {
1110 const relativePath = path.relative(workingDir, sourceFile.fileName);
1111
1112 function inspect(node: ts.Node, tc: ts.TypeChecker) {
1113
1114 if (node.kind === ts.SyntaxKind.ClassDeclaration
1115 || node.kind === ts.SyntaxKind.InterfaceDeclaration
1116 || node.kind === ts.SyntaxKind.EnumDeclaration
1117 || node.kind === ts.SyntaxKind.TypeAliasDeclaration
1118 ) {
1119 const symbol: ts.Symbol = (<any>node).symbol;
1120 const nodeType = tc.getTypeAtLocation(node);
1121 const fullyQualifiedName = tc.getFullyQualifiedName(symbol);
1122 const typeName = fullyQualifiedName.replace(/".*"\./, "");
1123 const name = !args.uniqueNames ? typeName : `${typeName}.${generateHashOfNode(node, relativePath)}`;
1124
1125 symbols.push({ name, typeName, fullyQualifiedName, symbol });
1126 if (!userSymbols[name]) {
1127 allSymbols[name] = nodeType;
1128 }
1129
1130 if (isUserFile(sourceFile)) {
1131 userSymbols[name] = symbol;
1132 }
1133
1134 const baseTypes = nodeType.getBaseTypes() || [];
1135
1136 baseTypes.forEach(baseType => {
1137 var baseName = tc.typeToString(baseType, undefined, ts.TypeFormatFlags.UseFullyQualifiedType);
1138 if (!inheritingTypes[baseName]) {
1139 inheritingTypes[baseName] = [];
1140 }
1141 inheritingTypes[baseName].push(name);
1142 });
1143 } else {
1144 ts.forEachChild(node, n => inspect(n, tc));
1145 }
1146 }
1147 inspect(sourceFile, typeChecker);
1148 });
1149
1150 return new JsonSchemaGenerator(symbols, allSymbols, userSymbols, inheritingTypes, typeChecker, settings);
1151 } else {
1152 diagnostics.forEach((diagnostic) => {
1153 const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
1154 if(diagnostic.file) {
1155 const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start!);
1156 console.error(`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`);
1157 } else {
1158 console.error(message);
1159 }
1160 });
1161 return null;
1162 }
1163}
1164
1165export function generateSchema(program: ts.Program, fullTypeName: string, args: PartialArgs = {}, onlyIncludeFiles?: string[]): Definition|null {
1166 const generator = buildGenerator(program, args, onlyIncludeFiles);
1167
1168 if (generator === null) {
1169 return null;
1170 }
1171
1172 if (fullTypeName === "*") { // All types in file(s)
1173 return generator.getSchemaForSymbols(generator.getMainFileSymbols(program, onlyIncludeFiles));
1174 } else { // Use specific type as root object
1175 return generator.getSchemaForSymbol(fullTypeName);
1176 }
1177}
1178
1179export function programFromConfig(configFileName: string, onlyIncludeFiles?: string[]): ts.Program {
1180 // basically a copy of https://github.com/Microsoft/TypeScript/blob/3663d400270ccae8b69cbeeded8ffdc8fa12d7ad/src/compiler/tsc.ts -> parseConfigFile
1181 const result = ts.parseConfigFileTextToJson(configFileName, ts.sys.readFile(configFileName)!);
1182 const configObject = result.config;
1183
1184 const configParseResult = ts.parseJsonConfigFileContent(configObject, ts.sys, path.dirname(configFileName), {}, path.basename(configFileName));
1185 const options = configParseResult.options;
1186 options.noEmit = true;
1187 delete options.out;
1188 delete options.outDir;
1189 delete options.outFile;
1190 delete options.declaration;
1191 delete options.declarationDir;
1192 delete options.declarationMap;
1193
1194 const program = ts.createProgram({
1195 rootNames: onlyIncludeFiles || configParseResult.fileNames,
1196 options,
1197 projectReferences: configParseResult.projectReferences
1198 });
1199 return program;
1200}
1201
1202function normalizeFileName(fn: string): string {
1203 while (fn.substr(0, 2) === "./") {
1204 fn = fn.substr(2);
1205 }
1206 return fn;
1207}
1208
1209export function exec(filePattern: string, fullTypeName: string, args = getDefaultArgs()) {
1210 let program: ts.Program;
1211 let onlyIncludeFiles: string[] | undefined = undefined;
1212 if (REGEX_TSCONFIG_NAME.test(path.basename(filePattern))) {
1213 if (args.include && args.include.length > 0) {
1214 const globs: string[][] = args.include.map(f => glob.sync(f));
1215 onlyIncludeFiles = ([] as string[]).concat(...globs).map(normalizeFileName);
1216 }
1217 program = programFromConfig(filePattern, onlyIncludeFiles);
1218 } else {
1219 onlyIncludeFiles = glob.sync(filePattern);
1220 program = getProgramFromFiles(onlyIncludeFiles, {
1221 strictNullChecks: args.strictNullChecks
1222 });
1223 onlyIncludeFiles = onlyIncludeFiles.map(normalizeFileName);
1224 }
1225
1226 const definition = generateSchema(program, fullTypeName, args, onlyIncludeFiles);
1227 if (definition === null) {
1228 throw new Error("No output definition. Probably caused by errors prior to this?");
1229 }
1230
1231 const json = stringify(definition, {space: 4}) + "\n\n";
1232 if (args.out) {
1233 require("fs").writeFile(args.out, json, function(err: Error) {
1234 if (err) {
1235 throw new Error("Unable to write output file: " + err.message);
1236 }
1237 });
1238 } else {
1239 process.stdout.write(json);
1240 }
1241}