Skip to content

Commit 160a3ef

Browse files
chore: extract type analysis and resolver utilities (backport #2369) (#2387)
# Backport This will backport the following commits from `main` to `maintenance/v5.8`: - [chore: extract type analysis and resolver utilities (#2369)](#2369) <!--- Backport version: 9.5.1 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) Co-authored-by: Momo Kornher <[email protected]>
1 parent adaff16 commit 160a3ef

File tree

3 files changed

+191
-168
lines changed

3 files changed

+191
-168
lines changed

src/type-analysis.ts

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import * as spec from '@jsii/spec';
2+
import * as deepEqual from 'fast-deep-equal';
3+
import { TypeResolver } from './type-reference';
4+
5+
/**
6+
* Check if subType is an allowed covariant subtype to superType
7+
*
8+
* This is not a generic check for subtypes or covariance, but a specific implementation
9+
* that checks the currently allowed conditions for class covariance.
10+
* In practice, this is driven by C# limitations.
11+
*/
12+
export function isAllowedCovariantSubtype(
13+
subType: spec.TypeReference | undefined,
14+
superType: spec.TypeReference | undefined,
15+
dereference: TypeResolver,
16+
): boolean {
17+
// one void, while other isn't => not covariant
18+
if ((subType === undefined) !== (superType === undefined)) {
19+
return false;
20+
}
21+
22+
// Same type is always covariant
23+
if (deepEqual(subType, superType)) {
24+
return true;
25+
}
26+
27+
// Handle array collections (covariant)
28+
if (spec.isCollectionTypeReference(subType) && spec.isCollectionTypeReference(superType)) {
29+
if (subType.collection.kind === 'array' && superType.collection.kind === 'array') {
30+
return isAllowedCovariantSubtype(subType.collection.elementtype, superType.collection.elementtype, dereference);
31+
}
32+
// Maps are not allowed to be covariant in C#, so we exclude them here.
33+
// This seems to be because we use C# Dictionary to implements Maps, which are using generics and generics are not allowed to be covariant
34+
return false;
35+
}
36+
37+
// Union types are currently not allowed, because we have not seen the need for it.
38+
// Technically narrowing (removing `| Type` or subtyping) could be allowed and this works in C#.
39+
if (spec.isUnionTypeReference(subType) || spec.isUnionTypeReference(superType)) {
40+
return false;
41+
}
42+
43+
// Intersection types are invalid, because intersections are only allowed as inputs
44+
// and covariance is only allowed in outputs.
45+
if (spec.isIntersectionTypeReference(subType) || spec.isIntersectionTypeReference(superType)) {
46+
return false;
47+
}
48+
49+
// Primitives can never be covariant to each other in C#
50+
if (spec.isPrimitiveTypeReference(subType) || spec.isPrimitiveTypeReference(superType)) {
51+
return false;
52+
}
53+
54+
// We really only support covariance for named types (and lists of named types).
55+
// To be safe, let's guard against any unknown cases.
56+
if (!spec.isNamedTypeReference(subType) || !spec.isNamedTypeReference(superType)) {
57+
return false;
58+
}
59+
60+
const subTypeSpec = dereference(subType.fqn);
61+
const superTypeSpec = dereference(superType.fqn);
62+
63+
if (!subTypeSpec || !superTypeSpec) {
64+
return false;
65+
}
66+
67+
// Handle class-to-class inheritance
68+
if (spec.isClassType(subTypeSpec) && spec.isClassType(superTypeSpec)) {
69+
return _classExtendsClass(subTypeSpec, superType.fqn);
70+
}
71+
72+
// Handle interface-to-interface inheritance
73+
if (spec.isInterfaceType(subTypeSpec) && spec.isInterfaceType(superTypeSpec)) {
74+
return _interfaceExtendsInterface(subTypeSpec, superType.fqn);
75+
}
76+
77+
// Handle class implementing interface
78+
if (spec.isClassType(subTypeSpec) && spec.isInterfaceType(superTypeSpec)) {
79+
return _classImplementsInterface(subTypeSpec, superType.fqn);
80+
}
81+
82+
return false;
83+
84+
function _classExtendsClass(classType: spec.ClassType, targetFqn: string): boolean {
85+
let current = classType;
86+
while (current.base) {
87+
if (current.base === targetFqn) {
88+
return true;
89+
}
90+
const baseType = dereference(current.base);
91+
if (!spec.isClassType(baseType)) {
92+
break;
93+
}
94+
current = baseType;
95+
}
96+
return false;
97+
}
98+
99+
function _classImplementsInterface(classType: spec.ClassType, interfaceFqn: string): boolean {
100+
// Check direct interfaces
101+
if (classType.interfaces?.includes(interfaceFqn)) {
102+
return true;
103+
}
104+
105+
// Check inherited interfaces
106+
if (classType.interfaces) {
107+
for (const iface of classType.interfaces) {
108+
const ifaceType = dereference(iface);
109+
if (spec.isInterfaceType(ifaceType) && _interfaceExtendsInterface(ifaceType, interfaceFqn)) {
110+
return true;
111+
}
112+
}
113+
}
114+
115+
// Check base class interfaces
116+
if (classType.base) {
117+
const baseType = dereference(classType.base);
118+
if (spec.isClassType(baseType)) {
119+
return _classImplementsInterface(baseType, interfaceFqn);
120+
}
121+
}
122+
123+
return false;
124+
}
125+
126+
function _interfaceExtendsInterface(interfaceType: spec.InterfaceType, targetFqn: string): boolean {
127+
if (interfaceType.fqn === targetFqn) {
128+
return true;
129+
}
130+
131+
if (interfaceType.interfaces) {
132+
for (const iface of interfaceType.interfaces) {
133+
if (iface === targetFqn) {
134+
return true;
135+
}
136+
const ifaceType = dereference(iface);
137+
if (spec.isInterfaceType(ifaceType) && _interfaceExtendsInterface(ifaceType, targetFqn)) {
138+
return true;
139+
}
140+
}
141+
}
142+
143+
return false;
144+
}
145+
}

src/type-reference.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,32 @@ export function typeReferenceEqual(a: spec.TypeReference, b: spec.TypeReference)
5353
}
5454
return false;
5555
}
56+
57+
export type TypeResolver = (typeRef: string | spec.NamedTypeReference) => spec.Type | undefined;
58+
59+
/**
60+
* Creates a type resolver function for a given context (assembly + dependency closure).
61+
*/
62+
export function createTypeResolver(assembly: spec.Assembly, dependencyClosure: readonly spec.Assembly[]): TypeResolver {
63+
return (typeRef) => resolveType(typeRef, assembly, dependencyClosure);
64+
}
65+
66+
/**
67+
* Resolve a type from a name to the actual type.
68+
* Uses a given assembly and dependency closure for lookup.
69+
*/
70+
export function resolveType(
71+
typeRef: string | spec.NamedTypeReference,
72+
assembly: spec.Assembly,
73+
dependencyClosure: readonly spec.Assembly[],
74+
): spec.Type | undefined {
75+
if (typeof typeRef !== 'string') {
76+
typeRef = typeRef.fqn;
77+
}
78+
const [assm] = typeRef.split('.');
79+
if (assembly.name === assm) {
80+
return assembly.types?.[typeRef];
81+
}
82+
const foreignAssm = dependencyClosure.find((dep) => dep.name === assm);
83+
return foreignAssm?.types?.[typeRef];
84+
}

0 commit comments

Comments
 (0)