Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(jsii-diff): correctly handle assignability of type unions #500

Merged
merged 1 commit into from
May 7, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions packages/jsii-diff/lib/type-analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { flatMap } from './util';
/**
* Check whether A is a supertype of B
*
* Put differently: whether an value of type B would be assignable to a
* Put differently: whether any value of type B would be assignable to a
* variable of type A.
*
* We always check the relationship in the NEW (latest, updated) typesystem.
Expand Down Expand Up @@ -33,16 +33,7 @@ export function isSuperType(a: reflect.TypeReference, b: reflect.TypeReference,
`${b} is not assignable to ${a}`);
}

// Any element of A should accept all of B
if (a.unionOfTypes !== undefined) {
const analyses = a.unionOfTypes.map(aaa => isSuperType(aaa, b, updatedSystem));
if (analyses.some(x => x.success)) { return { success: true }; }
return failure(
`none of ${b} are assignable to ${a}`,
...flatMap(analyses, x => x.success ? [] : x.reasons)
);
}
// All potential elements of B should go into A
// Every element of B can be assigned to A
if (b.unionOfTypes !== undefined) {
const analyses = b.unionOfTypes.map(bbb => isSuperType(a, bbb, updatedSystem));
if (analyses.every(x => x.success)) { return { success: true }; }
Expand All @@ -51,6 +42,15 @@ export function isSuperType(a: reflect.TypeReference, b: reflect.TypeReference,
...flatMap(analyses, x => x.success ? [] : x.reasons)
);
}
// There should be an element of A which can accept all of B
if (a.unionOfTypes !== undefined) {
const analyses = a.unionOfTypes.map(aaa => isSuperType(aaa, b, updatedSystem));
if (analyses.some(x => x.success)) { return { success: true }; }
return failure(
`none of ${b} are assignable to ${a}`,
...flatMap(analyses, x => x.success ? [] : x.reasons)
);
}

// For named types, we'll always do a nominal typing relationship.
// That is, if in the updated typesystem someone were to use the type name
Expand Down
5 changes: 5 additions & 0 deletions packages/jsii-diff/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@
"typescript": "^3.4.3",
"yargs": "^13.2.2"
},
"repository": {
"type": "git",
"url": "https://github.com/awslabs/jsii.git",
"directory": "packages/jsii-diff"
},
"nyc": {
"reporter": [
"lcov",
Expand Down
145 changes: 145 additions & 0 deletions packages/jsii-diff/test/test.type-unions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { Test } from 'nodeunit';
import { expectError, expectNoError } from './util';

export = {

// ----------------------------------------------------------------------

async 'type unions in return structs can be the same'(test: Test) {
await expectNoError(test,
`
export interface Henk {
readonly henk: string | number;
}
export class Actions {
returnHenk(): Henk { return { henk: 'henk' }; }
}
`, `
export interface Henk {
readonly henk: string | number;
}
export class Actions {
returnHenk(): Henk { return { henk: 'henk' }; }
}
`);

test.done();
},

// ----------------------------------------------------------------------

async 'type unions in return structs can be narrowed'(test: Test) {
await expectNoError(test,
`
export interface Henk {
readonly henk: string | number;
}
export class Actions {
returnHenk(): Henk { return { henk: 'henk' }; }
}
`, `
export interface Henk {
readonly henk: string;
}
export class Actions {
returnHenk(): Henk { return { henk: 'henk' }; }
}
`);

test.done();
},

// ----------------------------------------------------------------------

async 'type unions in return structs can not be widened'(test: Test) {
await expectError(test,
/some of string \| number \| boolean are not assignable to string \| number/,
`
export interface Henk {
readonly henk: string | number;
}
export class Actions {
returnHenk(): Henk { return { henk: 'henk' }; }
}
`, `
export interface Henk {
readonly henk: string | number | boolean;
}
export class Actions {
returnHenk(): Henk { return { henk: 'henk' }; }
}
`);

test.done();
},

// ----------------------------------------------------------------------

async 'type unions in input structs can be the same'(test: Test) {
await expectNoError(test,
`
export interface Henk {
readonly henk: string | number;
}
export class Actions {
takeHenk(_henk: Henk): void { }
}
`, `
export interface Henk {
readonly henk: string | number;
}
export class Actions {
takeHenk(_henk: Henk): void { }
}
`);

test.done();
},

// ----------------------------------------------------------------------

async 'type unions in input structs can be widened'(test: Test) {
await expectNoError(test,
`
export interface Henk {
readonly henk: string | number;
}
export class Actions {
takeHenk(_henk: Henk): void { }
}
`, `
export interface Henk {
readonly henk: string | number | boolean;
}
export class Actions {
takeHenk(_henk: Henk): void { }
}
`);

test.done();
},

// ----------------------------------------------------------------------

async 'type unions in input structs can not be narrowed'(test: Test) {
await expectError(test,
/string \| number is not assignable to string/,
`
export interface Henk {
readonly henk: string | number;
}
export class Actions {
takeHenk(_henk: Henk): void { }
}
`, `
export interface Henk {
readonly henk: string;
}
export class Actions {
takeHenk(_henk: Henk): void { }
}
`);

test.done();
},
};