Skip to content

Commit 4ea0b82

Browse files
rix0rrrgithub-actions
andauthored
feat: intersection types (#2325)
This adds support for intersection types to the jsii compiler. There are a number of restrictions to the use of intersection types: - They can only appear in argument position, or in structs that are exclusively used as the input to functions. - Not as return values of functions - Not in structs that are returned by functions - Not as arguments to constructors - Not as property types - They must intersect only (behavioral) interface types. - Not structs - Not primitives - Not class types - All shared members of interfaces that are intersected must have the same types and arguments as each other. --- By submitting this pull request, I confirm that my contribution is made under the terms of the [Apache 2.0 license]. [Apache 2.0 license]: https://www.apache.org/licenses/LICENSE-2.0 --------- Signed-off-by: github-actions <[email protected]> Co-authored-by: github-actions <[email protected]>
1 parent f230103 commit 4ea0b82

22 files changed

+3236
-68
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/assembler.ts

Lines changed: 341 additions & 41 deletions
Large diffs are not rendered by default.

src/helpers.ts

Lines changed: 75 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import * as os from 'node:os';
1111
import * as path from 'node:path';
1212
import { PackageJson, loadAssemblyFromPath, writeAssembly } from '@jsii/spec';
1313
import * as spec from '@jsii/spec';
14-
import { DiagnosticCategory } from 'typescript';
14+
import { Diagnostic, DiagnosticCategory } from 'typescript';
1515

1616
import { Compiler, CompilerOptions } from './compiler';
1717
import { loadProjectInfo, ProjectInfo } from './project-info';
@@ -25,6 +25,11 @@ export type MultipleSourceFiles = {
2525
[name: string]: string;
2626
};
2727

28+
/**
29+
* Assembly features supported by this compiler
30+
*/
31+
export const ASSEMBLY_FEATURES_SUPPORTED: spec.JsiiFeature[] = ['intersection-types'];
32+
2833
/**
2934
* Compile a piece of source and return the JSII assembly for it
3035
*
@@ -39,10 +44,24 @@ export function sourceToAssemblyHelper(
3944
source: string | MultipleSourceFiles,
4045
options?: TestCompilationOptions | ((obj: PackageJson) => void),
4146
): spec.Assembly {
42-
return compileJsiiForTest(source, options).assembly;
47+
const result = compileJsiiForTest(source, options);
48+
if (result.type !== 'success') {
49+
throw new Error('Compilation failed');
50+
}
51+
return result.assembly;
4352
}
4453

54+
export type HelperCompilationOut = HelperCompilationResult | HelperCompilationFailure;
55+
56+
/**
57+
* Successful output of a compilation command (for testing)
58+
*
59+
* A better name would have been `HelperCompilationSuccess`, but the name is part of
60+
* the public API surface, so we keep it like this.
61+
*/
4562
export interface HelperCompilationResult {
63+
readonly type: 'success';
64+
4665
/**
4766
* The generated assembly
4867
*/
@@ -62,6 +81,22 @@ export interface HelperCompilationResult {
6281
* Whether to compress the assembly file
6382
*/
6483
readonly compressAssembly: boolean;
84+
85+
/**
86+
* Diagnostics that occurred during compilation
87+
*/
88+
readonly diagnostics: readonly Diagnostic[];
89+
}
90+
91+
export interface HelperCompilationFailure {
92+
readonly type: 'failure';
93+
94+
/**
95+
* Diagnostics that occurred during compilation
96+
*
97+
* Contains at least one error.
98+
*/
99+
readonly diagnostics: readonly Diagnostic[];
65100
}
66101

67102
/**
@@ -74,11 +109,11 @@ export interface HelperCompilationResult {
74109
* @param options accepts a callback for historical reasons but really expects to
75110
* take an options object.
76111
*/
77-
export function compileJsiiForTest(
112+
export function compileJsiiForTest<O extends TestCompilationOptions>(
78113
source: string | { 'index.ts': string; [name: string]: string },
79-
options?: TestCompilationOptions | ((obj: PackageJson) => void),
114+
options?: O | ((obj: PackageJson) => void),
80115
compilerOptions?: Omit<CompilerOptions, 'projectInfo' | 'watch'>,
81-
): HelperCompilationResult {
116+
): ResultOrSuccess<O> {
82117
if (typeof source === 'string') {
83118
source = { 'index.ts': source };
84119
}
@@ -108,14 +143,25 @@ export function compileJsiiForTest(
108143
const emitResult = compiler.emit();
109144

110145
const errors = emitResult.diagnostics.filter((d) => d.category === DiagnosticCategory.Error);
111-
for (const error of errors) {
112-
console.error(formatDiagnostic(error, projectInfo.projectRoot));
113-
// logDiagnostic() doesn't work out of the box, so console.error() it is.
146+
147+
if (typeof options !== 'object' || !options?.captureDiagnostics) {
148+
for (const error of errors) {
149+
console.error(formatDiagnostic(error, projectInfo.projectRoot));
150+
// logDiagnostic() doesn't work out of the box, so console.error() it is.
151+
}
114152
}
153+
115154
if (errors.length > 0 || emitResult.emitSkipped) {
155+
if (typeof options === 'object' && options?.captureDiagnostics) {
156+
return {
157+
type: 'failure',
158+
diagnostics: emitResult.diagnostics,
159+
} satisfies HelperCompilationFailure;
160+
}
116161
throw new JsiiError('There were compiler errors');
117162
}
118-
const assembly = loadAssemblyFromPath(process.cwd(), false);
163+
164+
const assembly = loadAssemblyFromPath(process.cwd(), false, ASSEMBLY_FEATURES_SUPPORTED);
119165
const files: Record<string, string> = {};
120166

121167
for (const filename of Object.keys(source)) {
@@ -141,14 +187,20 @@ export function compileJsiiForTest(
141187
}
142188

143189
return {
190+
type: 'success',
144191
assembly,
145192
files,
146193
packageJson,
147194
compressAssembly: isOptionsObject(options) && options.compressAssembly ? true : false,
148-
} as HelperCompilationResult;
149-
});
195+
diagnostics: emitResult.diagnostics,
196+
} satisfies HelperCompilationResult;
197+
}) as ResultOrSuccess<O>;
150198
}
151199

200+
type ResultOrSuccess<O extends TestCompilationOptions> = O['captureDiagnostics'] extends true
201+
? HelperCompilationOut
202+
: HelperCompilationResult;
203+
152204
function inTempDir<T>(block: () => T): T {
153205
const origDir = process.cwd();
154206
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jsii'));
@@ -236,6 +288,13 @@ export interface TestCompilationOptions {
236288
* @default false
237289
*/
238290
readonly compressAssembly?: boolean;
291+
292+
/**
293+
* Whether or not to print the diagnostics
294+
*
295+
* @default false
296+
*/
297+
readonly captureDiagnostics?: boolean;
239298
}
240299

241300
function isOptionsObject(
@@ -278,7 +337,11 @@ export class TestWorkspace {
278337
/**
279338
* Add a test-compiled jsii assembly as a dependency
280339
*/
281-
public addDependency(dependencyAssembly: HelperCompilationResult) {
340+
public addDependency(dependencyAssembly: HelperCompilationOut) {
341+
if (dependencyAssembly.type !== 'success') {
342+
throw new JsiiError('Cannot add dependency: assembly compilation failed');
343+
}
344+
282345
if (this.installed.has(dependencyAssembly.assembly.name)) {
283346
throw new JsiiError(
284347
`A dependency with name '${dependencyAssembly.assembly.name}' was already installed. Give one a different name.`,

src/jsii-diagnostic.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,37 @@ export class JsiiDiagnostic implements ts.Diagnostic {
294294
name: 'typescript-restriction/generic-type',
295295
});
296296

297+
public static readonly JSII_1007_NEVER_TYPE = Code.error({
298+
code: 1007,
299+
formatter: () => 'The "never" type is not allowed because it cannot be represented in target languages.',
300+
name: 'typescript-restriction/no-never-type',
301+
});
302+
303+
public static readonly JSII_1008_ONLY_INTERFACE_INTERSECTION = Code.error({
304+
code: 1008,
305+
formatter: (type: string) => `Found non-interface type in type intersection: ${type}`,
306+
name: 'typescript-restriction/only-interface-intersection',
307+
});
308+
309+
public static readonly JSII_1009_INTERSECTION_ONLY_INPUT = Code.error({
310+
code: 1009,
311+
formatter: (location: string) => `Intersection types may only be used as inputs, but ${location} is used as output`,
312+
name: 'typescript-restriction/intersection-only-input',
313+
});
314+
315+
public static readonly JSII_1010_INTERSECTION_NOT_IN_CTOR = Code.error({
316+
code: 1010,
317+
formatter: () => `Intersection types cannot be used as constructor arguments`,
318+
name: 'typescript-restriction/intersection-no-ctor',
319+
});
320+
321+
public static readonly JSII_1011_INTERSECTION_MEMBER_DIFFERENT = Code.error({
322+
code: 1011,
323+
formatter: (member: string, type1: string, reason1: string, type2: string, reason2: string) =>
324+
`Member ${member} is different between types in a type intersection: ${type1} (${reason1}) and ${type2} (${reason2})`,
325+
name: 'typescript-restriction/intersection-member-different',
326+
});
327+
297328
public static readonly JSII_1999_UNSUPPORTED = Code.error({
298329
code: 1999,
299330
formatter: ({

src/project-info.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as semver from 'semver';
77
import * as ts from 'typescript';
88

99
import { findDependencyDirectory } from './common/find-utils';
10+
import { ASSEMBLY_FEATURES_SUPPORTED } from './helpers';
1011
import { JsiiDiagnostic } from './jsii-diagnostic';
1112
import { TypeScriptConfigValidationRuleSet } from './tsconfig';
1213
import { JsiiError, parsePerson, parseRepository } from './utils';
@@ -392,7 +393,7 @@ class DependencyResolver {
392393
return this.cache.get(jsiiFile)!;
393394
}
394395

395-
const assembly = loadAssemblyFromFile(jsiiFile);
396+
const assembly = loadAssemblyFromFile(jsiiFile, true, ASSEMBLY_FEATURES_SUPPORTED);
396397

397398
// Continue loading any dependencies declared in the asm
398399
const resolvedDependencies = assembly.dependencies

src/sets.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
export abstract class Sets {
2+
/**
3+
* Return the intersection of N sets
4+
*/
5+
public static intersection<T>(...xss: Array<Set<T>>): Set<T> {
6+
if (xss.length === 0) {
7+
return new Set();
8+
}
9+
const ret = new Set(xss[0]);
10+
for (const x of xss[0]) {
11+
if (!xss.every((xs) => xs.has(x))) {
12+
ret.delete(x);
13+
}
14+
}
15+
return ret;
16+
}
17+
18+
/**
19+
* Return the union of N sets
20+
*/
21+
public static union<T>(...xss: Array<Set<T>>): Set<T> {
22+
return new Set(xss.flatMap((xs) => Array.from(xs)));
23+
}
24+
25+
/**
26+
* Return the diff of 2 sets
27+
*/
28+
public static diff<T>(xs: Set<T>, ys: Set<T>) {
29+
return new Set(Array.from(xs).filter((x) => !ys.has(x)));
30+
}
31+
32+
public static *intersect<T>(xs: Set<T>, ys: Set<T>) {
33+
for (const x of xs) {
34+
if (ys.has(x)) {
35+
yield x;
36+
}
37+
}
38+
}
39+
}

src/type-reference.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import * as spec from '@jsii/spec';
2+
import { visitTypeReference } from './type-visitor';
3+
4+
/**
5+
* Convert a type reference to a string
6+
*/
7+
export function typeReferenceToString(x: spec.TypeReference): string {
8+
return visitTypeReference<string>(x, {
9+
named: function (ref: spec.NamedTypeReference) {
10+
return ref.fqn;
11+
},
12+
primitive: function (ref: spec.PrimitiveTypeReference) {
13+
return ref.primitive;
14+
},
15+
collection: function (ref: spec.CollectionTypeReference) {
16+
return `${ref.collection.kind}<${typeReferenceToString(ref.collection.elementtype)}>`;
17+
},
18+
union: function (ref: spec.UnionTypeReference) {
19+
return ref.union.types.map(typeReferenceToString).join(' | ');
20+
},
21+
intersection: function (ref: spec.IntersectionTypeReference) {
22+
return ref.intersection.types.map(typeReferenceToString).join(' & ');
23+
},
24+
});
25+
}
26+
27+
/**
28+
* Return whether the given type references are equal
29+
*/
30+
export function typeReferenceEqual(a: spec.TypeReference, b: spec.TypeReference): boolean {
31+
if (spec.isNamedTypeReference(a) && spec.isNamedTypeReference(b)) {
32+
return a.fqn === b.fqn;
33+
}
34+
if (spec.isPrimitiveTypeReference(a) && spec.isPrimitiveTypeReference(b)) {
35+
return a.primitive === b.primitive;
36+
}
37+
if (spec.isCollectionTypeReference(a) && spec.isCollectionTypeReference(b)) {
38+
return (
39+
a.collection.kind === b.collection.kind && typeReferenceEqual(a.collection.elementtype, b.collection.elementtype)
40+
);
41+
}
42+
if (spec.isUnionTypeReference(a) && spec.isUnionTypeReference(b)) {
43+
return (
44+
a.union.types.length === b.union.types.length &&
45+
a.union.types.every((aType, i) => typeReferenceEqual(aType, b.union.types[i]))
46+
);
47+
}
48+
if (spec.isIntersectionTypeReference(a) && spec.isIntersectionTypeReference(b)) {
49+
return (
50+
a.intersection.types.length === b.intersection.types.length &&
51+
a.intersection.types.every((aType, i) => typeReferenceEqual(aType, b.intersection.types[i]))
52+
);
53+
}
54+
return false;
55+
}

src/type-visitor.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import * as spec from '@jsii/spec';
2+
3+
export interface TypeReferenceVisitor<A = void> {
4+
named(ref: spec.NamedTypeReference): A;
5+
primitive(ref: spec.PrimitiveTypeReference): A;
6+
collection(ref: spec.CollectionTypeReference): A;
7+
union(ref: spec.UnionTypeReference): A;
8+
intersection(ref: spec.IntersectionTypeReference): A;
9+
}
10+
11+
export function visitTypeReference<A>(typeRef: spec.TypeReference, visitor: TypeReferenceVisitor<A>) {
12+
if (spec.isNamedTypeReference(typeRef)) {
13+
return visitor.named(typeRef);
14+
} else if (spec.isPrimitiveTypeReference(typeRef)) {
15+
return visitor.primitive(typeRef);
16+
} else if (spec.isCollectionTypeReference(typeRef)) {
17+
return visitor.collection(typeRef);
18+
} else if (spec.isUnionTypeReference(typeRef)) {
19+
return visitor.union(typeRef);
20+
} else if (spec.isIntersectionTypeReference(typeRef)) {
21+
return visitor.intersection(typeRef);
22+
} else {
23+
throw new Error(`Unknown type reference: ${JSON.stringify(typeRef)}`);
24+
}
25+
}
26+
27+
export interface TypeVisitor<A = void> {
28+
classType(t: spec.ClassType): A;
29+
interfaceType(t: spec.InterfaceType): A;
30+
dataType(t: spec.InterfaceType): A;
31+
enumType(t: spec.EnumType): A;
32+
}
33+
34+
export function visitType<A>(t: spec.Type, visitor: TypeVisitor<A>) {
35+
if (spec.isClassType(t)) {
36+
return visitor.classType(t);
37+
} else if (spec.isInterfaceType(t) && t.datatype) {
38+
return visitor.dataType(t);
39+
} else if (spec.isInterfaceType(t)) {
40+
return visitor.interfaceType(t);
41+
} else if (spec.isEnumType(t)) {
42+
return visitor.enumType(t);
43+
} else {
44+
throw new Error(`Unknown type: ${JSON.stringify(t)}`);
45+
}
46+
}
47+
48+
export function isDataType(t: spec.Type): t is spec.InterfaceType {
49+
return spec.isInterfaceType(t) && !!t.datatype;
50+
}
51+
52+
export function isBehavioralInterfaceType(t: spec.Type): t is spec.InterfaceType {
53+
return spec.isInterfaceType(t) && !t.datatype;
54+
}

test/abstract.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import { PrimitiveType, Type, TypeKind } from '@jsii/spec';
22
import { sourceToAssemblyHelper } from '../lib';
3+
import { compileJsiiForErrors } from './compiler-helpers';
34

45
// ----------------------------------------------------------------------
56
test('Abstract member cant be marked async', () => {
6-
expect(() =>
7-
sourceToAssemblyHelper(`
7+
expect(
8+
compileJsiiForErrors(`
89
export abstract class AbstractClass {
910
public abstract async abstractMethod(): Promise<void>;
1011
}
1112
`),
12-
).toThrow(/There were compiler errors/);
13+
).toContainEqual(expect.stringMatching(/'async' modifier cannot be used with 'abstract' modifier/));
1314
});
1415

1516
test('Abstract member can have a Promise<void> return type', () => {

0 commit comments

Comments
 (0)