From 0fb2a1e7c96d46d55cadcc24eadcc89dd0a84db8 Mon Sep 17 00:00:00 2001 From: Klaus Reimer Date: Sat, 1 Jun 2019 18:01:36 +0200 Subject: [PATCH 01/25] Add build and test task --- .vscode/tasks.json | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .vscode/tasks.json diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 000000000..f28ef924f --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,31 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Build (and watch) project", + "type": "npm", + "script": "watch", + "isBackground": true, + "presentation": { + "reveal": "never" + }, + "problemMatcher": [ + "$tsc-watch" + ], + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "Run unit tests", + "type": "npm", + "script": "test", + "problemMatcher": [], + "group": { + "kind": "test", + "isDefault": true + } + } + ] +} From 2b2665fc99fd4adcf1b1ffdd55efa02f4c519d65 Mon Sep 17 00:00:00 2001 From: Klaus Reimer Date: Sat, 1 Jun 2019 18:02:24 +0200 Subject: [PATCH 02/25] Implement crude conditional type node parser --- factory/parser.ts | 4 + src/NodeParser/ConditionalTypeNodeParser.ts | 197 ++++++++++++++++++++ src/NodeParser/NeverTypeNodeParser.ts | 14 ++ src/Type/NeverType.ts | 7 + 4 files changed, 222 insertions(+) create mode 100644 src/NodeParser/ConditionalTypeNodeParser.ts create mode 100644 src/NodeParser/NeverTypeNodeParser.ts create mode 100644 src/Type/NeverType.ts diff --git a/factory/parser.ts b/factory/parser.ts index 44cc7a58b..4fd96b6cb 100644 --- a/factory/parser.ts +++ b/factory/parser.ts @@ -12,6 +12,7 @@ import { ArrayNodeParser } from "../src/NodeParser/ArrayNodeParser"; import { BooleanLiteralNodeParser } from "../src/NodeParser/BooleanLiteralNodeParser"; import { BooleanTypeNodeParser } from "../src/NodeParser/BooleanTypeNodeParser"; import { CallExpressionParser } from "../src/NodeParser/CallExpressionParser"; +import { ConditionalTypeNodeParser } from "../src/NodeParser/ConditionalTypeNodeParser"; import { EnumNodeParser } from "../src/NodeParser/EnumNodeParser"; import { ExpressionWithTypeArgumentsNodeParser } from "../src/NodeParser/ExpressionWithTypeArgumentsNodeParser"; import { IndexedAccessTypeNodeParser } from "../src/NodeParser/IndexedAccessTypeNodeParser"; @@ -19,6 +20,7 @@ import { InterfaceNodeParser } from "../src/NodeParser/InterfaceNodeParser"; import { IntersectionNodeParser } from "../src/NodeParser/IntersectionNodeParser"; import { LiteralNodeParser } from "../src/NodeParser/LiteralNodeParser"; import { MappedTypeNodeParser } from "../src/NodeParser/MappedTypeNodeParser"; +import { NeverTypeNodeParser } from "../src/NodeParser/NeverTypeNodeParser"; import { NullLiteralNodeParser } from "../src/NodeParser/NullLiteralNodeParser"; import { NumberLiteralNodeParser } from "../src/NodeParser/NumberLiteralNodeParser"; import { NumberTypeNodeParser } from "../src/NodeParser/NumberTypeNodeParser"; @@ -71,6 +73,7 @@ export function createParser(program: ts.Program, config: Config): NodeParser { .addNodeParser(new BooleanTypeNodeParser()) .addNodeParser(new AnyTypeNodeParser()) .addNodeParser(new UndefinedTypeNodeParser()) + .addNodeParser(new NeverTypeNodeParser()) .addNodeParser(new ObjectTypeNodeParser()) .addNodeParser(new StringLiteralNodeParser()) @@ -87,6 +90,7 @@ export function createParser(program: ts.Program, config: Config): NodeParser { .addNodeParser(new IndexedAccessTypeNodeParser(chainNodeParser)) .addNodeParser(new TypeofNodeParser(typeChecker, chainNodeParser)) .addNodeParser(new MappedTypeNodeParser(chainNodeParser)) + .addNodeParser(new ConditionalTypeNodeParser(chainNodeParser)) .addNodeParser(new TypeOperatorNodeParser(chainNodeParser)) .addNodeParser(new UnionNodeParser(typeChecker, chainNodeParser)) diff --git a/src/NodeParser/ConditionalTypeNodeParser.ts b/src/NodeParser/ConditionalTypeNodeParser.ts new file mode 100644 index 000000000..8aa6c1b20 --- /dev/null +++ b/src/NodeParser/ConditionalTypeNodeParser.ts @@ -0,0 +1,197 @@ +import * as ts from "typescript"; +import { Context, NodeParser } from "../NodeParser"; +import { SubNodeParser } from "../SubNodeParser"; +import { AliasType } from "../Type/AliasType"; +import { AnnotatedType } from "../Type/AnnotatedType"; +import { AnyType } from "../Type/AnyType"; +import { BaseType } from "../Type/BaseType"; +import { DefinitionType } from "../Type/DefinitionType"; +import { IntersectionType } from "../Type/IntersectionType"; +import { NeverType } from "../Type/NeverType"; +import { ObjectProperty, ObjectType } from "../Type/ObjectType"; +import { UnionType } from "../Type/UnionType"; + +export class ConditionalTypeNodeParser implements SubNodeParser { + public constructor( + private childNodeParser: NodeParser, + ) {} + + public supportsNode(node: ts.ConditionalTypeNode): boolean { + return node.kind === ts.SyntaxKind.ConditionalType; + } + + public createType(node: ts.ConditionalTypeNode, context: Context): BaseType { + const extendsType = this.unwrapType(this.childNodeParser.createType(node.extendsType, context)); + + // Get the check type from the condition and expand them into an array of check types in case the check + // type is a union type. Each union type candidate is checked separately and the result is again grouped + // into a union type if necessary + const checkType = this.unwrapType(this.childNodeParser.createType(node.checkType, context)); + const checkTypes = checkType instanceof UnionType ? checkType.getTypes() : [ checkType ]; + + // Process each part of the check type separately + const resultTypes: BaseType[] = []; + for (const type of checkTypes) { + const resultType = this.unwrapType(this.isAssignableFrom(extendsType, type) + ? this.childNodeParser.createType(node.trueType, context) + : this.childNodeParser.createType(node.falseType, context)); + + // Ignore never types (Used in exclude conditions) so they are not added to the result union type + if (resultType instanceof NeverType) { + continue; + } + + // If result type is actually the original check type (Which may be a union type) then only record the + // currently processed check type as a result. If check type is not a union type then this makes no + // difference but for union types this ensures that only the matching part of it is added to the result + // which is important for exclude conditions. + if (resultType.getId() === checkType.getId()) { + resultTypes.push(type); + } else { + resultTypes.push(resultType); + } + } + + // If there is only one result type then return this one directly. Otherwise return the recorded + // result types as a union type. + if (resultTypes.length === 1) { + return resultTypes[0]; + } else { + return new UnionType(resultTypes); + } + } + + /** + * Unwraps a type if necessary. + * + * @param type - The type to unwrap + * @return The unwrapped type. + */ + private unwrapType(type: BaseType): BaseType { + if (type instanceof AliasType || type instanceof DefinitionType || type instanceof AnnotatedType) { + return this.unwrapType(type.getType()); + } + return type; + } + + /** + * Returns all properties of the given type and its base types (if any). + * + * @param type - The type for which to return the properties. + * @return The object properties. May be empty if no objects are present or type is not an object type. + */ + private getObjectProperties(type: BaseType): ObjectProperty[] { + type = this.unwrapType(type); + const properties: ObjectProperty[] = []; + if (type instanceof ObjectType) { + properties.push(...type.getProperties()); + for (const baseType of type.getBaseTypes()) { + properties.push(...this.getObjectProperties(baseType)); + } + } + return properties; + } + + /** + * Checks if given source type is assignable to given target type. + * + * @param target - The target type. + * @param source - The source type. + * @return True if source type is assignable to target type. + */ + private isAssignableFrom(target: BaseType, source: BaseType): boolean { + source = this.unwrapType(source); + target = this.unwrapType(target); + + // If type IDs matches or target is any type then source can be assigned to target + if (target.getId() === source.getId() || target instanceof AnyType) { + return true; + } + + // When target is a union type then check if source type can be assigned to any of it + if (target instanceof UnionType) { + return target.getTypes().some(type => this.isAssignableFrom(type, source)); + } + + // When target is an intersection type then check if source type can be assigned to all of them + if (target instanceof IntersectionType) { + return target.getTypes().every(type => this.isAssignableFrom(type, source)); + } + + // When source is an intersection type then check if at least one of the intersect types can be assigned to + // target type + if (source instanceof IntersectionType) { + return source.getTypes().some(type => this.isAssignableFrom(target, type)); + } + + // If source type and target type is an object type then check inheritance + if (source instanceof ObjectType && target instanceof ObjectType) { + // First do a quick base type check. Maybe their IDs already match so we don't have to compare properties + if (source.getBaseTypes().some(type => this.isAssignableFrom(target, type))) { + return true; + } + + // Perform a full object compatibility check + return this.isCompatibleTo(target, source); + } + return false; + } + + /** + * Checks if the given source type is compatible to given target type but comparing their properties. + * + * @param target - The target object type. + * @param source - The source object type. + * @return True if source is compatible to target, false if not. + */ + private isCompatibleTo(target: ObjectType, source: ObjectType): boolean { + // Check property compatibility + const sourceProperties = this.getObjectProperties(source); + for (const targetProperty of target.getProperties()) { + const sourceProperty = sourceProperties.find(property => property.getName() === targetProperty.getName()); + if (sourceProperty) { + // If source property with same name as in target property exists then compare its types + if (!this.isAssignableFrom(targetProperty.getType(), sourceProperty.getType())) { + return false; + } + } else { + // If source has no such property but property is required then types are not compatible + if (targetProperty.isRequired()) { + return false; + } + } + } + + // Check additional properties compatibility + const targetAdditionalPropertyType = target.getAdditionalProperties(); + const sourceAdditionalProperties = source.getAdditionalProperties(); + if (typeof targetAdditionalPropertyType === "boolean") { + if (sourceAdditionalProperties !== targetAdditionalPropertyType) { + return false; + } + } else { + if (typeof sourceAdditionalProperties === "boolean") { + return false; + } else { + if (!this.isAssignableFrom(targetAdditionalPropertyType, sourceAdditionalProperties)) { + return false; + } + } + } + + // Check compatibility to base types + for (const baseType of target.getBaseTypes()) { + const resolved = this.unwrapType(baseType); + if (resolved instanceof ObjectType) { + if (!this.isCompatibleTo(resolved, source)) { + return false; + } + } else { + return false; + } + } + + // Looks like types are compatible + return true; + } +} diff --git a/src/NodeParser/NeverTypeNodeParser.ts b/src/NodeParser/NeverTypeNodeParser.ts new file mode 100644 index 000000000..dea2016c0 --- /dev/null +++ b/src/NodeParser/NeverTypeNodeParser.ts @@ -0,0 +1,14 @@ +import * as ts from "typescript"; +import { Context } from "../NodeParser"; +import { SubNodeParser } from "../SubNodeParser"; +import { BaseType } from "../Type/BaseType"; +import { NeverType } from "../Type/NeverType"; + +export class NeverTypeNodeParser implements SubNodeParser { + public supportsNode(node: ts.KeywordTypeNode): boolean { + return node.kind === ts.SyntaxKind.NeverKeyword; + } + public createType(node: ts.KeywordTypeNode, context: Context): BaseType { + return new NeverType(); + } +} diff --git a/src/Type/NeverType.ts b/src/Type/NeverType.ts new file mode 100644 index 000000000..f2f8c7e78 --- /dev/null +++ b/src/Type/NeverType.ts @@ -0,0 +1,7 @@ +import { BaseType } from "./BaseType"; + +export class NeverType extends BaseType { + public getId(): string { + return "never"; + } +} From a135acf6b1d0cd01b8ed7775fc26870272ccca3c Mon Sep 17 00:00:00 2001 From: Klaus Reimer Date: Sat, 1 Jun 2019 18:03:10 +0200 Subject: [PATCH 03/25] Fix linter warning --- test/valid-data/type-mapped-generic/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/valid-data/type-mapped-generic/main.ts b/test/valid-data/type-mapped-generic/main.ts index 463937de0..3c56d6d16 100644 --- a/test/valid-data/type-mapped-generic/main.ts +++ b/test/valid-data/type-mapped-generic/main.ts @@ -9,4 +9,4 @@ export type NullableAndPartial = { [K in keyof T]?: T[K] | null; }; -export type MyObject = NullableAndPartial; \ No newline at end of file +export type MyObject = NullableAndPartial; From 8c97e42df479ccd8a47ca1e815dbfbc296bdc4d2 Mon Sep 17 00:00:00 2001 From: Klaus Reimer Date: Sat, 1 Jun 2019 18:03:29 +0200 Subject: [PATCH 04/25] Add some tests for conditional type --- test/valid-data.test.ts | 6 ++ .../type-conditional-exclude/main.ts | 8 ++ .../type-conditional-exclude/schema.json | 40 ++++++++++ .../type-conditional-inheritance/main.ts | 41 ++++++++++ .../type-conditional-inheritance/schema.json | 74 +++++++++++++++++++ .../type-conditional-intersection/main.ts | 30 ++++++++ .../type-conditional-intersection/schema.json | 56 ++++++++++++++ .../type-conditional-simple/main.ts | 10 +++ .../type-conditional-simple/schema.json | 35 +++++++++ .../valid-data/type-conditional-union/main.ts | 22 ++++++ .../type-conditional-union/schema.json | 42 +++++++++++ 11 files changed, 364 insertions(+) create mode 100644 test/valid-data/type-conditional-exclude/main.ts create mode 100644 test/valid-data/type-conditional-exclude/schema.json create mode 100644 test/valid-data/type-conditional-inheritance/main.ts create mode 100644 test/valid-data/type-conditional-inheritance/schema.json create mode 100644 test/valid-data/type-conditional-intersection/main.ts create mode 100644 test/valid-data/type-conditional-intersection/schema.json create mode 100644 test/valid-data/type-conditional-simple/main.ts create mode 100644 test/valid-data/type-conditional-simple/schema.json create mode 100644 test/valid-data/type-conditional-union/main.ts create mode 100644 test/valid-data/type-conditional-union/schema.json diff --git a/test/valid-data.test.ts b/test/valid-data.test.ts index f5f2a85a8..af1fd9897 100644 --- a/test/valid-data.test.ts +++ b/test/valid-data.test.ts @@ -146,4 +146,10 @@ describe("valid-data", () => { it("undefined-property", assertSchema("undefined-property", "MyType")); it("any-unknown", assertSchema("any-unknown", "MyObject")); + + it("type-conditional-simple", assertSchema("type-conditional-simple", "MyObject")); + it("type-conditional-inheritance", assertSchema("type-conditional-inheritance", "MyObject")); + it("type-conditional-union", assertSchema("type-conditional-union", "MyObject")); + it("type-conditional-intersection", assertSchema("type-conditional-intersection", "MyObject")); + it("type-conditional-exclude", assertSchema("type-conditional-exclude", "MyObject")); }); diff --git a/test/valid-data/type-conditional-exclude/main.ts b/test/valid-data/type-conditional-exclude/main.ts new file mode 100644 index 000000000..beff73079 --- /dev/null +++ b/test/valid-data/type-conditional-exclude/main.ts @@ -0,0 +1,8 @@ +export type Primitives = string | number | boolean; + +export type MyObject = { + primitives: Primitives; + noNumber: Exclude; + noNumberAndBoolean: Exclude; + noStringAndNumber: Exclude, string>; +}; diff --git a/test/valid-data/type-conditional-exclude/schema.json b/test/valid-data/type-conditional-exclude/schema.json new file mode 100644 index 000000000..a23064f59 --- /dev/null +++ b/test/valid-data/type-conditional-exclude/schema.json @@ -0,0 +1,40 @@ +{ + "$ref": "#/definitions/MyObject", + "$schema": "http://json-schema.org/draft-06/schema#", + "definitions": { + "MyObject": { + "additionalProperties": false, + "properties": { + "noNumber": { + "type": [ + "string", + "boolean" + ] + }, + "noNumberAndBoolean": { + "type": "string" + }, + "noStringAndNumber": { + "type": "boolean" + }, + "primitives": { + "$ref": "#/definitions/Primitives" + } + }, + "required": [ + "primitives", + "noNumber", + "noNumberAndBoolean", + "noStringAndNumber" + ], + "type": "object" + }, + "Primitives": { + "type": [ + "string", + "number", + "boolean" + ] + } + } +} diff --git a/test/valid-data/type-conditional-inheritance/main.ts b/test/valid-data/type-conditional-inheritance/main.ts new file mode 100644 index 000000000..48290207c --- /dev/null +++ b/test/valid-data/type-conditional-inheritance/main.ts @@ -0,0 +1,41 @@ +export interface A { + a: string; +} + +export interface B extends A { + b: string; +} + +export interface C extends B { + a: string; + c: string; +} + +export interface D { + a: number; + d: string; +} + +export interface E extends D {} + +export interface F extends D { + f: boolean; +} + +export type Map = + T extends A ? "a" : + T extends B ? "b" : + T extends C ? "c" : + T extends F ? "f" : + T extends D ? "d" : + T extends E ? "e" : + "unknown"; + +export type MyObject = { + a: Map; + b: Map; + c: Map; + d: Map; + e: Map; + f: Map; +}; diff --git a/test/valid-data/type-conditional-inheritance/schema.json b/test/valid-data/type-conditional-inheritance/schema.json new file mode 100644 index 000000000..8f172faa6 --- /dev/null +++ b/test/valid-data/type-conditional-inheritance/schema.json @@ -0,0 +1,74 @@ +{ + "$ref": "#/definitions/MyObject", + "$schema": "http://json-schema.org/draft-06/schema#", + "definitions": { + "Map": { + "enum": [ + "a" + ], + "type": "string" + }, + "Map": { + "enum": [ + "a" + ], + "type": "string" + }, + "Map": { + "enum": [ + "a" + ], + "type": "string" + }, + "Map": { + "enum": [ + "d" + ], + "type": "string" + }, + "Map": { + "enum": [ + "d" + ], + "type": "string" + }, + "Map": { + "enum": [ + "f" + ], + "type": "string" + }, + "MyObject": { + "additionalProperties": false, + "properties": { + "a": { + "$ref": "#/definitions/Map" + }, + "b": { + "$ref": "#/definitions/Map" + }, + "c": { + "$ref": "#/definitions/Map" + }, + "d": { + "$ref": "#/definitions/Map" + }, + "e": { + "$ref": "#/definitions/Map" + }, + "f": { + "$ref": "#/definitions/Map" + } + }, + "required": [ + "a", + "b", + "c", + "d", + "e", + "f" + ], + "type": "object" + } + } +} diff --git a/test/valid-data/type-conditional-intersection/main.ts b/test/valid-data/type-conditional-intersection/main.ts new file mode 100644 index 000000000..f7db9ca0d --- /dev/null +++ b/test/valid-data/type-conditional-intersection/main.ts @@ -0,0 +1,30 @@ +interface A { + a: string; +} + +interface B { + b: string; +} + +interface C extends A, B {} + +interface D extends A, B { + d: string; +} + +type Map = + T extends D ? "D" : + T extends A & B ? "a and b" : + T extends A ? "a" : + T extends B ? "b" : + T extends C ? "c" : + "unknown"; + +export type MyObject = { + a: Map; + b: Map; + c: Map; + d: Map; + e: Map; + f: Map; +}; diff --git a/test/valid-data/type-conditional-intersection/schema.json b/test/valid-data/type-conditional-intersection/schema.json new file mode 100644 index 000000000..4b7a454ee --- /dev/null +++ b/test/valid-data/type-conditional-intersection/schema.json @@ -0,0 +1,56 @@ +{ + "$ref": "#/definitions/MyObject", + "$schema": "http://json-schema.org/draft-06/schema#", + "definitions": { + "MyObject": { + "additionalProperties": false, + "properties": { + "a": { + "enum": [ + "a" + ], + "type": "string" + }, + "b": { + "enum": [ + "b" + ], + "type": "string" + }, + "c": { + "enum": [ + "a and b" + ], + "type": "string" + }, + "d": { + "enum": [ + "D" + ], + "type": "string" + }, + "e": { + "enum": [ + "a and b" + ], + "type": "string" + }, + "f": { + "enum": [ + "D" + ], + "type": "string" + } + }, + "required": [ + "a", + "b", + "c", + "d", + "e", + "f" + ], + "type": "object" + } + } +} diff --git a/test/valid-data/type-conditional-simple/main.ts b/test/valid-data/type-conditional-simple/main.ts new file mode 100644 index 000000000..df0e446fb --- /dev/null +++ b/test/valid-data/type-conditional-simple/main.ts @@ -0,0 +1,10 @@ +type TypeName = + T extends string ? "string" : + T extends number ? "number" : + "unknown"; + +export type MyObject = { + a: TypeName; + b: TypeName; + c: TypeName; +}; diff --git a/test/valid-data/type-conditional-simple/schema.json b/test/valid-data/type-conditional-simple/schema.json new file mode 100644 index 000000000..0731a82e1 --- /dev/null +++ b/test/valid-data/type-conditional-simple/schema.json @@ -0,0 +1,35 @@ +{ + "$ref": "#/definitions/MyObject", + "$schema": "http://json-schema.org/draft-06/schema#", + "definitions": { + "MyObject": { + "additionalProperties": false, + "properties": { + "a": { + "enum": [ + "string" + ], + "type": "string" + }, + "b": { + "enum": [ + "number" + ], + "type": "string" + }, + "c": { + "enum": [ + "unknown" + ], + "type": "string" + } + }, + "required": [ + "a", + "b", + "c" + ], + "type": "object" + } + } +} diff --git a/test/valid-data/type-conditional-union/main.ts b/test/valid-data/type-conditional-union/main.ts new file mode 100644 index 000000000..482bb5427 --- /dev/null +++ b/test/valid-data/type-conditional-union/main.ts @@ -0,0 +1,22 @@ +interface A { + a: string; +} + +interface B { + b: string; +} + +interface C { + c: string; +} + +type Map = + T extends (A | B) ? "a or b" : + "unknown"; + +export type MyObject = { + a: Map; + b: Map; + c: Map; + d: Map; +}; diff --git a/test/valid-data/type-conditional-union/schema.json b/test/valid-data/type-conditional-union/schema.json new file mode 100644 index 000000000..98c6cb0a7 --- /dev/null +++ b/test/valid-data/type-conditional-union/schema.json @@ -0,0 +1,42 @@ +{ + "$ref": "#/definitions/MyObject", + "$schema": "http://json-schema.org/draft-06/schema#", + "definitions": { + "MyObject": { + "additionalProperties": false, + "properties": { + "a": { + "enum": [ + "a or b" + ], + "type": "string" + }, + "b": { + "enum": [ + "a or b" + ], + "type": "string" + }, + "c": { + "enum": [ + "unknown" + ], + "type": "string" + }, + "d": { + "enum": [ + "a or b" + ], + "type": "string" + } + }, + "required": [ + "a", + "b", + "c", + "d" + ], + "type": "object" + } + } +} From c8db06b16a9bcb557fb3990d54bbfa6cbeea9aae Mon Sep 17 00:00:00 2001 From: Klaus Reimer Date: Mon, 3 Jun 2019 07:20:45 +0200 Subject: [PATCH 05/25] Only unwrap types internally when needed --- src/NodeParser/ConditionalTypeNodeParser.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/NodeParser/ConditionalTypeNodeParser.ts b/src/NodeParser/ConditionalTypeNodeParser.ts index 8aa6c1b20..4665d6e1e 100644 --- a/src/NodeParser/ConditionalTypeNodeParser.ts +++ b/src/NodeParser/ConditionalTypeNodeParser.ts @@ -21,23 +21,25 @@ export class ConditionalTypeNodeParser implements SubNodeParser { } public createType(node: ts.ConditionalTypeNode, context: Context): BaseType { - const extendsType = this.unwrapType(this.childNodeParser.createType(node.extendsType, context)); + const extendsType = this.childNodeParser.createType(node.extendsType, context); // Get the check type from the condition and expand them into an array of check types in case the check // type is a union type. Each union type candidate is checked separately and the result is again grouped // into a union type if necessary - const checkType = this.unwrapType(this.childNodeParser.createType(node.checkType, context)); - const checkTypes = checkType instanceof UnionType ? checkType.getTypes() : [ checkType ]; + const checkType = this.childNodeParser.createType(node.checkType, context); + const unwrappedCheckType = this.unwrapType(checkType); + const checkTypes = unwrappedCheckType instanceof UnionType ? unwrappedCheckType.getTypes() : [ checkType ]; // Process each part of the check type separately const resultTypes: BaseType[] = []; for (const type of checkTypes) { - const resultType = this.unwrapType(this.isAssignableFrom(extendsType, type) + const resultType = this.isAssignableFrom(extendsType, type) ? this.childNodeParser.createType(node.trueType, context) - : this.childNodeParser.createType(node.falseType, context)); + : this.childNodeParser.createType(node.falseType, context); + const unwrappedResultType = this.unwrapType(resultType); // Ignore never types (Used in exclude conditions) so they are not added to the result union type - if (resultType instanceof NeverType) { + if (unwrappedResultType instanceof NeverType) { continue; } From c7c2aa2df03f1c5b0f104f8f5837eb7cfa7da110 Mon Sep 17 00:00:00 2001 From: Klaus Reimer Date: Wed, 5 Jun 2019 08:15:14 +0200 Subject: [PATCH 06/25] Update schema URLs to draft-07 --- test/valid-data/type-conditional-exclude/schema.json | 2 +- test/valid-data/type-conditional-inheritance/schema.json | 2 +- test/valid-data/type-conditional-intersection/schema.json | 2 +- test/valid-data/type-conditional-simple/schema.json | 2 +- test/valid-data/type-conditional-union/schema.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/valid-data/type-conditional-exclude/schema.json b/test/valid-data/type-conditional-exclude/schema.json index a23064f59..9516d8c39 100644 --- a/test/valid-data/type-conditional-exclude/schema.json +++ b/test/valid-data/type-conditional-exclude/schema.json @@ -1,6 +1,6 @@ { "$ref": "#/definitions/MyObject", - "$schema": "http://json-schema.org/draft-06/schema#", + "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "MyObject": { "additionalProperties": false, diff --git a/test/valid-data/type-conditional-inheritance/schema.json b/test/valid-data/type-conditional-inheritance/schema.json index 8f172faa6..705d790c8 100644 --- a/test/valid-data/type-conditional-inheritance/schema.json +++ b/test/valid-data/type-conditional-inheritance/schema.json @@ -1,6 +1,6 @@ { "$ref": "#/definitions/MyObject", - "$schema": "http://json-schema.org/draft-06/schema#", + "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "Map": { "enum": [ diff --git a/test/valid-data/type-conditional-intersection/schema.json b/test/valid-data/type-conditional-intersection/schema.json index 4b7a454ee..9dee72747 100644 --- a/test/valid-data/type-conditional-intersection/schema.json +++ b/test/valid-data/type-conditional-intersection/schema.json @@ -1,6 +1,6 @@ { "$ref": "#/definitions/MyObject", - "$schema": "http://json-schema.org/draft-06/schema#", + "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "MyObject": { "additionalProperties": false, diff --git a/test/valid-data/type-conditional-simple/schema.json b/test/valid-data/type-conditional-simple/schema.json index 0731a82e1..cb0116d05 100644 --- a/test/valid-data/type-conditional-simple/schema.json +++ b/test/valid-data/type-conditional-simple/schema.json @@ -1,6 +1,6 @@ { "$ref": "#/definitions/MyObject", - "$schema": "http://json-schema.org/draft-06/schema#", + "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "MyObject": { "additionalProperties": false, diff --git a/test/valid-data/type-conditional-union/schema.json b/test/valid-data/type-conditional-union/schema.json index 98c6cb0a7..9d8d74804 100644 --- a/test/valid-data/type-conditional-union/schema.json +++ b/test/valid-data/type-conditional-union/schema.json @@ -1,6 +1,6 @@ { "$ref": "#/definitions/MyObject", - "$schema": "http://json-schema.org/draft-06/schema#", + "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "MyObject": { "additionalProperties": false, From b0b71dfb191e4b5305e9c284c91a9513592b9db9 Mon Sep 17 00:00:00 2001 From: Klaus Reimer Date: Wed, 5 Jun 2019 08:18:22 +0200 Subject: [PATCH 07/25] Use already existing derefType --- src/NodeParser/ConditionalTypeNodeParser.ts | 29 +++++---------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/src/NodeParser/ConditionalTypeNodeParser.ts b/src/NodeParser/ConditionalTypeNodeParser.ts index 4665d6e1e..504de6658 100644 --- a/src/NodeParser/ConditionalTypeNodeParser.ts +++ b/src/NodeParser/ConditionalTypeNodeParser.ts @@ -1,15 +1,13 @@ import * as ts from "typescript"; import { Context, NodeParser } from "../NodeParser"; import { SubNodeParser } from "../SubNodeParser"; -import { AliasType } from "../Type/AliasType"; -import { AnnotatedType } from "../Type/AnnotatedType"; import { AnyType } from "../Type/AnyType"; import { BaseType } from "../Type/BaseType"; -import { DefinitionType } from "../Type/DefinitionType"; import { IntersectionType } from "../Type/IntersectionType"; import { NeverType } from "../Type/NeverType"; import { ObjectProperty, ObjectType } from "../Type/ObjectType"; import { UnionType } from "../Type/UnionType"; +import { derefType } from "../Utils/derefType"; export class ConditionalTypeNodeParser implements SubNodeParser { public constructor( @@ -27,7 +25,7 @@ export class ConditionalTypeNodeParser implements SubNodeParser { // type is a union type. Each union type candidate is checked separately and the result is again grouped // into a union type if necessary const checkType = this.childNodeParser.createType(node.checkType, context); - const unwrappedCheckType = this.unwrapType(checkType); + const unwrappedCheckType = derefType(checkType); const checkTypes = unwrappedCheckType instanceof UnionType ? unwrappedCheckType.getTypes() : [ checkType ]; // Process each part of the check type separately @@ -36,7 +34,7 @@ export class ConditionalTypeNodeParser implements SubNodeParser { const resultType = this.isAssignableFrom(extendsType, type) ? this.childNodeParser.createType(node.trueType, context) : this.childNodeParser.createType(node.falseType, context); - const unwrappedResultType = this.unwrapType(resultType); + const unwrappedResultType = derefType(resultType); // Ignore never types (Used in exclude conditions) so they are not added to the result union type if (unwrappedResultType instanceof NeverType) { @@ -63,19 +61,6 @@ export class ConditionalTypeNodeParser implements SubNodeParser { } } - /** - * Unwraps a type if necessary. - * - * @param type - The type to unwrap - * @return The unwrapped type. - */ - private unwrapType(type: BaseType): BaseType { - if (type instanceof AliasType || type instanceof DefinitionType || type instanceof AnnotatedType) { - return this.unwrapType(type.getType()); - } - return type; - } - /** * Returns all properties of the given type and its base types (if any). * @@ -83,7 +68,7 @@ export class ConditionalTypeNodeParser implements SubNodeParser { * @return The object properties. May be empty if no objects are present or type is not an object type. */ private getObjectProperties(type: BaseType): ObjectProperty[] { - type = this.unwrapType(type); + type = derefType(type); const properties: ObjectProperty[] = []; if (type instanceof ObjectType) { properties.push(...type.getProperties()); @@ -102,8 +87,8 @@ export class ConditionalTypeNodeParser implements SubNodeParser { * @return True if source type is assignable to target type. */ private isAssignableFrom(target: BaseType, source: BaseType): boolean { - source = this.unwrapType(source); - target = this.unwrapType(target); + source = derefType(source); + target = derefType(target); // If type IDs matches or target is any type then source can be assigned to target if (target.getId() === source.getId() || target instanceof AnyType) { @@ -183,7 +168,7 @@ export class ConditionalTypeNodeParser implements SubNodeParser { // Check compatibility to base types for (const baseType of target.getBaseTypes()) { - const resolved = this.unwrapType(baseType); + const resolved = derefType(baseType); if (resolved instanceof ObjectType) { if (!this.isCompatibleTo(resolved, source)) { return false; From b9a5a5e3216efce259cc41fb650c510bcee8fd65 Mon Sep 17 00:00:00 2001 From: Klaus Reimer Date: Wed, 5 Jun 2019 08:20:13 +0200 Subject: [PATCH 08/25] Remove accidentally commited tasks.json --- .vscode/tasks.json | 31 ------------------------------- 1 file changed, 31 deletions(-) delete mode 100644 .vscode/tasks.json diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index f28ef924f..000000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "label": "Build (and watch) project", - "type": "npm", - "script": "watch", - "isBackground": true, - "presentation": { - "reveal": "never" - }, - "problemMatcher": [ - "$tsc-watch" - ], - "group": { - "kind": "build", - "isDefault": true - } - }, - { - "label": "Run unit tests", - "type": "npm", - "script": "test", - "problemMatcher": [], - "group": { - "kind": "test", - "isDefault": true - } - } - ] -} From 31816a47d1b67386ac68e3e93a858ae7d08a43f9 Mon Sep 17 00:00:00 2001 From: Klaus Reimer Date: Fri, 7 Jun 2019 11:44:29 +0200 Subject: [PATCH 09/25] Split types "any" and "unknown" This is needed because for isAssignableTo checks these types works differently --- factory/formatter.ts | 2 ++ factory/parser.ts | 3 ++- src/NodeParser/AnyTypeNodeParser.ts | 2 +- src/NodeParser/UnknownTypeNodeParser.ts | 14 ++++++++++++++ src/Type/UnknownType.ts | 7 +++++++ src/TypeFormatter/NeverTypeFormatter.ts | 16 ++++++++++++++++ src/TypeFormatter/UnknownTypeFormatter.ts | 16 ++++++++++++++++ 7 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 src/NodeParser/UnknownTypeNodeParser.ts create mode 100644 src/Type/UnknownType.ts create mode 100644 src/TypeFormatter/NeverTypeFormatter.ts create mode 100644 src/TypeFormatter/UnknownTypeFormatter.ts diff --git a/factory/formatter.ts b/factory/formatter.ts index faaa3f589..ac63b92bd 100644 --- a/factory/formatter.ts +++ b/factory/formatter.ts @@ -23,6 +23,7 @@ import { StringTypeFormatter } from "../src/TypeFormatter/StringTypeFormatter"; import { TupleTypeFormatter } from "../src/TypeFormatter/TupleTypeFormatter"; import { UndefinedTypeFormatter } from "../src/TypeFormatter/UndefinedTypeFormatter"; import { UnionTypeFormatter } from "../src/TypeFormatter/UnionTypeFormatter"; +import { UnknownTypeFormatter } from "../src/TypeFormatter/UnknownTypeFormatter"; @@ -40,6 +41,7 @@ export function createFormatter(config: Config): TypeFormatter { .addTypeFormatter(new AnyTypeFormatter()) .addTypeFormatter(new UndefinedTypeFormatter()) + .addTypeFormatter(new UnknownTypeFormatter()) .addTypeFormatter(new LiteralTypeFormatter()) .addTypeFormatter(new EnumTypeFormatter()) diff --git a/factory/parser.ts b/factory/parser.ts index f7cf247b3..bf24ca0a5 100644 --- a/factory/parser.ts +++ b/factory/parser.ts @@ -38,10 +38,10 @@ import { TypeOperatorNodeParser } from "../src/NodeParser/TypeOperatorNodeParser import { TypeReferenceNodeParser } from "../src/NodeParser/TypeReferenceNodeParser"; import { UndefinedTypeNodeParser } from "../src/NodeParser/UndefinedTypeNodeParser"; import { UnionNodeParser } from "../src/NodeParser/UnionNodeParser"; +import { UnknownTypeNodeParser } from "../src/NodeParser/UnknownTypeNodeParser"; import { SubNodeParser } from "../src/SubNodeParser"; import { TopRefNodeParser } from "../src/TopRefNodeParser"; - export function createParser(program: ts.Program, config: Config): NodeParser { const typeChecker = program.getTypeChecker(); const chainNodeParser = new ChainNodeParser(typeChecker, []); @@ -72,6 +72,7 @@ export function createParser(program: ts.Program, config: Config): NodeParser { .addNodeParser(new NumberTypeNodeParser()) .addNodeParser(new BooleanTypeNodeParser()) .addNodeParser(new AnyTypeNodeParser()) + .addNodeParser(new UnknownTypeNodeParser()) .addNodeParser(new UndefinedTypeNodeParser()) .addNodeParser(new NeverTypeNodeParser()) .addNodeParser(new ObjectTypeNodeParser()) diff --git a/src/NodeParser/AnyTypeNodeParser.ts b/src/NodeParser/AnyTypeNodeParser.ts index f15707304..d99327743 100644 --- a/src/NodeParser/AnyTypeNodeParser.ts +++ b/src/NodeParser/AnyTypeNodeParser.ts @@ -6,7 +6,7 @@ import { BaseType } from "../Type/BaseType"; export class AnyTypeNodeParser implements SubNodeParser { public supportsNode(node: ts.KeywordTypeNode): boolean { - return node.kind === ts.SyntaxKind.AnyKeyword || node.kind === ts.SyntaxKind.UnknownKeyword; + return node.kind === ts.SyntaxKind.AnyKeyword; } public createType(node: ts.KeywordTypeNode, context: Context): BaseType { return new AnyType(); diff --git a/src/NodeParser/UnknownTypeNodeParser.ts b/src/NodeParser/UnknownTypeNodeParser.ts new file mode 100644 index 000000000..1dd97be2a --- /dev/null +++ b/src/NodeParser/UnknownTypeNodeParser.ts @@ -0,0 +1,14 @@ +import * as ts from "typescript"; +import { Context } from "../NodeParser"; +import { SubNodeParser } from "../SubNodeParser"; +import { BaseType } from "../Type/BaseType"; +import { UnknownType } from "../Type/UnknownType"; + +export class UnknownTypeNodeParser implements SubNodeParser { + public supportsNode(node: ts.KeywordTypeNode): boolean { + return node.kind === ts.SyntaxKind.UnknownKeyword; + } + public createType(node: ts.KeywordTypeNode, context: Context): BaseType { + return new UnknownType(); + } +} diff --git a/src/Type/UnknownType.ts b/src/Type/UnknownType.ts new file mode 100644 index 000000000..24967695b --- /dev/null +++ b/src/Type/UnknownType.ts @@ -0,0 +1,7 @@ +import { BaseType } from "./BaseType"; + +export class UnknownType extends BaseType { + public getId(): string { + return "unknown"; + } +} diff --git a/src/TypeFormatter/NeverTypeFormatter.ts b/src/TypeFormatter/NeverTypeFormatter.ts new file mode 100644 index 000000000..3b5e13cf9 --- /dev/null +++ b/src/TypeFormatter/NeverTypeFormatter.ts @@ -0,0 +1,16 @@ +import { Definition } from "../Schema/Definition"; +import { SubTypeFormatter } from "../SubTypeFormatter"; +import { BaseType } from "../Type/BaseType"; +import { NeverType } from "../Type/NeverType"; + +export class NeverTypeFormatter implements SubTypeFormatter { + public supportsType(type: NeverType): boolean { + return type instanceof NeverType; + } + public getDefinition(type: NeverType): Definition { + return {not: {}}; + } + public getChildren(type: NeverType): BaseType[] { + return []; + } +} diff --git a/src/TypeFormatter/UnknownTypeFormatter.ts b/src/TypeFormatter/UnknownTypeFormatter.ts new file mode 100644 index 000000000..444b9f00b --- /dev/null +++ b/src/TypeFormatter/UnknownTypeFormatter.ts @@ -0,0 +1,16 @@ +import { Definition } from "../Schema/Definition"; +import { SubTypeFormatter } from "../SubTypeFormatter"; +import { BaseType } from "../Type/BaseType"; +import { UnknownType } from "../Type/UnknownType"; + +export class UnknownTypeFormatter implements SubTypeFormatter { + public supportsType(type: UnknownType): boolean { + return type instanceof UnknownType; + } + public getDefinition(type: UnknownType): Definition { + return {}; + } + public getChildren(type: UnknownType): BaseType[] { + return []; + } +} From e0089ed79aef10b2d580600962b6af61480c0f6e Mon Sep 17 00:00:00 2001 From: Klaus Reimer Date: Fri, 7 Jun 2019 11:45:28 +0200 Subject: [PATCH 10/25] Add NeverTypeFormatter in case this type is exposed --- factory/formatter.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/factory/formatter.ts b/factory/formatter.ts index ac63b92bd..c288616fa 100644 --- a/factory/formatter.ts +++ b/factory/formatter.ts @@ -12,6 +12,7 @@ import { EnumTypeFormatter } from "../src/TypeFormatter/EnumTypeFormatter"; import { IntersectionTypeFormatter } from "../src/TypeFormatter/IntersectionTypeFormatter"; import { LiteralTypeFormatter } from "../src/TypeFormatter/LiteralTypeFormatter"; import { LiteralUnionTypeFormatter } from "../src/TypeFormatter/LiteralUnionTypeFormatter"; +import { NeverTypeFormatter } from "../src/TypeFormatter/NeverTypeFormatter"; import { NullTypeFormatter } from "../src/TypeFormatter/NullTypeFormatter"; import { NumberTypeFormatter } from "../src/TypeFormatter/NumberTypeFormatter"; import { ObjectTypeFormatter } from "../src/TypeFormatter/ObjectTypeFormatter"; @@ -40,6 +41,7 @@ export function createFormatter(config: Config): TypeFormatter { .addTypeFormatter(new NullTypeFormatter()) .addTypeFormatter(new AnyTypeFormatter()) + .addTypeFormatter(new NeverTypeFormatter()) .addTypeFormatter(new UndefinedTypeFormatter()) .addTypeFormatter(new UnknownTypeFormatter()) From 10c71ae8733ea92b3b9cad379fec6774cb285159 Mon Sep 17 00:00:00 2001 From: Klaus Reimer Date: Fri, 7 Jun 2019 11:45:52 +0200 Subject: [PATCH 11/25] Add new classes to index --- index.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/index.ts b/index.ts index 1bf693ece..493e59cb6 100644 --- a/index.ts +++ b/index.ts @@ -45,8 +45,10 @@ export * from "./src/SubTypeFormatter"; export * from "./src/ChainTypeFormatter"; export * from "./src/CircularReferenceTypeFormatter"; export * from "./src/TypeFormatter/AnyTypeFormatter"; +export * from "./src/TypeFormatter/UnknownTypeFormatter"; export * from "./src/TypeFormatter/NullTypeFormatter"; export * from "./src/TypeFormatter/UndefinedTypeFormatter"; +export * from "./src/TypeFormatter/NeverTypeFormatter"; export * from "./src/TypeFormatter/BooleanTypeFormatter"; export * from "./src/TypeFormatter/NumberTypeFormatter"; export * from "./src/TypeFormatter/StringTypeFormatter"; @@ -71,9 +73,11 @@ export * from "./src/ExposeNodeParser"; export * from "./src/TopRefNodeParser"; export * from "./src/CircularReferenceNodeParser"; export * from "./src/NodeParser/AnyTypeNodeParser"; +export * from "./src/NodeParser/UnknownTypeNodeParser"; export * from "./src/NodeParser/LiteralNodeParser"; export * from "./src/NodeParser/NullLiteralNodeParser"; export * from "./src/NodeParser/UndefinedTypeNodeParser"; +export * from "./src/NodeParser/NeverTypeNodeParser"; export * from "./src/NodeParser/NumberLiteralNodeParser"; export * from "./src/NodeParser/StringLiteralNodeParser"; export * from "./src/NodeParser/BooleanLiteralNodeParser"; @@ -93,6 +97,7 @@ export * from "./src/NodeParser/UnionNodeParser"; export * from "./src/NodeParser/TupleNodeParser"; export * from "./src/NodeParser/AnnotatedNodeParser"; export * from "./src/NodeParser/CallExpressionParser"; +export * from "./src/NodeParser/ConditionalTypeNodeParser"; export * from "./src/SchemaGenerator"; From 81027bc33eb1f8311b44b320d3137ccc68bcebcf Mon Sep 17 00:00:00 2001 From: Klaus Reimer Date: Fri, 7 Jun 2019 11:46:14 +0200 Subject: [PATCH 12/25] Move isAssignable check to utility function --- src/NodeParser/ConditionalTypeNodeParser.ts | 127 +-------- src/Utils/isAssignableTo.ts | 176 ++++++++++++ test/unit/isAssignableTo.test.ts | 294 ++++++++++++++++++++ 3 files changed, 472 insertions(+), 125 deletions(-) create mode 100644 src/Utils/isAssignableTo.ts create mode 100644 test/unit/isAssignableTo.test.ts diff --git a/src/NodeParser/ConditionalTypeNodeParser.ts b/src/NodeParser/ConditionalTypeNodeParser.ts index 504de6658..93de03a5c 100644 --- a/src/NodeParser/ConditionalTypeNodeParser.ts +++ b/src/NodeParser/ConditionalTypeNodeParser.ts @@ -1,13 +1,11 @@ import * as ts from "typescript"; import { Context, NodeParser } from "../NodeParser"; import { SubNodeParser } from "../SubNodeParser"; -import { AnyType } from "../Type/AnyType"; import { BaseType } from "../Type/BaseType"; -import { IntersectionType } from "../Type/IntersectionType"; import { NeverType } from "../Type/NeverType"; -import { ObjectProperty, ObjectType } from "../Type/ObjectType"; import { UnionType } from "../Type/UnionType"; import { derefType } from "../Utils/derefType"; +import { isAssignableTo } from "../Utils/isAssignableTo"; export class ConditionalTypeNodeParser implements SubNodeParser { public constructor( @@ -31,7 +29,7 @@ export class ConditionalTypeNodeParser implements SubNodeParser { // Process each part of the check type separately const resultTypes: BaseType[] = []; for (const type of checkTypes) { - const resultType = this.isAssignableFrom(extendsType, type) + const resultType = isAssignableTo(extendsType, type) ? this.childNodeParser.createType(node.trueType, context) : this.childNodeParser.createType(node.falseType, context); const unwrappedResultType = derefType(resultType); @@ -60,125 +58,4 @@ export class ConditionalTypeNodeParser implements SubNodeParser { return new UnionType(resultTypes); } } - - /** - * Returns all properties of the given type and its base types (if any). - * - * @param type - The type for which to return the properties. - * @return The object properties. May be empty if no objects are present or type is not an object type. - */ - private getObjectProperties(type: BaseType): ObjectProperty[] { - type = derefType(type); - const properties: ObjectProperty[] = []; - if (type instanceof ObjectType) { - properties.push(...type.getProperties()); - for (const baseType of type.getBaseTypes()) { - properties.push(...this.getObjectProperties(baseType)); - } - } - return properties; - } - - /** - * Checks if given source type is assignable to given target type. - * - * @param target - The target type. - * @param source - The source type. - * @return True if source type is assignable to target type. - */ - private isAssignableFrom(target: BaseType, source: BaseType): boolean { - source = derefType(source); - target = derefType(target); - - // If type IDs matches or target is any type then source can be assigned to target - if (target.getId() === source.getId() || target instanceof AnyType) { - return true; - } - - // When target is a union type then check if source type can be assigned to any of it - if (target instanceof UnionType) { - return target.getTypes().some(type => this.isAssignableFrom(type, source)); - } - - // When target is an intersection type then check if source type can be assigned to all of them - if (target instanceof IntersectionType) { - return target.getTypes().every(type => this.isAssignableFrom(type, source)); - } - - // When source is an intersection type then check if at least one of the intersect types can be assigned to - // target type - if (source instanceof IntersectionType) { - return source.getTypes().some(type => this.isAssignableFrom(target, type)); - } - - // If source type and target type is an object type then check inheritance - if (source instanceof ObjectType && target instanceof ObjectType) { - // First do a quick base type check. Maybe their IDs already match so we don't have to compare properties - if (source.getBaseTypes().some(type => this.isAssignableFrom(target, type))) { - return true; - } - - // Perform a full object compatibility check - return this.isCompatibleTo(target, source); - } - return false; - } - - /** - * Checks if the given source type is compatible to given target type but comparing their properties. - * - * @param target - The target object type. - * @param source - The source object type. - * @return True if source is compatible to target, false if not. - */ - private isCompatibleTo(target: ObjectType, source: ObjectType): boolean { - // Check property compatibility - const sourceProperties = this.getObjectProperties(source); - for (const targetProperty of target.getProperties()) { - const sourceProperty = sourceProperties.find(property => property.getName() === targetProperty.getName()); - if (sourceProperty) { - // If source property with same name as in target property exists then compare its types - if (!this.isAssignableFrom(targetProperty.getType(), sourceProperty.getType())) { - return false; - } - } else { - // If source has no such property but property is required then types are not compatible - if (targetProperty.isRequired()) { - return false; - } - } - } - - // Check additional properties compatibility - const targetAdditionalPropertyType = target.getAdditionalProperties(); - const sourceAdditionalProperties = source.getAdditionalProperties(); - if (typeof targetAdditionalPropertyType === "boolean") { - if (sourceAdditionalProperties !== targetAdditionalPropertyType) { - return false; - } - } else { - if (typeof sourceAdditionalProperties === "boolean") { - return false; - } else { - if (!this.isAssignableFrom(targetAdditionalPropertyType, sourceAdditionalProperties)) { - return false; - } - } - } - - // Check compatibility to base types - for (const baseType of target.getBaseTypes()) { - const resolved = derefType(baseType); - if (resolved instanceof ObjectType) { - if (!this.isCompatibleTo(resolved, source)) { - return false; - } - } else { - return false; - } - } - - // Looks like types are compatible - return true; - } } diff --git a/src/Utils/isAssignableTo.ts b/src/Utils/isAssignableTo.ts new file mode 100644 index 000000000..eff90a35a --- /dev/null +++ b/src/Utils/isAssignableTo.ts @@ -0,0 +1,176 @@ +import { AnyType } from "../Type/AnyType"; +import { ArrayType } from "../Type/ArrayType"; +import { BaseType } from "../Type/BaseType"; +import { IntersectionType } from "../Type/IntersectionType"; +import { NeverType } from "../Type/NeverType"; +import { NullType } from "../Type/NullType"; +import { ObjectProperty, ObjectType } from "../Type/ObjectType"; +import { OptionalType } from "../Type/OptionalType"; +import { TupleType } from "../Type/TupleType"; +import { UndefinedType } from "../Type/UndefinedType"; +import { UnionType } from "../Type/UnionType"; +import { UnknownType } from "../Type/UnknownType"; +import { derefType } from "./derefType"; + +/** + * Returns the combined types from the given intersection. Currently only object types are combined. Maybe more + * types needs to be combined to properly support complex intersections. + * + * @param intersection - The intersection type to combine. + * @return The combined types within the intersection. + */ +function combineIntersectingTypes(intersection: IntersectionType): BaseType[] { + const objectTypes: ObjectType[] = []; + const combined = intersection.getTypes().filter(type => { + if (type instanceof ObjectType) { + objectTypes.push(type); + } else { + return true; + } + return false; + }); + if (objectTypes.length === 1) { + combined.push(objectTypes[0]); + } else if (objectTypes.length > 1) { + combined.push(new ObjectType("combined-objects-" + intersection.getId(), objectTypes, [], false)); + } + return combined; +} + +/** + * Returns all object properties of the given type and all its base types. + * + * @param type - The type for which to return the properties. If type is not an object type or object has no properties + * Then an empty list ist returned. + * @return All object properties of the type. Empty if none. + */ +function getObjectProperties(type: BaseType): ObjectProperty[] { + type = derefType(type); + const properties = []; + if (type instanceof ObjectType) { + properties.push(...type.getProperties()); + for (const baseType of type.getBaseTypes()) { + properties.push(...getObjectProperties(baseType)); + } + } + return properties; +} + +/** + * Checks if given source type is assignable to given target type. + * + * @param source - The source type. + * @param target - The target type. + * @return True if source type is assignable to target type. + */ +export function isAssignableTo(target: BaseType, source: BaseType): boolean { + // Dereference source and target + source = derefType(source); + target = derefType(target); + + // Check for simple type equality + if (source.getId() === target.getId()) { + return true; + } + + // Nothing can be assigned to never-type + if (target instanceof NeverType) { + return false; + } + + // Assigning from or to any-type is always possible + if (source instanceof AnyType || target instanceof AnyType) { + return true; + } + + // assigning to unknown type is always possible + if (target instanceof UnknownType) { + return true; + } + + // Type "never" can be assigned to anything + if (source instanceof NeverType) { + return true; + } + + // Union type is assignable to target when all types in the union are assignable to it + if (source instanceof UnionType) { + return source.getTypes().every(type => isAssignableTo(target, type)); + } + + // When source is an intersection type then it can be assigned to target if any of the sub types matches. Object + // types within the intersection must be combined first + if (source instanceof IntersectionType) { + return combineIntersectingTypes(source).some(type => isAssignableTo(target, type)); + } + + // For arrays check if item types are assignable + if (target instanceof ArrayType) { + const targetItemType = target.getItem(); + if (source instanceof ArrayType) { + return isAssignableTo(targetItemType, source.getItem()); + } else if (source instanceof TupleType) { + return source.getTypes().every(type => isAssignableTo(targetItemType, type)); + } else { + return false; + } + } + + // When target is a union type then check if source type can be assigned to any variant + if (target instanceof UnionType) { + return target.getTypes().some(type => isAssignableTo(type, source)); + } + + // When target is an intersection type then source can be assigned to it if it matches all sub types. Object + // types within the intersection must be combined first + if (target instanceof IntersectionType) { + return combineIntersectingTypes(target).every(type => isAssignableTo(type, source)); + } + + if (target instanceof ObjectType) { + const membersA = getObjectProperties(target); + if (membersA.length === 0) { + // When target object is empty then anything except null and undefined can be assigned to it + return !isAssignableTo(new UnionType([ new UndefinedType(), new NullType() ]), source); + } else if (source instanceof ObjectType) { + const membersB = getObjectProperties(source); + + // Check if target has properties in common with source + const inCommon = membersA.some(memberA => membersB.some(memberB => + memberA.getName() === memberB.getName())); + + return membersA.every(memberA => { + // Make sure that every required property in target type is present + const memberB = membersB.find(member => memberA.getName() === member.getName()); + return memberB == null ? (inCommon && !memberA.isRequired()) : true; + }) && membersB.every(memberB => { + const memberA = membersA.find(member => member.getName() === memberB.getName()); + if (memberA == null) { + return true; + } + return isAssignableTo(memberA.getType(), memberB.getType()); + }); + } + } + + // Check if tuple types are compatible + if (target instanceof TupleType) { + if (source instanceof TupleType) { + const membersB = source.getTypes(); + return target.getTypes().every((memberA, i) => { + const memberB = membersB[i]; + if (memberA instanceof OptionalType) { + if (memberB) { + return isAssignableTo(memberA, memberB) || isAssignableTo(memberA.getType(), memberB); + } else { + return true; + } + } else { + return isAssignableTo(memberA, memberB); + } + }); + } + } + + return false; +} diff --git a/test/unit/isAssignableTo.test.ts b/test/unit/isAssignableTo.test.ts new file mode 100644 index 000000000..93b95a38c --- /dev/null +++ b/test/unit/isAssignableTo.test.ts @@ -0,0 +1,294 @@ +import { AliasType } from "../../src/Type/AliasType"; +import { AnnotatedType } from "../../src/Type/AnnotatedType"; +import { AnyType } from "../../src/Type/AnyType"; +import { ArrayType } from "../../src/Type/ArrayType"; +import { BooleanType } from "../../src/Type/BooleanType"; +import { DefinitionType } from "../../src/Type/DefinitionType"; +import { IntersectionType } from "../../src/Type/IntersectionType"; +import { LiteralType } from "../../src/Type/LiteralType"; +import { NeverType } from "../../src/Type/NeverType"; +import { NullType } from "../../src/Type/NullType"; +import { NumberType } from "../../src/Type/NumberType"; +import { ObjectProperty, ObjectType } from "../../src/Type/ObjectType"; +import { OptionalType } from "../../src/Type/OptionalType"; +import { ReferenceType } from "../../src/Type/ReferenceType"; +import { StringType } from "../../src/Type/StringType"; +import { TupleType } from "../../src/Type/TupleType"; +import { UndefinedType } from "../../src/Type/UndefinedType"; +import { UnionType } from "../../src/Type/UnionType"; +import { UnknownType } from "../../src/Type/UnknownType"; +import { isAssignableTo } from "../../src/Utils/isAssignableTo"; + +describe("isAssignableTo", () => { + it("returns true for same types", () => { + expect(isAssignableTo(new BooleanType(), new BooleanType())).toBe(true); + expect(isAssignableTo(new NullType(), new NullType())).toBe(true); + expect(isAssignableTo(new NumberType(), new NumberType())).toBe(true); + expect(isAssignableTo(new BooleanType(), new BooleanType())).toBe(true); + expect(isAssignableTo(new StringType(), new StringType())).toBe(true); + expect(isAssignableTo(new UndefinedType(), new UndefinedType())).toBe(true); + }); + it("returns false for different types", () => { + expect(isAssignableTo(new BooleanType(), new NullType())).toBe(false); + expect(isAssignableTo(new NullType(), new NumberType())).toBe(false); + expect(isAssignableTo(new NumberType(), new BooleanType())).toBe(false); + expect(isAssignableTo(new BooleanType(), new StringType())).toBe(false); + expect(isAssignableTo(new StringType(), new UndefinedType())).toBe(false); + expect(isAssignableTo(new UndefinedType(), new BooleanType())).toBe(false); + expect(isAssignableTo(new ArrayType(new StringType()), new StringType())).toBe(false); + }); + it("returns true for arrays with same item type", () => { + expect(isAssignableTo(new ArrayType(new StringType()), new ArrayType(new StringType()))).toBe(true); + }); + it("returns false when array item types do not match", () => { + expect(isAssignableTo(new ArrayType(new StringType()), new ArrayType(new NumberType()))).toBe(false); + }); + it("returns true when source type is compatible to target union type", () => { + const union = new UnionType([ + new StringType(), + new NumberType(), + ]); + expect(isAssignableTo(union, new StringType())).toBe(true); + expect(isAssignableTo(union, new NumberType())).toBe(true); + }); + it("returns false when source type is not compatible to target union type", () => { + const union = new UnionType([ + new StringType(), + new NumberType(), + ]); + expect(isAssignableTo(union, new BooleanType())).toBe(false); + }); + it("derefs reference types", () => { + const stringRef = new ReferenceType(); + stringRef.setType(new StringType()); + const anotherStringRef = new ReferenceType(); + anotherStringRef.setType(new StringType()); + const numberRef = new ReferenceType(); + numberRef.setType(new NumberType()); + expect(isAssignableTo(stringRef, new StringType())).toBe(true); + expect(isAssignableTo(stringRef, new NumberType())).toBe(false); + expect(isAssignableTo(new StringType(), stringRef)).toBe(true); + expect(isAssignableTo(new NumberType(), stringRef)).toBe(false); + expect(isAssignableTo(stringRef, anotherStringRef)).toBe(true); + expect(isAssignableTo(numberRef, stringRef)).toBe(false); + }); + it("derefs alias types", () => { + const stringAlias = new AliasType("a", new StringType()); + const anotherStringAlias = new AliasType("b", new StringType()); + const numberAlias = new AliasType("c", new NumberType()); + expect(isAssignableTo(stringAlias, new StringType())).toBe(true); + expect(isAssignableTo(stringAlias, new NumberType())).toBe(false); + expect(isAssignableTo(new StringType(), stringAlias)).toBe(true); + expect(isAssignableTo(new NumberType(), stringAlias)).toBe(false); + expect(isAssignableTo(stringAlias, anotherStringAlias)).toBe(true); + expect(isAssignableTo(numberAlias, stringAlias)).toBe(false); + }); + it("derefs annotated types", () => { + const annotatedString = new AnnotatedType(new StringType(), {}, false); + const anotherAnnotatedString = new AnnotatedType(new StringType(), {}, false); + const annotatedNumber = new AnnotatedType(new NumberType(), {}, false); + expect(isAssignableTo(annotatedString, new StringType())).toBe(true); + expect(isAssignableTo(annotatedString, new NumberType())).toBe(false); + expect(isAssignableTo(new StringType(), annotatedString)).toBe(true); + expect(isAssignableTo(new NumberType(), annotatedString)).toBe(false); + expect(isAssignableTo(annotatedString, anotherAnnotatedString)).toBe(true); + expect(isAssignableTo(annotatedNumber, annotatedString)).toBe(false); + }); + it("derefs definition types", () => { + const stringDefinition = new DefinitionType("a", new StringType()); + const anotherStringDefinition = new DefinitionType("b", new StringType()); + const numberDefinition = new DefinitionType("c", new NumberType()); + expect(isAssignableTo(stringDefinition, new StringType())).toBe(true); + expect(isAssignableTo(stringDefinition, new NumberType())).toBe(false); + expect(isAssignableTo(new StringType(), stringDefinition)).toBe(true); + expect(isAssignableTo(new NumberType(), stringDefinition)).toBe(false); + expect(isAssignableTo(stringDefinition, anotherStringDefinition)).toBe(true); + expect(isAssignableTo(numberDefinition, stringDefinition)).toBe(false); + }); + it("lets type 'any' to be assigned to anything except 'never'", () => { + expect(isAssignableTo(new AnyType(), new AnyType())).toBe(true); + expect(isAssignableTo(new ArrayType(new NumberType()), new AnyType())).toBe(true); + expect(isAssignableTo(new IntersectionType([ new StringType(), new NullType() ]), new AnyType())).toBe(true); + expect(isAssignableTo(new LiteralType("literal"), new AnyType())).toBe(true); + expect(isAssignableTo(new NeverType(), new AnyType())).toBe(false); + expect(isAssignableTo(new NullType(), new AnyType())).toBe(true); + expect(isAssignableTo(new ObjectType("obj", [], [ new ObjectProperty("foo", new StringType(), true) ], true), + new AnyType())).toBe(true); + expect(isAssignableTo(new BooleanType(), new AnyType())).toBe(true); + expect(isAssignableTo(new NumberType(), new AnyType())).toBe(true); + expect(isAssignableTo(new BooleanType(), new AnyType())).toBe(true); + expect(isAssignableTo(new StringType(), new AnyType())).toBe(true); + expect(isAssignableTo(new TupleType([new StringType(), new NumberType() ]), new AnyType())).toBe(true); + expect(isAssignableTo(new UndefinedType(), new AnyType())).toBe(true); + }); + it("lets type 'never' to be assigned to anything", () => { + expect(isAssignableTo(new AnyType(), new NeverType())).toBe(true); + expect(isAssignableTo(new ArrayType(new NumberType()), new NeverType())).toBe(true); + expect(isAssignableTo(new IntersectionType([ new StringType(), new NullType() ]), new NeverType())).toBe(true); + expect(isAssignableTo(new LiteralType("literal"), new NeverType())).toBe(true); + expect(isAssignableTo(new NeverType(), new NeverType())).toBe(true); + expect(isAssignableTo(new NullType(), new NeverType())).toBe(true); + expect(isAssignableTo(new ObjectType("obj", [], [ new ObjectProperty("foo", new StringType(), true) ], true), + new NeverType())).toBe(true); + expect(isAssignableTo(new BooleanType(), new NeverType())).toBe(true); + expect(isAssignableTo(new NumberType(), new NeverType())).toBe(true); + expect(isAssignableTo(new BooleanType(), new NeverType())).toBe(true); + expect(isAssignableTo(new StringType(), new NeverType())).toBe(true); + expect(isAssignableTo(new TupleType([new StringType(), new NumberType() ]), new NeverType())).toBe(true); + expect(isAssignableTo(new UndefinedType(), new NeverType())).toBe(true); + }); + it("lets anything to be assigned to type 'any'", () => { + expect(isAssignableTo(new AnyType(), new AnyType())).toBe(true); + expect(isAssignableTo(new AnyType(), new ArrayType(new NumberType()))).toBe(true); + expect(isAssignableTo(new AnyType(), new IntersectionType([ new StringType(), new NullType() ]))).toBe(true); + expect(isAssignableTo(new AnyType(), new LiteralType("literal"))).toBe(true); + expect(isAssignableTo(new AnyType(), new NeverType())).toBe(true); + expect(isAssignableTo(new AnyType(), new NullType())).toBe(true); + expect(isAssignableTo(new AnyType(), + new ObjectType("obj", [], [ new ObjectProperty("foo", new StringType(), true) ], true))).toBe(true); + expect(isAssignableTo(new AnyType(), new BooleanType())).toBe(true); + expect(isAssignableTo(new AnyType(), new NumberType())).toBe(true); + expect(isAssignableTo(new AnyType(), new BooleanType())).toBe(true); + expect(isAssignableTo(new AnyType(), new StringType())).toBe(true); + expect(isAssignableTo(new AnyType(), new TupleType([new StringType(), new NumberType() ]))).toBe(true); + expect(isAssignableTo(new AnyType(), new UndefinedType())).toBe(true); + }); + it("lets anything to be assigned to type 'unknown'", () => { + expect(isAssignableTo(new UnknownType(), new AnyType())).toBe(true); + expect(isAssignableTo(new UnknownType(), new ArrayType(new NumberType()))).toBe(true); + expect(isAssignableTo(new UnknownType(), + new IntersectionType([ new StringType(), new NullType() ]))).toBe(true); + expect(isAssignableTo(new UnknownType(), new LiteralType("literal"))).toBe(true); + expect(isAssignableTo(new UnknownType(), new NeverType())).toBe(true); + expect(isAssignableTo(new UnknownType(), new NullType())).toBe(true); + expect(isAssignableTo(new UnknownType(), + new ObjectType("obj", [], [ new ObjectProperty("foo", new StringType(), true) ], true))).toBe(true); + expect(isAssignableTo(new UnknownType(), new BooleanType())).toBe(true); + expect(isAssignableTo(new UnknownType(), new NumberType())).toBe(true); + expect(isAssignableTo(new UnknownType(), new BooleanType())).toBe(true); + expect(isAssignableTo(new UnknownType(), new StringType())).toBe(true); + expect(isAssignableTo(new UnknownType(), new TupleType([new StringType(), new NumberType() ]))).toBe(true); + expect(isAssignableTo(new UnknownType(), new UndefinedType())).toBe(true); + }); + it("lets 'unknown' only to be assigned to type 'unknown' or 'any'", () => { + expect(isAssignableTo(new AnyType(), new UnknownType())).toBe(true); + expect(isAssignableTo(new ArrayType(new NumberType()), new UnknownType())).toBe(false); + expect(isAssignableTo(new IntersectionType([ new StringType(), new NullType() ]), + new UnknownType())).toBe(false); + expect(isAssignableTo(new LiteralType("literal"), new UnknownType())).toBe(false); + expect(isAssignableTo(new NeverType(), new UnknownType())).toBe(false); + expect(isAssignableTo(new NullType(), new UnknownType())).toBe(false); + expect(isAssignableTo(new UnknownType(), new UnknownType())).toBe(true); + expect(isAssignableTo(new ObjectType("obj", [], [ new ObjectProperty("foo", new StringType(), true) ], false), + new UnknownType())).toBe(false); + expect(isAssignableTo(new BooleanType(), new UnknownType())).toBe(false); + expect(isAssignableTo(new NumberType(), new UnknownType())).toBe(false); + expect(isAssignableTo(new BooleanType(), new UnknownType())).toBe(false); + expect(isAssignableTo(new StringType(), new UnknownType())).toBe(false); + expect(isAssignableTo(new TupleType([new StringType(), new NumberType() ]), new UnknownType())).toBe(false); + expect(isAssignableTo(new UndefinedType(), new UnknownType())).toBe(false); + }); + it("lets union type to be assigned if all sub types are compatible to target type", () => { + const typeA = new ObjectType("a", [], [ new ObjectProperty("a", new StringType(), true) ], true); + const typeB = new ObjectType("b", [], [ new ObjectProperty("b", new StringType(), true) ], true); + const typeC = new ObjectType("c", [], [ new ObjectProperty("c", new StringType(), true) ], true); + const typeAB = new ObjectType("ab", [ typeA, typeB ], [], true); + const typeAorB = new UnionType([ typeA, typeB ]); + expect(isAssignableTo(typeAB, new UnionType([ typeA, typeA ]))).toBe(false); + expect(isAssignableTo(typeAB, new UnionType([ typeB, typeB ]))).toBe(false); + expect(isAssignableTo(typeAB, new UnionType([ typeA, typeB ]))).toBe(false); + expect(isAssignableTo(typeAB, new UnionType([ typeB, typeA ]))).toBe(false); + expect(isAssignableTo(typeAB, new UnionType([ typeB, typeA, typeC ]))).toBe(false); + expect(isAssignableTo(typeAorB, new UnionType([ typeB, typeA ]))).toBe(true); + expect(isAssignableTo(typeAorB, new UnionType([ typeA, typeB ]))).toBe(true); + expect(isAssignableTo(typeAorB, new UnionType([ typeAB, typeB, typeC ]))).toBe(false); + }); + it("lets tuple type to be assigned to array type if item types match", () => { + expect(isAssignableTo(new ArrayType(new StringType()), new TupleType([ new StringType(), new StringType() ]))) + .toBe(true); + expect(isAssignableTo(new ArrayType(new NumberType()), new TupleType([ new StringType(), new StringType() ]))) + .toBe(false); + expect(isAssignableTo(new ArrayType(new StringType()), new TupleType([ new StringType(), new NumberType() ]))) + .toBe(false); + }); + it("lets only compatible tuple type to be assigned to tuple type", () => { + expect(isAssignableTo(new TupleType([ new StringType(), new StringType() ]), new ArrayType(new StringType()))) + .toBe(false); + expect(isAssignableTo(new TupleType([ new StringType(), new StringType() ]), new StringType())).toBe(false); + expect(isAssignableTo(new TupleType([ new StringType(), new StringType() ]), + new TupleType([ new StringType(), new NumberType() ]))).toBe(false); + expect(isAssignableTo(new TupleType([ new StringType(), new StringType() ]), + new TupleType([ new StringType(), new StringType() ]))).toBe(true); + expect(isAssignableTo(new TupleType([ new StringType(), new OptionalType(new StringType()) ]), + new TupleType([ new StringType() ]))).toBe(true); + expect(isAssignableTo(new TupleType([ new StringType(), new OptionalType(new StringType()) ]), + new TupleType([ new StringType(), new StringType() ]))).toBe(true); + }); + it("lets anything except null and undefined to be assigned to empty object type", () => { + const empty = new ObjectType("empty", [], [], false); + expect(isAssignableTo(empty, new AnyType())).toBe(true); + expect(isAssignableTo(empty, new ArrayType(new NumberType()))).toBe(true); + expect(isAssignableTo(empty, new IntersectionType([ new StringType(), new NullType() ]))).toBe(true); + expect(isAssignableTo(empty, new LiteralType("literal"))).toBe(true); + expect(isAssignableTo(empty, new NeverType())).toBe(true); + expect(isAssignableTo(empty, new NullType())).toBe(false); + expect(isAssignableTo(empty, + new ObjectType("obj", [], [ new ObjectProperty("foo", new StringType(), true) ], true))).toBe(true); + expect(isAssignableTo(empty, new BooleanType())).toBe(true); + expect(isAssignableTo(empty, new NumberType())).toBe(true); + expect(isAssignableTo(empty, new BooleanType())).toBe(true); + expect(isAssignableTo(empty, new StringType())).toBe(true); + expect(isAssignableTo(empty, new TupleType([new StringType(), new NumberType() ]))).toBe(true); + expect(isAssignableTo(empty, new UndefinedType())).toBe(false); + }); + it("lets only compatible object types to be assigned to object type", () => { + const typeA = new ObjectType("a", [], [ new ObjectProperty("a", new StringType(), true) ], false); + const typeB = new ObjectType("b", [], [ new ObjectProperty("b", new StringType(), true) ], false); + const typeC = new ObjectType("c", [], [ new ObjectProperty("c", new StringType(), true) ], false); + const typeAB = new ObjectType("ab", [ typeA, typeB ], [], false); + expect(isAssignableTo(typeA, new StringType())).toBe(false); + expect(isAssignableTo(typeA, typeAB)).toBe(true); + expect(isAssignableTo(typeB, typeAB)).toBe(true); + expect(isAssignableTo(typeC, typeAB)).toBe(false); + expect(isAssignableTo(typeAB, typeA)).toBe(false); + expect(isAssignableTo(typeAB, typeB)).toBe(false); + }); + it("does let object to be assigned to object with optional properties and at least one property in common", () => { + const typeA = new ObjectType("a", [], [ + new ObjectProperty("a", new StringType(), false), + new ObjectProperty("b", new StringType(), false), + ], false); + const typeB = new ObjectType("b", [], [ new ObjectProperty("b", new StringType(), false) ], false); + expect(isAssignableTo(typeB, typeA)).toBe(true); + }); + it("does not let object to be assigned to object with only optional properties and no properties in common", () => { + const typeA = new ObjectType("a", [], [ new ObjectProperty("a", new StringType(), true) ], false); + const typeB = new ObjectType("b", [], [ new ObjectProperty("b", new StringType(), false) ], false); + expect(isAssignableTo(typeB, typeA)).toBe(false); + }); + it("correctly handles primitive source intersection types", () => { + const numberAndString = new IntersectionType([ new StringType(), new NumberType() ]); + expect(isAssignableTo(new StringType(), numberAndString)).toBe(true); + expect(isAssignableTo(new NumberType(), numberAndString)).toBe(true); + expect(isAssignableTo(new BooleanType(), numberAndString)).toBe(false); + }); + it("correctly handles intersection types with objects", () => { + const a = new ObjectType("a", [], [ new ObjectProperty("a", new StringType(), true) ], false); + const b = new ObjectType("b", [], [ new ObjectProperty("b", new StringType(), true) ], false); + const c = new ObjectType("c", [], [ new ObjectProperty("c", new StringType(), true) ], false); + const ab = new ObjectType("ab", [], [ + new ObjectProperty("a", new StringType(), true), + new ObjectProperty("b", new StringType(), true), + ], false); + const aAndB = new IntersectionType([ a, b] ); + expect(isAssignableTo(a, aAndB)).toBe(true); + expect(isAssignableTo(b, aAndB)).toBe(true); + expect(isAssignableTo(c, aAndB)).toBe(false); + expect(isAssignableTo(ab, aAndB)).toBe(true); + expect(isAssignableTo(aAndB, a)).toBe(false); + expect(isAssignableTo(aAndB, b)).toBe(false); + expect(isAssignableTo(aAndB, c)).toBe(false); + expect(isAssignableTo(aAndB, ab)).toBe(true); + expect(isAssignableTo(aAndB, aAndB)).toBe(true); + }); +}); From 548488705b06305d0709e0e48c7e81efe510ba60 Mon Sep 17 00:00:00 2001 From: Klaus Reimer Date: Fri, 7 Jun 2019 11:57:31 +0200 Subject: [PATCH 13/25] Better variable names --- src/Utils/isAssignableTo.ts | 39 +++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/src/Utils/isAssignableTo.ts b/src/Utils/isAssignableTo.ts index eff90a35a..dfc216b97 100644 --- a/src/Utils/isAssignableTo.ts +++ b/src/Utils/isAssignableTo.ts @@ -128,27 +128,27 @@ export function isAssignableTo(target: BaseType, source: BaseType): boolean { } if (target instanceof ObjectType) { - const membersA = getObjectProperties(target); - if (membersA.length === 0) { + const targetMembers = getObjectProperties(target); + if (targetMembers.length === 0) { // When target object is empty then anything except null and undefined can be assigned to it return !isAssignableTo(new UnionType([ new UndefinedType(), new NullType() ]), source); } else if (source instanceof ObjectType) { - const membersB = getObjectProperties(source); + const sourceMembers = getObjectProperties(source); // Check if target has properties in common with source - const inCommon = membersA.some(memberA => membersB.some(memberB => - memberA.getName() === memberB.getName())); + const inCommon = targetMembers.some(targetMember => sourceMembers.some(sourceMember => + targetMember.getName() === sourceMember.getName())); - return membersA.every(memberA => { + return targetMembers.every(targetMember => { // Make sure that every required property in target type is present - const memberB = membersB.find(member => memberA.getName() === member.getName()); - return memberB == null ? (inCommon && !memberA.isRequired()) : true; - }) && membersB.every(memberB => { - const memberA = membersA.find(member => member.getName() === memberB.getName()); - if (memberA == null) { + const sourceMember = sourceMembers.find(member => targetMember.getName() === member.getName()); + return sourceMember == null ? (inCommon && !targetMember.isRequired()) : true; + }) && sourceMembers.every(sourceMember => { + const targetMember = targetMembers.find(member => member.getName() === sourceMember.getName()); + if (targetMember == null) { return true; } - return isAssignableTo(memberA.getType(), memberB.getType()); + return isAssignableTo(targetMember.getType(), sourceMember.getType()); }); } } @@ -156,17 +156,18 @@ export function isAssignableTo(target: BaseType, source: BaseType): boolean { // Check if tuple types are compatible if (target instanceof TupleType) { if (source instanceof TupleType) { - const membersB = source.getTypes(); - return target.getTypes().every((memberA, i) => { - const memberB = membersB[i]; - if (memberA instanceof OptionalType) { - if (memberB) { - return isAssignableTo(memberA, memberB) || isAssignableTo(memberA.getType(), memberB); + const sourceMembers = source.getTypes(); + return target.getTypes().every((targetMember, i) => { + const sourceMember = sourceMembers[i]; + if (targetMember instanceof OptionalType) { + if (sourceMember) { + return isAssignableTo(targetMember, sourceMember) || + isAssignableTo(targetMember.getType(), sourceMember); } else { return true; } } else { - return isAssignableTo(memberA, memberB); + return isAssignableTo(targetMember, sourceMember); } }); } From b27d0d9b1c69307fa3eeb6cd5d13d9b541a5e805 Mon Sep 17 00:00:00 2001 From: Klaus Reimer Date: Fri, 7 Jun 2019 13:17:44 +0200 Subject: [PATCH 14/25] Fix stack overflow in isAssignableTo for circular dependencies --- src/Utils/isAssignableTo.ts | 38 ++++++++++++++++++++------------ test/unit/isAssignableTo.test.ts | 21 ++++++++++++++++++ 2 files changed, 45 insertions(+), 14 deletions(-) diff --git a/src/Utils/isAssignableTo.ts b/src/Utils/isAssignableTo.ts index dfc216b97..51aa56827 100644 --- a/src/Utils/isAssignableTo.ts +++ b/src/Utils/isAssignableTo.ts @@ -59,11 +59,15 @@ function getObjectProperties(type: BaseType): ObjectProperty[] { /** * Checks if given source type is assignable to given target type. * - * @param source - The source type. - * @param target - The target type. + * The logic of this function is heavily inspired by + * https://github.com/runem/ts-simple-type/blob/master/src/is-assignable-to-simple-type.ts + * + * @param source - The source type. + * @param target - The target type. + * @param insideTypes - Optional parameter used internally to solve circular dependencies. * @return True if source type is assignable to target type. */ -export function isAssignableTo(target: BaseType, source: BaseType): boolean { +export function isAssignableTo(target: BaseType, source: BaseType, insideTypes: Set = new Set()): boolean { // Dereference source and target source = derefType(source); target = derefType(target); @@ -73,6 +77,11 @@ export function isAssignableTo(target: BaseType, source: BaseType): boolean { return true; } + /** Don't check types when already inside them. This solves circular dependencies. */ + if (insideTypes.has(source) || insideTypes.has(target)) { + return true; + } + // Nothing can be assigned to never-type if (target instanceof NeverType) { return false; @@ -95,22 +104,22 @@ export function isAssignableTo(target: BaseType, source: BaseType): boolean { // Union type is assignable to target when all types in the union are assignable to it if (source instanceof UnionType) { - return source.getTypes().every(type => isAssignableTo(target, type)); + return source.getTypes().every(type => isAssignableTo(target, type, insideTypes)); } // When source is an intersection type then it can be assigned to target if any of the sub types matches. Object // types within the intersection must be combined first if (source instanceof IntersectionType) { - return combineIntersectingTypes(source).some(type => isAssignableTo(target, type)); + return combineIntersectingTypes(source).some(type => isAssignableTo(target, type, insideTypes)); } // For arrays check if item types are assignable if (target instanceof ArrayType) { const targetItemType = target.getItem(); if (source instanceof ArrayType) { - return isAssignableTo(targetItemType, source.getItem()); + return isAssignableTo(targetItemType, source.getItem(), insideTypes); } else if (source instanceof TupleType) { - return source.getTypes().every(type => isAssignableTo(targetItemType, type)); + return source.getTypes().every(type => isAssignableTo(targetItemType, type, insideTypes)); } else { return false; } @@ -118,20 +127,20 @@ export function isAssignableTo(target: BaseType, source: BaseType): boolean { // When target is a union type then check if source type can be assigned to any variant if (target instanceof UnionType) { - return target.getTypes().some(type => isAssignableTo(type, source)); + return target.getTypes().some(type => isAssignableTo(type, source, insideTypes)); } // When target is an intersection type then source can be assigned to it if it matches all sub types. Object // types within the intersection must be combined first if (target instanceof IntersectionType) { - return combineIntersectingTypes(target).every(type => isAssignableTo(type, source)); + return combineIntersectingTypes(target).every(type => isAssignableTo(type, source, insideTypes)); } if (target instanceof ObjectType) { const targetMembers = getObjectProperties(target); if (targetMembers.length === 0) { // When target object is empty then anything except null and undefined can be assigned to it - return !isAssignableTo(new UnionType([ new UndefinedType(), new NullType() ]), source); + return !isAssignableTo(new UnionType([ new UndefinedType(), new NullType() ]), source, insideTypes); } else if (source instanceof ObjectType) { const sourceMembers = getObjectProperties(source); @@ -148,7 +157,8 @@ export function isAssignableTo(target: BaseType, source: BaseType): boolean { if (targetMember == null) { return true; } - return isAssignableTo(targetMember.getType(), sourceMember.getType()); + return isAssignableTo(targetMember.getType(), sourceMember.getType(), + new Set(insideTypes).add(source).add(target)); }); } } @@ -161,13 +171,13 @@ export function isAssignableTo(target: BaseType, source: BaseType): boolean { const sourceMember = sourceMembers[i]; if (targetMember instanceof OptionalType) { if (sourceMember) { - return isAssignableTo(targetMember, sourceMember) || - isAssignableTo(targetMember.getType(), sourceMember); + return isAssignableTo(targetMember, sourceMember, insideTypes) || + isAssignableTo(targetMember.getType(), sourceMember, insideTypes); } else { return true; } } else { - return isAssignableTo(targetMember, sourceMember); + return isAssignableTo(targetMember, sourceMember, insideTypes); } }); } diff --git a/test/unit/isAssignableTo.test.ts b/test/unit/isAssignableTo.test.ts index 93b95a38c..530cf7d3d 100644 --- a/test/unit/isAssignableTo.test.ts +++ b/test/unit/isAssignableTo.test.ts @@ -291,4 +291,25 @@ describe("isAssignableTo", () => { expect(isAssignableTo(aAndB, ab)).toBe(true); expect(isAssignableTo(aAndB, aAndB)).toBe(true); }); + it("correctly handles circular dependencies", () => { + const nodeTypeARef = new ReferenceType(); + const nodeTypeA = new ObjectType("a", [], [ new ObjectProperty("parent", nodeTypeARef, false) ], false); + nodeTypeARef.setType(nodeTypeA); + + const nodeTypeBRef = new ReferenceType(); + const nodeTypeB = new ObjectType("b", [], [ new ObjectProperty("parent", nodeTypeBRef, false) ], false); + nodeTypeBRef.setType(nodeTypeB); + + const nodeTypeCRef = new ReferenceType(); + const nodeTypeC = new ObjectType("c", [], [ new ObjectProperty("child", nodeTypeCRef, false) ], false); + nodeTypeCRef.setType(nodeTypeC); + + expect(isAssignableTo(nodeTypeA, nodeTypeA)).toBe(true); + expect(isAssignableTo(nodeTypeA, nodeTypeB)).toBe(true); + expect(isAssignableTo(nodeTypeB, nodeTypeA)).toBe(true); + expect(isAssignableTo(nodeTypeC, nodeTypeA)).toBe(false); + expect(isAssignableTo(nodeTypeC, nodeTypeB)).toBe(false); + expect(isAssignableTo(nodeTypeA, nodeTypeC)).toBe(false); + expect(isAssignableTo(nodeTypeB, nodeTypeC)).toBe(false); + }); }); From 3fd9004a332cd4cef4e25425e6cd9fa79ef27011 Mon Sep 17 00:00:00 2001 From: Klaus Reimer Date: Fri, 7 Jun 2019 14:08:40 +0200 Subject: [PATCH 15/25] Simplify condition type node parser --- src/NodeParser/ConditionalTypeNodeParser.ts | 52 +++++++-------------- 1 file changed, 18 insertions(+), 34 deletions(-) diff --git a/src/NodeParser/ConditionalTypeNodeParser.ts b/src/NodeParser/ConditionalTypeNodeParser.ts index 93de03a5c..3e894fcf7 100644 --- a/src/NodeParser/ConditionalTypeNodeParser.ts +++ b/src/NodeParser/ConditionalTypeNodeParser.ts @@ -2,7 +2,6 @@ import * as ts from "typescript"; import { Context, NodeParser } from "../NodeParser"; import { SubNodeParser } from "../SubNodeParser"; import { BaseType } from "../Type/BaseType"; -import { NeverType } from "../Type/NeverType"; import { UnionType } from "../Type/UnionType"; import { derefType } from "../Utils/derefType"; import { isAssignableTo } from "../Utils/isAssignableTo"; @@ -17,45 +16,30 @@ export class ConditionalTypeNodeParser implements SubNodeParser { } public createType(node: ts.ConditionalTypeNode, context: Context): BaseType { - const extendsType = this.childNodeParser.createType(node.extendsType, context); - - // Get the check type from the condition and expand them into an array of check types in case the check - // type is a union type. Each union type candidate is checked separately and the result is again grouped - // into a union type if necessary const checkType = this.childNodeParser.createType(node.checkType, context); - const unwrappedCheckType = derefType(checkType); - const checkTypes = unwrappedCheckType instanceof UnionType ? unwrappedCheckType.getTypes() : [ checkType ]; - - // Process each part of the check type separately - const resultTypes: BaseType[] = []; - for (const type of checkTypes) { - const resultType = isAssignableTo(extendsType, type) - ? this.childNodeParser.createType(node.trueType, context) - : this.childNodeParser.createType(node.falseType, context); - const unwrappedResultType = derefType(resultType); + const extendsType = this.childNodeParser.createType(node.extendsType, context); - // Ignore never types (Used in exclude conditions) so they are not added to the result union type - if (unwrappedResultType instanceof NeverType) { - continue; + if (isAssignableTo(extendsType, checkType)) { + const result = this.childNodeParser.createType(node.trueType, context); + if (derefType(result).getId() === derefType(checkType).getId()) { + return this.narrowType(result, type => isAssignableTo(extendsType, type)); } - - // If result type is actually the original check type (Which may be a union type) then only record the - // currently processed check type as a result. If check type is not a union type then this makes no - // difference but for union types this ensures that only the matching part of it is added to the result - // which is important for exclude conditions. - if (resultType.getId() === checkType.getId()) { - resultTypes.push(type); - } else { - resultTypes.push(resultType); + return result; + } else { + const result = this.childNodeParser.createType(node.falseType, context); + if (derefType(result).getId() === derefType(checkType).getId()) { + return this.narrowType(result, type => !isAssignableTo(extendsType, type)); } + return result; } + } - // If there is only one result type then return this one directly. Otherwise return the recorded - // result types as a union type. - if (resultTypes.length === 1) { - return resultTypes[0]; - } else { - return new UnionType(resultTypes); + private narrowType(type: BaseType, predicate: (type: BaseType) => boolean): BaseType { + const derefed = derefType(type); + if (!(derefed instanceof UnionType)) { + return type; } + const matchingTypes = derefed.getTypes().filter(predicate); + return matchingTypes.length === 1 ? matchingTypes[0] : new UnionType(matchingTypes); } } From 9d7500adc27f771d69ab3603d26dc5624c553873 Mon Sep 17 00:00:00 2001 From: Klaus Reimer Date: Fri, 7 Jun 2019 14:08:52 +0200 Subject: [PATCH 16/25] Add test for Omit --- test/valid-data.test.ts | 1 + test/valid-data/type-conditional-omit/main.ts | 8 +++++++ .../type-conditional-omit/schema.json | 22 +++++++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 test/valid-data/type-conditional-omit/main.ts create mode 100644 test/valid-data/type-conditional-omit/schema.json diff --git a/test/valid-data.test.ts b/test/valid-data.test.ts index c3d83581d..684b8a9b6 100644 --- a/test/valid-data.test.ts +++ b/test/valid-data.test.ts @@ -161,4 +161,5 @@ describe("valid-data", () => { it("type-conditional-union", assertSchema("type-conditional-union", "MyObject")); it("type-conditional-intersection", assertSchema("type-conditional-intersection", "MyObject")); it("type-conditional-exclude", assertSchema("type-conditional-exclude", "MyObject")); + it("type-conditional-omit", assertSchema("type-conditional-omit", "MyObject")); }); diff --git a/test/valid-data/type-conditional-omit/main.ts b/test/valid-data/type-conditional-omit/main.ts new file mode 100644 index 000000000..d56e97dbb --- /dev/null +++ b/test/valid-data/type-conditional-omit/main.ts @@ -0,0 +1,8 @@ +interface Test { + a: string; + b: number; + c: boolean; + d: string[]; +} + +export type MyObject = Omit; diff --git a/test/valid-data/type-conditional-omit/schema.json b/test/valid-data/type-conditional-omit/schema.json new file mode 100644 index 000000000..719f8c4db --- /dev/null +++ b/test/valid-data/type-conditional-omit/schema.json @@ -0,0 +1,22 @@ +{ + "$ref": "#/definitions/MyObject", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MyObject": { + "additionalProperties": false, + "properties": { + "a": { + "type": "string" + }, + "c": { + "type": "boolean" + } + }, + "required": [ + "a", + "c" + ], + "type": "object" + } + } +} From eac11e38f8c058558f9d60d1eed3e88aa92d6e45 Mon Sep 17 00:00:00 2001 From: Klaus Reimer Date: Fri, 7 Jun 2019 14:16:10 +0200 Subject: [PATCH 17/25] Simplified conditional type node parser even more --- src/NodeParser/ConditionalTypeNodeParser.ts | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/src/NodeParser/ConditionalTypeNodeParser.ts b/src/NodeParser/ConditionalTypeNodeParser.ts index 3e894fcf7..1d883bdfb 100644 --- a/src/NodeParser/ConditionalTypeNodeParser.ts +++ b/src/NodeParser/ConditionalTypeNodeParser.ts @@ -18,20 +18,12 @@ export class ConditionalTypeNodeParser implements SubNodeParser { public createType(node: ts.ConditionalTypeNode, context: Context): BaseType { const checkType = this.childNodeParser.createType(node.checkType, context); const extendsType = this.childNodeParser.createType(node.extendsType, context); - - if (isAssignableTo(extendsType, checkType)) { - const result = this.childNodeParser.createType(node.trueType, context); - if (derefType(result).getId() === derefType(checkType).getId()) { - return this.narrowType(result, type => isAssignableTo(extendsType, type)); - } - return result; - } else { - const result = this.childNodeParser.createType(node.falseType, context); - if (derefType(result).getId() === derefType(checkType).getId()) { - return this.narrowType(result, type => !isAssignableTo(extendsType, type)); - } - return result; + const result = isAssignableTo(extendsType, checkType); + const resultType = this.childNodeParser.createType(result ? node.trueType : node.falseType, context); + if (derefType(resultType).getId() === derefType(checkType).getId()) { + return this.narrowType(resultType, type => isAssignableTo(extendsType, type) === result); } + return resultType; } private narrowType(type: BaseType, predicate: (type: BaseType) => boolean): BaseType { From 093f8474ce634c37b02687dcf5993bce63625a7c Mon Sep 17 00:00:00 2001 From: Klaus Reimer Date: Fri, 7 Jun 2019 15:59:32 +0200 Subject: [PATCH 18/25] Support enums in conditionals types --- src/NodeParser/ConditionalTypeNodeParser.ts | 8 ++++--- src/Type/EnumType.ts | 9 ++++++++ src/Utils/isAssignableTo.ts | 11 ++++++---- test/valid-data.test.ts | 1 + test/valid-data/type-conditional-enum/main.ts | 10 +++++++++ .../type-conditional-enum/schema.json | 22 +++++++++++++++++++ 6 files changed, 54 insertions(+), 7 deletions(-) create mode 100644 test/valid-data/type-conditional-enum/main.ts create mode 100644 test/valid-data/type-conditional-enum/schema.json diff --git a/src/NodeParser/ConditionalTypeNodeParser.ts b/src/NodeParser/ConditionalTypeNodeParser.ts index 1d883bdfb..6b5775e90 100644 --- a/src/NodeParser/ConditionalTypeNodeParser.ts +++ b/src/NodeParser/ConditionalTypeNodeParser.ts @@ -2,6 +2,7 @@ import * as ts from "typescript"; import { Context, NodeParser } from "../NodeParser"; import { SubNodeParser } from "../SubNodeParser"; import { BaseType } from "../Type/BaseType"; +import { EnumType } from "../Type/EnumType"; import { UnionType } from "../Type/UnionType"; import { derefType } from "../Utils/derefType"; import { isAssignableTo } from "../Utils/isAssignableTo"; @@ -28,10 +29,11 @@ export class ConditionalTypeNodeParser implements SubNodeParser { private narrowType(type: BaseType, predicate: (type: BaseType) => boolean): BaseType { const derefed = derefType(type); - if (!(derefed instanceof UnionType)) { + if (derefed instanceof UnionType || derefed instanceof EnumType) { + const matchingTypes = derefed.getTypes().filter(predicate); + return matchingTypes.length === 1 ? matchingTypes[0] : new UnionType(matchingTypes); + } else { return type; } - const matchingTypes = derefed.getTypes().filter(predicate); - return matchingTypes.length === 1 ? matchingTypes[0] : new UnionType(matchingTypes); } } diff --git a/src/Type/EnumType.ts b/src/Type/EnumType.ts index e6083f862..1c21e8a9b 100644 --- a/src/Type/EnumType.ts +++ b/src/Type/EnumType.ts @@ -1,13 +1,18 @@ import { BaseType } from "./BaseType"; +import { LiteralType } from "./LiteralType"; +import { NullType } from "./NullType"; export type EnumValue = string|boolean|number|null; export class EnumType extends BaseType { + private types: BaseType[]; + public constructor( private id: string, private values: EnumValue[], ) { super(); + this.types = values.map(value => value == null ? new NullType() : new LiteralType(value)); } public getId(): string { @@ -17,4 +22,8 @@ export class EnumType extends BaseType { public getValues(): EnumValue[] { return this.values; } + + public getTypes(): BaseType[] { + return this.types; + } } diff --git a/src/Utils/isAssignableTo.ts b/src/Utils/isAssignableTo.ts index 51aa56827..7130b0b72 100644 --- a/src/Utils/isAssignableTo.ts +++ b/src/Utils/isAssignableTo.ts @@ -1,7 +1,9 @@ import { AnyType } from "../Type/AnyType"; import { ArrayType } from "../Type/ArrayType"; import { BaseType } from "../Type/BaseType"; +import { EnumType } from "../Type/EnumType"; import { IntersectionType } from "../Type/IntersectionType"; +import { LiteralType } from "../Type/LiteralType"; import { NeverType } from "../Type/NeverType"; import { NullType } from "../Type/NullType"; import { ObjectProperty, ObjectType } from "../Type/ObjectType"; @@ -11,6 +13,7 @@ import { UndefinedType } from "../Type/UndefinedType"; import { UnionType } from "../Type/UnionType"; import { UnknownType } from "../Type/UnknownType"; import { derefType } from "./derefType"; +import { uniqueArray } from "./uniqueArray"; /** * Returns the combined types from the given intersection. Currently only object types are combined. Maybe more @@ -102,8 +105,8 @@ export function isAssignableTo(target: BaseType, source: BaseType, insideTypes: return true; } - // Union type is assignable to target when all types in the union are assignable to it - if (source instanceof UnionType) { + // Union and enum type is assignable to target when all types in the union/enum are assignable to it + if (source instanceof UnionType || source instanceof EnumType) { return source.getTypes().every(type => isAssignableTo(target, type, insideTypes)); } @@ -125,8 +128,8 @@ export function isAssignableTo(target: BaseType, source: BaseType, insideTypes: } } - // When target is a union type then check if source type can be assigned to any variant - if (target instanceof UnionType) { + // When target is a union or enum type then check if source type can be assigned to any variant + if (target instanceof UnionType || target instanceof EnumType) { return target.getTypes().some(type => isAssignableTo(type, source, insideTypes)); } diff --git a/test/valid-data.test.ts b/test/valid-data.test.ts index 684b8a9b6..f417337e2 100644 --- a/test/valid-data.test.ts +++ b/test/valid-data.test.ts @@ -159,6 +159,7 @@ describe("valid-data", () => { it("type-conditional-simple", assertSchema("type-conditional-simple", "MyObject")); it("type-conditional-inheritance", assertSchema("type-conditional-inheritance", "MyObject")); it("type-conditional-union", assertSchema("type-conditional-union", "MyObject")); + it("type-conditional-enum", assertSchema("type-conditional-enum", "IParameter")); it("type-conditional-intersection", assertSchema("type-conditional-intersection", "MyObject")); it("type-conditional-exclude", assertSchema("type-conditional-exclude", "MyObject")); it("type-conditional-omit", assertSchema("type-conditional-omit", "MyObject")); diff --git a/test/valid-data/type-conditional-enum/main.ts b/test/valid-data/type-conditional-enum/main.ts new file mode 100644 index 000000000..c367119e4 --- /dev/null +++ b/test/valid-data/type-conditional-enum/main.ts @@ -0,0 +1,10 @@ +enum ParameterType { + Enum = "enum", + Number = "number", + String = "string", + Date = "date", +} + +export interface IParameter { + type: Exclude; +} diff --git a/test/valid-data/type-conditional-enum/schema.json b/test/valid-data/type-conditional-enum/schema.json new file mode 100644 index 000000000..ace0a4a98 --- /dev/null +++ b/test/valid-data/type-conditional-enum/schema.json @@ -0,0 +1,22 @@ +{ + "$ref": "#/definitions/IParameter", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "IParameter": { + "additionalProperties": false, + "properties": { + "type": { + "enum": [ + "string", + "date" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + } + } +} From ae5fbcc01a20bd5e2e913bc987d0a717d68e2c47 Mon Sep 17 00:00:00 2001 From: Klaus Reimer Date: Fri, 7 Jun 2019 22:05:03 +0200 Subject: [PATCH 19/25] Combine unions --- src/NodeParser/ConditionalTypeNodeParser.ts | 14 +++++++++++++- src/Utils/typeKeys.ts | 11 ++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/NodeParser/ConditionalTypeNodeParser.ts b/src/NodeParser/ConditionalTypeNodeParser.ts index 6b5775e90..c4727045a 100644 --- a/src/NodeParser/ConditionalTypeNodeParser.ts +++ b/src/NodeParser/ConditionalTypeNodeParser.ts @@ -27,10 +27,22 @@ export class ConditionalTypeNodeParser implements SubNodeParser { return resultType; } + private combineUnion(union: UnionType | EnumType): UnionType { + return new UnionType(union.getTypes().reduce((types, type) => { + const derefed = derefType(type); + if (derefed instanceof UnionType) { + types.push(...this.combineUnion(derefed).getTypes()); + } else { + types.push(type); + } + return types; + }, [])); + } + private narrowType(type: BaseType, predicate: (type: BaseType) => boolean): BaseType { const derefed = derefType(type); if (derefed instanceof UnionType || derefed instanceof EnumType) { - const matchingTypes = derefed.getTypes().filter(predicate); + const matchingTypes = this.combineUnion(derefed).getTypes().filter(predicate); return matchingTypes.length === 1 ? matchingTypes[0] : new UnionType(matchingTypes); } else { return type; diff --git a/src/Utils/typeKeys.ts b/src/Utils/typeKeys.ts index a6af522c0..f7b1058e5 100644 --- a/src/Utils/typeKeys.ts +++ b/src/Utils/typeKeys.ts @@ -65,9 +65,14 @@ export function getTypeByKey(type: BaseType, index: LiteralType): BaseType | und const property = type.getProperties().find((it) => it.getName() === index.getValue()); if (property) { const propertyType = property.getType(); - if (!property.isRequired() && !(propertyType instanceof UnionType && - propertyType.getTypes().some(subType => subType instanceof UndefinedType))) { - return new UnionType([propertyType, new UndefinedType() ]); + if (!property.isRequired()) { + if (propertyType instanceof UnionType) { + if (!propertyType.getTypes().some(subType => subType instanceof UndefinedType)) { + return new UnionType([ ...propertyType.getTypes(), new UndefinedType() ]); + } + } else { + return new UnionType([ propertyType, new UndefinedType() ]); + } } return propertyType; } From 5faa296f8c8049cb72567c9976b1cba076de3bd8 Mon Sep 17 00:00:00 2001 From: Klaus Reimer Date: Fri, 7 Jun 2019 22:05:20 +0200 Subject: [PATCH 20/25] Deref type --- src/TypeFormatter/ObjectTypeFormatter.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/TypeFormatter/ObjectTypeFormatter.ts b/src/TypeFormatter/ObjectTypeFormatter.ts index 19cdae0c1..c40349a7a 100644 --- a/src/TypeFormatter/ObjectTypeFormatter.ts +++ b/src/TypeFormatter/ObjectTypeFormatter.ts @@ -7,6 +7,7 @@ import { UndefinedType } from "../Type/UndefinedType"; import { UnionType } from "../Type/UnionType"; import { TypeFormatter } from "../TypeFormatter"; import { getAllOfDefinitionReducer } from "../Utils/allOfDefinition"; +import { derefType } from "../Utils/derefType"; import { StringMap } from "../Utils/StringMap"; export class ObjectTypeFormatter implements SubTypeFormatter { @@ -75,7 +76,7 @@ export class ObjectTypeFormatter implements SubTypeFormatter { } private prepareObjectProperty(property: ObjectProperty): ObjectProperty { - const propType = property.getType(); + const propType = derefType(property.getType()); if (propType instanceof UndefinedType) { return new ObjectProperty(property.getName(), new UndefinedType(), false); } else if (!(propType instanceof UnionType)) { From dcba459e3dda68a5787fc67c17c89b04d799b9d2 Mon Sep 17 00:00:00 2001 From: Klaus Reimer Date: Fri, 7 Jun 2019 22:05:37 +0200 Subject: [PATCH 21/25] More tests --- test/unit/isAssignableTo.test.ts | 11 +++++++++ test/valid-data.test.ts | 1 + .../type-conditional-exclude-complex/main.ts | 23 +++++++++++++++++++ .../schema.json | 18 +++++++++++++++ 4 files changed, 53 insertions(+) create mode 100644 test/valid-data/type-conditional-exclude-complex/main.ts create mode 100644 test/valid-data/type-conditional-exclude-complex/schema.json diff --git a/test/unit/isAssignableTo.test.ts b/test/unit/isAssignableTo.test.ts index 530cf7d3d..5fec4aedc 100644 --- a/test/unit/isAssignableTo.test.ts +++ b/test/unit/isAssignableTo.test.ts @@ -312,4 +312,15 @@ describe("isAssignableTo", () => { expect(isAssignableTo(nodeTypeA, nodeTypeC)).toBe(false); expect(isAssignableTo(nodeTypeB, nodeTypeC)).toBe(false); }); + it("can handle deep union structures", () => { + const objectType = new ObjectType("interface-src/test.ts-0-53-src/test.ts-0-317", [], + [ new ObjectProperty("a", new StringType(), true) ], false); + const innerDefinition = new DefinitionType("NumericValueRef", objectType); + const innerUnion = new UnionType([ new NumberType(), innerDefinition ]); + const alias = new AliasType("alias-src/test.ts-53-106-src/test.ts-0-317", innerUnion); + const outerDefinition = new DefinitionType("NumberValue", alias); + const outerUnion = new UnionType([ outerDefinition, new UndefinedType() ]); + const def = new DefinitionType("NumericValueRef", objectType); + expect(isAssignableTo(outerUnion, def)).toBe(true); + }); }); diff --git a/test/valid-data.test.ts b/test/valid-data.test.ts index 4ab9c222c..da9f030c8 100644 --- a/test/valid-data.test.ts +++ b/test/valid-data.test.ts @@ -165,5 +165,6 @@ describe("valid-data", () => { it("type-conditional-enum", assertSchema("type-conditional-enum", "IParameter")); it("type-conditional-intersection", assertSchema("type-conditional-intersection", "MyObject")); it("type-conditional-exclude", assertSchema("type-conditional-exclude", "MyObject")); + it("type-conditional-exclude-complex", assertSchema("type-conditional-exclude-complex", "BaseAxisNoSignals")); it("type-conditional-omit", assertSchema("type-conditional-omit", "MyObject")); }); diff --git a/test/valid-data/type-conditional-exclude-complex/main.ts b/test/valid-data/type-conditional-exclude-complex/main.ts new file mode 100644 index 000000000..bbc81b70a --- /dev/null +++ b/test/valid-data/type-conditional-exclude-complex/main.ts @@ -0,0 +1,23 @@ +export interface NumericValueRef { + name: "numeric"; + ref: string; +} + +export interface StringValueRef { + name: "string"; + ref: string; +} + +export type NumberValue = number | NumericValueRef; +export type StringValue = string | StringValueRef; + +export interface BaseAxis { + minExtent?: N; + titleFont?: S; +} + +type OmitValueRef = { + [P in keyof T]?: Exclude, StringValueRef> +}; + +export type BaseAxisNoSignals = OmitValueRef; diff --git a/test/valid-data/type-conditional-exclude-complex/schema.json b/test/valid-data/type-conditional-exclude-complex/schema.json new file mode 100644 index 000000000..ff049f392 --- /dev/null +++ b/test/valid-data/type-conditional-exclude-complex/schema.json @@ -0,0 +1,18 @@ +{ + "$ref": "#/definitions/BaseAxisNoSignals", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "BaseAxisNoSignals": { + "additionalProperties": false, + "properties": { + "minExtent": { + "type": "number" + }, + "titleFont": { + "type": "string" + } + }, + "type": "object" + } + } +} From 6d685b0ddcd4e10e041d068461d13e7b6c197917 Mon Sep 17 00:00:00 2001 From: Klaus Reimer Date: Sat, 8 Jun 2019 22:13:07 +0200 Subject: [PATCH 22/25] Improve type narrowing so definition types are kept --- src/NodeParser/ConditionalTypeNodeParser.ts | 27 +----- .../IntersectionTypeFormatter.ts | 2 + src/Utils/narrowType.ts | 55 ++++++++++++ test/valid-data.test.ts | 1 + .../main.ts | 22 +++++ .../schema.json | 83 +++++++++++++++++++ 6 files changed, 165 insertions(+), 25 deletions(-) create mode 100644 src/Utils/narrowType.ts create mode 100644 test/valid-data/type-conditional-exclude-narrowing/main.ts create mode 100644 test/valid-data/type-conditional-exclude-narrowing/schema.json diff --git a/src/NodeParser/ConditionalTypeNodeParser.ts b/src/NodeParser/ConditionalTypeNodeParser.ts index c4727045a..8e481b60b 100644 --- a/src/NodeParser/ConditionalTypeNodeParser.ts +++ b/src/NodeParser/ConditionalTypeNodeParser.ts @@ -2,10 +2,9 @@ import * as ts from "typescript"; import { Context, NodeParser } from "../NodeParser"; import { SubNodeParser } from "../SubNodeParser"; import { BaseType } from "../Type/BaseType"; -import { EnumType } from "../Type/EnumType"; -import { UnionType } from "../Type/UnionType"; import { derefType } from "../Utils/derefType"; import { isAssignableTo } from "../Utils/isAssignableTo"; +import { narrowType } from "../Utils/narrowType"; export class ConditionalTypeNodeParser implements SubNodeParser { public constructor( @@ -22,30 +21,8 @@ export class ConditionalTypeNodeParser implements SubNodeParser { const result = isAssignableTo(extendsType, checkType); const resultType = this.childNodeParser.createType(result ? node.trueType : node.falseType, context); if (derefType(resultType).getId() === derefType(checkType).getId()) { - return this.narrowType(resultType, type => isAssignableTo(extendsType, type) === result); + return narrowType(resultType, type => isAssignableTo(extendsType, type) === result); } return resultType; } - - private combineUnion(union: UnionType | EnumType): UnionType { - return new UnionType(union.getTypes().reduce((types, type) => { - const derefed = derefType(type); - if (derefed instanceof UnionType) { - types.push(...this.combineUnion(derefed).getTypes()); - } else { - types.push(type); - } - return types; - }, [])); - } - - private narrowType(type: BaseType, predicate: (type: BaseType) => boolean): BaseType { - const derefed = derefType(type); - if (derefed instanceof UnionType || derefed instanceof EnumType) { - const matchingTypes = this.combineUnion(derefed).getTypes().filter(predicate); - return matchingTypes.length === 1 ? matchingTypes[0] : new UnionType(matchingTypes); - } else { - return type; - } - } } diff --git a/src/TypeFormatter/IntersectionTypeFormatter.ts b/src/TypeFormatter/IntersectionTypeFormatter.ts index 0e60ad9a8..9723caa4a 100644 --- a/src/TypeFormatter/IntersectionTypeFormatter.ts +++ b/src/TypeFormatter/IntersectionTypeFormatter.ts @@ -1,6 +1,7 @@ import { Definition } from "../Schema/Definition"; import { SubTypeFormatter } from "../SubTypeFormatter"; import { BaseType } from "../Type/BaseType"; +import { DefinitionType } from "../Type/DefinitionType"; import { IntersectionType } from "../Type/IntersectionType"; import { ObjectType } from "../Type/ObjectType"; import { TypeFormatter } from "../TypeFormatter"; @@ -31,6 +32,7 @@ export class IntersectionTypeFormatter implements SubTypeFormatter { // Remove the first child, which is the definition of the child itself because we are merging objects. // However, if the child is just a reference, we cannot remove it. const slice = item instanceof ObjectType ? 0 : 1; +// const slice = item instanceof DefinitionType ? 1 : 0; return [ ...result, ...this.childTypeFormatter.getChildren(item).slice(slice), diff --git a/src/Utils/narrowType.ts b/src/Utils/narrowType.ts new file mode 100644 index 000000000..07e64c1c1 --- /dev/null +++ b/src/Utils/narrowType.ts @@ -0,0 +1,55 @@ +import { BaseType } from "../Type/BaseType"; +import { EnumType } from "../Type/EnumType"; +import { NeverType } from "../Type/NeverType"; +import { UnionType } from "../Type/UnionType"; +import { derefType } from "./derefType"; + +/** + * Narrows the given type by passing all variants to the given predicate function. So when type is a union type then + * the predicate function is called for each type within the union and only the types for which this function returns + * true will remain in the returned type. Union types with only one sub type left are replaced by this one-and-only + * type. Empty union types are removed completely. Definition types are kept if possible. When in the end none of + * the type candidates match the predicate then NeverType is returned. + * + * @param type - The type to narrow down. + * @param predicate - The predicate function to filter the type variants. If it returns true then the type variant is + * kept, when returning false it is removed. + * @return The narrowed down type. + */ +export function narrowType(type: BaseType, predicate: (type: BaseType) => boolean): BaseType { + const derefed = derefType(type); + if (derefed instanceof UnionType || derefed instanceof EnumType) { + let changed = false; + const types: BaseType[] = []; + for (const sub of derefed.getTypes()) { + const derefedSub = derefType(sub); + + // Recursively narrow down all types within the union + const narrowed = narrowType(derefedSub, predicate); + if (!(narrowed instanceof NeverType)) { + if (narrowed === derefedSub) { + types.push(sub); + } else { + types.push(narrowed); + changed = true; + } + } else { + changed = true; + } + } + + // When union types were changed then return new narrowed-down type, otherwise return the original one to + // keep definitions + if (changed) { + if (types.length === 0) { + return new NeverType(); + } else if (types.length === 1) { + return types[0]; + } else { + return new UnionType(types); + } + } + return type; + } + return predicate(derefed) ? type : new NeverType(); +} diff --git a/test/valid-data.test.ts b/test/valid-data.test.ts index da9f030c8..1bd7d9594 100644 --- a/test/valid-data.test.ts +++ b/test/valid-data.test.ts @@ -166,5 +166,6 @@ describe("valid-data", () => { it("type-conditional-intersection", assertSchema("type-conditional-intersection", "MyObject")); it("type-conditional-exclude", assertSchema("type-conditional-exclude", "MyObject")); it("type-conditional-exclude-complex", assertSchema("type-conditional-exclude-complex", "BaseAxisNoSignals")); + it("type-conditional-exclude-narrowing", assertSchema("type-conditional-exclude-narrowing", "MyObject")); it("type-conditional-omit", assertSchema("type-conditional-omit", "MyObject")); }); diff --git a/test/valid-data/type-conditional-exclude-narrowing/main.ts b/test/valid-data/type-conditional-exclude-narrowing/main.ts new file mode 100644 index 000000000..fb967f5c7 --- /dev/null +++ b/test/valid-data/type-conditional-exclude-narrowing/main.ts @@ -0,0 +1,22 @@ +export type Align = "left" | "right" | "center"; + +export type Text = { + align?: Align | number; +}; + +type OmitPropertyType = { + [P in keyof T]: Exclude; +}; + +export type GoodPrimitives = string | number; +export type BadPrimitives = null | boolean; +export type Primitives = GoodPrimitives | BadPrimitives; + +export interface MyObject { + textWithoutAlign: OmitPropertyType; + textWithoutNumbers: OmitPropertyType; + allPrims: Exclude; + goodPrims: Exclude; + badPrims: Exclude, number>; + stringOrNull: Exclude; +} diff --git a/test/valid-data/type-conditional-exclude-narrowing/schema.json b/test/valid-data/type-conditional-exclude-narrowing/schema.json new file mode 100644 index 000000000..aaaff37cf --- /dev/null +++ b/test/valid-data/type-conditional-exclude-narrowing/schema.json @@ -0,0 +1,83 @@ +{ + "$ref": "#/definitions/MyObject", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "Align": { + "enum": [ + "left", + "right", + "center" + ], + "type": "string" + }, + "BadPrimitives": { + "type": [ + "null", + "boolean" + ] + }, + "GoodPrimitives": { + "type": [ + "string", + "number" + ] + }, + "MyObject": { + "additionalProperties": false, + "properties": { + "allPrims": { + "$ref": "#/definitions/Primitives" + }, + "badPrims": { + "$ref": "#/definitions/BadPrimitives" + }, + "goodPrims": { + "$ref": "#/definitions/GoodPrimitives" + }, + "stringOrNull": { + "type": [ + "string", + "null" + ] + }, + "textWithoutAlign": { + "additionalProperties": false, + "properties": { + "align": { + "type": "number" + } + }, + "type": "object" + }, + "textWithoutNumbers": { + "additionalProperties": false, + "properties": { + "align": { + "$ref": "#/definitions/Align" + } + }, + "type": "object" + } + }, + "required": [ + "textWithoutAlign", + "textWithoutNumbers", + "allPrims", + "goodPrims", + "badPrims", + "stringOrNull" + ], + "type": "object" + }, + "Primitives": { + "anyOf": [ + { + "$ref": "#/definitions/GoodPrimitives" + }, + { + "$ref": "#/definitions/BadPrimitives" + } + ] + } + } +} From 1e761a8a8ac0a5457bc5a38ef06876327a9321ef Mon Sep 17 00:00:00 2001 From: Klaus Reimer Date: Sun, 9 Jun 2019 14:24:13 +0200 Subject: [PATCH 23/25] Remove accidentally commited comment --- src/TypeFormatter/IntersectionTypeFormatter.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/TypeFormatter/IntersectionTypeFormatter.ts b/src/TypeFormatter/IntersectionTypeFormatter.ts index 9723caa4a..7f5761b1b 100644 --- a/src/TypeFormatter/IntersectionTypeFormatter.ts +++ b/src/TypeFormatter/IntersectionTypeFormatter.ts @@ -32,7 +32,6 @@ export class IntersectionTypeFormatter implements SubTypeFormatter { // Remove the first child, which is the definition of the child itself because we are merging objects. // However, if the child is just a reference, we cannot remove it. const slice = item instanceof ObjectType ? 0 : 1; -// const slice = item instanceof DefinitionType ? 1 : 0; return [ ...result, ...this.childTypeFormatter.getChildren(item).slice(slice), From a0df82d2743cfc79faedca7e5a1e14a8c48dc9fb Mon Sep 17 00:00:00 2001 From: Klaus Reimer Date: Sat, 22 Jun 2019 13:37:14 +0200 Subject: [PATCH 24/25] Only narrow down result type when type parameter matching the check type --- factory/parser.ts | 2 +- src/NodeParser/ConditionalTypeNodeParser.ts | 28 ++++++++-- test/valid-data.test.ts | 1 + .../valid-data/type-conditional-jsdoc/main.ts | 30 +++++++++++ .../type-conditional-jsdoc/schema.json | 53 +++++++++++++++++++ 5 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 test/valid-data/type-conditional-jsdoc/main.ts create mode 100644 test/valid-data/type-conditional-jsdoc/schema.json diff --git a/factory/parser.ts b/factory/parser.ts index 0bbda586e..5e6eab7d8 100644 --- a/factory/parser.ts +++ b/factory/parser.ts @@ -96,7 +96,7 @@ export function createParser(program: ts.Program, config: Config): NodeParser { .addNodeParser(new IndexedAccessTypeNodeParser(chainNodeParser)) .addNodeParser(new TypeofNodeParser(typeChecker, chainNodeParser)) .addNodeParser(new MappedTypeNodeParser(chainNodeParser)) - .addNodeParser(new ConditionalTypeNodeParser(chainNodeParser)) + .addNodeParser(new ConditionalTypeNodeParser(typeChecker, chainNodeParser)) .addNodeParser(new TypeOperatorNodeParser(chainNodeParser)) .addNodeParser(new UnionNodeParser(typeChecker, chainNodeParser)) diff --git a/src/NodeParser/ConditionalTypeNodeParser.ts b/src/NodeParser/ConditionalTypeNodeParser.ts index 8e481b60b..b2c0095ad 100644 --- a/src/NodeParser/ConditionalTypeNodeParser.ts +++ b/src/NodeParser/ConditionalTypeNodeParser.ts @@ -2,12 +2,12 @@ import * as ts from "typescript"; import { Context, NodeParser } from "../NodeParser"; import { SubNodeParser } from "../SubNodeParser"; import { BaseType } from "../Type/BaseType"; -import { derefType } from "../Utils/derefType"; import { isAssignableTo } from "../Utils/isAssignableTo"; import { narrowType } from "../Utils/narrowType"; export class ConditionalTypeNodeParser implements SubNodeParser { public constructor( + private typeChecker: ts.TypeChecker, private childNodeParser: NodeParser, ) {} @@ -19,10 +19,32 @@ export class ConditionalTypeNodeParser implements SubNodeParser { const checkType = this.childNodeParser.createType(node.checkType, context); const extendsType = this.childNodeParser.createType(node.extendsType, context); const result = isAssignableTo(extendsType, checkType); - const resultType = this.childNodeParser.createType(result ? node.trueType : node.falseType, context); - if (derefType(resultType).getId() === derefType(checkType).getId()) { + const tsResultType = result ? node.trueType : node.falseType; + const resultType = this.childNodeParser.createType(tsResultType, context); + + // If result type is the same type parameter as the check type then narrow down the result type + const checkTypeParameterName = this.getTypeParameterName(node.checkType); + const resultTypeParameterName = this.getTypeParameterName(tsResultType); + if (resultTypeParameterName != null && resultTypeParameterName === checkTypeParameterName) { return narrowType(resultType, type => isAssignableTo(extendsType, type) === result); } + return resultType; } + + /** + * Returns the type parameter name of the given type node if any. + * + * @param node - The type node for which to return the type parameter name. + * @return The type parameter name or null if specified type node is not a type parameter. + */ + private getTypeParameterName(node: ts.TypeNode): string | null { + if (ts.isTypeReferenceNode(node)) { + const typeSymbol = this.typeChecker.getSymbolAtLocation(node.typeName)!; + if (typeSymbol.flags & ts.SymbolFlags.TypeParameter) { + return typeSymbol.name; + } + } + return null; + } } diff --git a/test/valid-data.test.ts b/test/valid-data.test.ts index 2ebe289bd..ed0035205 100644 --- a/test/valid-data.test.ts +++ b/test/valid-data.test.ts @@ -174,4 +174,5 @@ describe("valid-data", () => { it("type-conditional-exclude-complex", assertSchema("type-conditional-exclude-complex", "BaseAxisNoSignals")); it("type-conditional-exclude-narrowing", assertSchema("type-conditional-exclude-narrowing", "MyObject")); it("type-conditional-omit", assertSchema("type-conditional-omit", "MyObject")); + it("type-conditional-jsdoc", assertSchema("type-conditional-jsdoc", "MyObject", "extended")); }); diff --git a/test/valid-data/type-conditional-jsdoc/main.ts b/test/valid-data/type-conditional-jsdoc/main.ts new file mode 100644 index 000000000..4754919dd --- /dev/null +++ b/test/valid-data/type-conditional-jsdoc/main.ts @@ -0,0 +1,30 @@ +/** + * Number or string + * @pattern foo + */ +type NumberOrString = number | string; + +type NoString = T extends string ? never : T; + +/** + * No string + * @pattern bar + */ +type NoStringDocumented = T extends string ? never : T; + +export type MyObject = { + a: NumberOrString extends number ? never : NumberOrString; + + /** Description of b */ + b: NumberOrString extends number ? never : NumberOrString; + + c: NoString; + + d: NoStringDocumented; + + /** Description of e */ + e: NoString; + + /** Description of f */ + f: NoStringDocumented; +}; diff --git a/test/valid-data/type-conditional-jsdoc/schema.json b/test/valid-data/type-conditional-jsdoc/schema.json new file mode 100644 index 000000000..204fc0701 --- /dev/null +++ b/test/valid-data/type-conditional-jsdoc/schema.json @@ -0,0 +1,53 @@ +{ + "$ref": "#/definitions/MyObject", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MyObject": { + "additionalProperties": false, + "properties": { + "a": { + "description": "Number or string", + "pattern": "foo", + "type": [ + "number", + "string" + ] + }, + "b": { + "description": "Description of b", + "pattern": "foo", + "type": [ + "number", + "string" + ] + }, + "c": { + "type": "number" + }, + "d": { + "description": "No string", + "pattern": "bar", + "type": "number" + }, + "e": { + "description": "Description of e", + "type": "number" + }, + "f": { + "description": "Description of f", + "pattern": "bar", + "type": "number" + } + }, + "required": [ + "a", + "b", + "c", + "d", + "e", + "f" + ], + "type": "object" + } + } +} From d7cdb26b9b1315d3927283e33bff50777917c58b Mon Sep 17 00:00:00 2001 From: Klaus Reimer Date: Sat, 22 Jun 2019 16:43:50 +0200 Subject: [PATCH 25/25] Ignore annotations from standard typescript types --- src/NodeParser/AnnotatedNodeParser.ts | 3 +++ .../valid-data/type-conditional-jsdoc/main.ts | 8 +++++++ .../type-conditional-jsdoc/schema.json | 24 ++++++++++++++++++- 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/NodeParser/AnnotatedNodeParser.ts b/src/NodeParser/AnnotatedNodeParser.ts index b44818e37..b12c9beb2 100644 --- a/src/NodeParser/AnnotatedNodeParser.ts +++ b/src/NodeParser/AnnotatedNodeParser.ts @@ -20,6 +20,9 @@ export class AnnotatedNodeParser implements SubNodeParser { public createType(node: ts.Node, context: Context, reference?: ReferenceType): BaseType { const baseType = this.childNodeParser.createType(node, context, reference); + if (node.getSourceFile().fileName.match(/[\/\\]typescript[\/\\]lib[\/\\]lib\.[^/\\]+\.d\.ts$/i)) { + return baseType; + } const annotatedNode = this.getAnnotatedNode(node); const annotations = this.annotationsReader.getAnnotations(annotatedNode); const nullable = this.annotationsReader instanceof ExtendedAnnotationsReader ? diff --git a/test/valid-data/type-conditional-jsdoc/main.ts b/test/valid-data/type-conditional-jsdoc/main.ts index 4754919dd..336690e29 100644 --- a/test/valid-data/type-conditional-jsdoc/main.ts +++ b/test/valid-data/type-conditional-jsdoc/main.ts @@ -6,6 +6,8 @@ type NumberOrString = number | string; type NoString = T extends string ? never : T; +type BooleanOrNumberOrString = NumberOrString | boolean; + /** * No string * @pattern bar @@ -27,4 +29,10 @@ export type MyObject = { /** Description of f */ f: NoStringDocumented; + + g: Exclude; + + h: Exclude; + + i: Exclude; }; diff --git a/test/valid-data/type-conditional-jsdoc/schema.json b/test/valid-data/type-conditional-jsdoc/schema.json index 204fc0701..2ba254610 100644 --- a/test/valid-data/type-conditional-jsdoc/schema.json +++ b/test/valid-data/type-conditional-jsdoc/schema.json @@ -37,6 +37,25 @@ "description": "Description of f", "pattern": "bar", "type": "number" + }, + "g": { + "description": "Number or string", + "pattern": "foo", + "type": [ + "number", + "string" + ] + }, + "h": { + "type": "string" + }, + "i": { + "description": "Number or string", + "pattern": "foo", + "type": [ + "number", + "string" + ] } }, "required": [ @@ -45,7 +64,10 @@ "c", "d", "e", - "f" + "f", + "g", + "h", + "i" ], "type": "object" }