Skip to content

Commit 8e31e1a

Browse files
authored
feat: Support variants of union-style enums (#1426)
* feat: Support variants of union-style enums * finish impl * Add tests * impl all tests + swift + kotlin * fix: align variant fixtures with generated specs * fix: update variant test expectations * fix: update someVariant expectation
1 parent 8fbd420 commit 8e31e1a

53 files changed

Lines changed: 980 additions & 567 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

example/src/getTests.ts

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1023,7 +1023,7 @@ export function getTests(
10231023
// @ts-expect-error
10241024
(testObject.someVariant = false)
10251025
).didThrow(
1026-
`Error: ${testObject.name}.someVariant: Cannot convert "false" to any type in ${debugOnly('variant<std::string, double>!')}`
1026+
`Error: ${testObject.name}.someVariant: Cannot convert "false" to any type in ${debugOnly('variant<double, std::string>!')}`
10271027
)
10281028
),
10291029

@@ -1147,15 +1147,39 @@ export function getTests(
11471147
createTest('getVariantEnum(...) throws at wrong type (string)', () =>
11481148
// @ts-expect-error
11491149
it(() => testObject.getVariantEnum('string')).didThrow(
1150-
`Error: ${testObject.name}.getVariantEnum(...): Cannot convert "string" to any type in ${debugOnly('variant<bool, margelo::nitro::test::OldEnum>!')}`
1150+
`Error: ${testObject.name}.getVariantEnum(...): Cannot convert "string" to any type in ${debugOnly('variant<margelo::nitro::test::OldEnum, bool>!')}`
11511151
)
11521152
),
11531153
createTest('getVariantEnum(...) throws at too high numerical value', () =>
11541154
// @ts-expect-error
11551155
it(() => testObject.getVariantEnum(9999)).didThrow(
1156-
`Error: ${testObject.name}.getVariantEnum(...): Cannot convert "9999" to any type in ${debugOnly('variant<bool, margelo::nitro::test::OldEnum>!')}`
1156+
`Error: ${testObject.name}.getVariantEnum(...): Cannot convert "9999" to any type in ${debugOnly('variant<margelo::nitro::test::OldEnum, bool>!')}`
11571157
)
11581158
),
1159+
createTest("bounceVariantUnionEnum(...) converts 'gas'", () =>
1160+
it(() => testObject.bounceVariantUnionEnum('gas'))
1161+
.didNotThrow()
1162+
.equals('gas')
1163+
),
1164+
createTest('bounceVariantUnionEnum(...) converts Car', () =>
1165+
it(() => testObject.bounceVariantUnionEnum(TEST_CAR))
1166+
.didNotThrow()
1167+
.equals(TEST_CAR)
1168+
),
1169+
createTest('bounceVariantUnionEnum(...) throws at wrong string', () =>
1170+
// @ts-expect-error
1171+
it(() => testObject.bounceVariantUnionEnum('diesel')).didThrow()
1172+
),
1173+
createTest('bounceVariantUnionEnum(...) throws at wrong object', () =>
1174+
// @ts-expect-error
1175+
it(() => testObject.bounceVariantUnionEnum({})).didThrow()
1176+
),
1177+
createTest(
1178+
'bounceVariantUnionEnum(...) throws at wrong type (boolean)',
1179+
() =>
1180+
// @ts-expect-error
1181+
it(() => testObject.bounceVariantUnionEnum(true)).didThrow()
1182+
),
11591183
createTest('getVariantWeirdNumbersEnum(...) converts enum', () =>
11601184
it(() => testObject.getVariantWeirdNumbersEnum(WeirdNumbersEnum.C))
11611185
.didNotThrow()
@@ -1171,15 +1195,15 @@ export function getTests(
11711195
() =>
11721196
// @ts-expect-error
11731197
it(() => testObject.getVariantWeirdNumbersEnum('string')).didThrow(
1174-
`Error: ${testObject.name}.getVariantWeirdNumbersEnum(...): Cannot convert "string" to any type in ${debugOnly('variant<bool, margelo::nitro::test::WeirdNumbersEnum>!')}`
1198+
`Error: ${testObject.name}.getVariantWeirdNumbersEnum(...): Cannot convert "string" to any type in ${debugOnly('variant<margelo::nitro::test::WeirdNumbersEnum, bool>!')}`
11751199
)
11761200
),
11771201
createTest(
11781202
'getVariantWeirdNumbersEnum(...) throws at too high numerical value',
11791203
() =>
11801204
// @ts-expect-error
11811205
it(() => testObject.getVariantWeirdNumbersEnum(99999)).didThrow(
1182-
`Error: ${testObject.name}.getVariantWeirdNumbersEnum(...): Cannot convert "99999" to any type in ${debugOnly('variant<bool, margelo::nitro::test::WeirdNumbersEnum>!')}`
1206+
`Error: ${testObject.name}.getVariantWeirdNumbersEnum(...): Cannot convert "99999" to any type in ${debugOnly('variant<margelo::nitro::test::WeirdNumbersEnum, bool>!')}`
11831207
)
11841208
),
11851209
createTest('getVariantObjects(...) converts Person', () =>
@@ -1202,7 +1226,7 @@ export function getTests(
12021226
createTest('getVariantObjects(...) throws at wrong type (string)', () =>
12031227
// @ts-expect-error
12041228
it(() => testObject.getVariantObjects('some-string')).didThrow(
1205-
`Error: ${testObject.name}.getVariantObjects(...): Cannot convert "some-string" to any type in ${debugOnly('variant<margelo::nitro::test::Car, margelo::nitro::test::Person>!')}`
1229+
`Error: ${testObject.name}.getVariantObjects(...): Cannot convert "some-string" to any type in ${debugOnly('variant<margelo::nitro::test::Person, margelo::nitro::test::Car>!')}`
12061230
)
12071231
),
12081232
createTest(
@@ -1212,7 +1236,7 @@ export function getTests(
12121236
// @ts-expect-error
12131237
testObject.getVariantObjects({ someValue: 55 })
12141238
).didThrow(
1215-
`Error: ${testObject.name}.getVariantObjects(...): Cannot convert "[object Object]" to any type in ${debugOnly('variant<margelo::nitro::test::Car, margelo::nitro::test::Person>!')}`
1239+
`Error: ${testObject.name}.getVariantObjects(...): Cannot convert "[object Object]" to any type in ${debugOnly('variant<margelo::nitro::test::Person, margelo::nitro::test::Car>!')}`
12161240
)
12171241
),
12181242
createTest('getVariantHybrid(...) converts Hybrid', () =>

packages/nitrogen/src/createPlatformSpec.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,14 +113,24 @@ function getHybridObjectSpec(type: Type, language: Language): HybridObjectSpec {
113113
const t = declaration.getType()
114114
const isOptional =
115115
prop.isOptional() || t.getUnionTypes().some((u) => u.isUndefined())
116-
const propType = createType(language, t, isOptional)
116+
const propType = createType(
117+
language,
118+
t,
119+
isOptional,
120+
declaration.getTypeNode()
121+
)
117122
properties.push(
118123
new Property(prop.getName(), propType, declaration.isReadonly())
119124
)
120125
} else if (Node.isMethodSignature(declaration)) {
121126
const returnType = declaration.getReturnType()
122127
const isOptional = returnType.getUnionTypes().some((t) => t.isUndefined())
123-
const methodReturnType = createType(language, returnType, isOptional)
128+
const methodReturnType = createType(
129+
language,
130+
returnType,
131+
isOptional,
132+
declaration.getReturnTypeNode()
133+
)
124134
const methodParameters = declaration
125135
.getParameters()
126136
.map((p) => new Parameter(p, language))

packages/nitrogen/src/syntax/Parameter.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,13 @@ export class Parameter implements CodeNode {
2626
const type = param.getType()
2727
const isOptional =
2828
param.hasQuestionToken() || param.isOptional() || type.isNullable()
29-
this.type = createNamedType(language, name, type, isOptional)
29+
this.type = createNamedType(
30+
language,
31+
name,
32+
type,
33+
isOptional,
34+
param.getTypeNode()
35+
)
3036
} else {
3137
// constructor(...)???
3238
throw new Error(`Invalid constructor! Arguments: ${args}`)

packages/nitrogen/src/syntax/createType.ts

Lines changed: 67 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ts, Type as TSMorphType, type Signature } from 'ts-morph'
1+
import { Node, ts, Type as TSMorphType, type Signature } from 'ts-morph'
22
import type { Type } from './types/Type.js'
33
import { BooleanType } from './types/BooleanType.js'
44
import { NumberType } from './types/NumberType.js'
@@ -83,6 +83,54 @@ type Tuple<
8383
N extends number,
8484
R extends unknown[] = [],
8585
> = R['length'] extends N ? R : Tuple<T, N, [T, ...R]>
86+
87+
type TypeNode = Node<ts.TypeNode>
88+
89+
interface UnionConstituent {
90+
type: TSMorphType
91+
typeNode?: TypeNode
92+
}
93+
94+
function getAliasTypeNode(type: TSMorphType): TypeNode | undefined {
95+
const symbol = type.getNonNullableType().getAliasSymbol()
96+
const declaration = symbol?.getDeclarations()[0]
97+
if (declaration != null && Node.isTypeAliasDeclaration(declaration)) {
98+
return declaration.getTypeNode()
99+
}
100+
return undefined
101+
}
102+
103+
function getUnionTypeNodes(
104+
type: TSMorphType,
105+
typeNode?: TypeNode
106+
): TypeNode[] | undefined {
107+
if (typeNode != null && Node.isUnionTypeNode(typeNode)) {
108+
return typeNode.getTypeNodes()
109+
}
110+
111+
const aliasTypeNode = getAliasTypeNode(type)
112+
if (aliasTypeNode != null && Node.isUnionTypeNode(aliasTypeNode)) {
113+
return aliasTypeNode.getTypeNodes()
114+
}
115+
116+
return undefined
117+
}
118+
119+
function getUnionConstituents(
120+
type: TSMorphType,
121+
typeNode?: TypeNode
122+
): UnionConstituent[] {
123+
const typeNodes = getUnionTypeNodes(type, typeNode)
124+
if (typeNodes != null) {
125+
return typeNodes.map((node) => ({
126+
type: node.getType(),
127+
typeNode: node,
128+
}))
129+
}
130+
131+
return type.getUnionTypes().map((unionType) => ({ type: unionType }))
132+
}
133+
86134
function getArguments<N extends number>(
87135
type: TSMorphType,
88136
typename: string,
@@ -107,14 +155,18 @@ export function createNamedType(
107155
language: Language,
108156
name: string,
109157
type: TSMorphType,
110-
isOptional: boolean
158+
isOptional: boolean,
159+
typeNode?: TypeNode
111160
) {
112161
if (name.startsWith('__')) {
113162
throw new Error(
114163
`Name cannot start with two underscores (__) as this is reserved syntax for Nitrogen! (In ${type.getText()})`
115164
)
116165
}
117-
return new NamedWrappingType(name, createType(language, type, isOptional))
166+
return new NamedWrappingType(
167+
name,
168+
createType(language, type, isOptional, typeNode)
169+
)
118170
}
119171

120172
export function createVoidType(): Type {
@@ -171,7 +223,8 @@ export function addKnownType(
171223
export function createType(
172224
language: Language,
173225
type: TSMorphType,
174-
isOptional: boolean
226+
isOptional: boolean,
227+
typeNode?: TypeNode
175228
): Type {
176229
const key = getTypeId(type, isOptional)
177230
if (key != null && knownTypes[language].has(key)) {
@@ -183,7 +236,7 @@ export function createType(
183236

184237
const get = () => {
185238
if (isOptional) {
186-
const wrapping = createType(language, type, false)
239+
const wrapping = createType(language, type, false, typeNode)
187240
return new OptionalType(wrapping)
188241
}
189242

@@ -286,10 +339,10 @@ export function createType(
286339
// - of string literals (then it's an enum)
287340
// - of type `T | undefined` (then it's just optional `T`)
288341
// - of different types (then it's a variant `A | B | C`)
289-
const types = type.getUnionTypes()
290-
const nonNullTypes = types.filter(
291-
(t) => !t.isNull() && !t.isUndefined() && !t.isVoid()
292-
)
342+
const unionConstituents = getUnionConstituents(type, typeNode)
343+
const nonNullTypes = unionConstituents
344+
.map((c) => c.type)
345+
.filter((t) => !t.isNull() && !t.isUndefined() && !t.isVoid())
293346
const isEnumUnion = nonNullTypes.every((t) => t.isStringLiteral())
294347
if (isEnumUnion) {
295348
// It consists only of string literaly - that means it's describing an enum!
@@ -305,11 +358,12 @@ export function createType(
305358
return new EnumType(typename, type)
306359
} else {
307360
// It consists of different types - that means it's a variant!
308-
let variants = type
309-
.getUnionTypes()
361+
let variants = unionConstituents
310362
// Filter out any undefineds/voids, as those are already treated as `isOptional`.
311-
.filter((t) => !t.isUndefined() && !t.isVoid())
312-
.map((t) => createType(language, t, false))
363+
.filter(({ type: t }) => !t.isUndefined() && !t.isVoid())
364+
.map(({ type: t, typeNode: node }) =>
365+
createType(language, t, false, node)
366+
)
313367
.toSorted(compareLooselyness)
314368
variants = removeDuplicates(variants)
315369

packages/nitrogen/src/syntax/getInterfaceProperties.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ts, Type } from 'ts-morph'
1+
import { Node, type ts, type Type } from 'ts-morph'
22
import type { NamedType } from './types/Type.js'
33
import { createNamedType } from './createType.js'
44
import type { Language } from '../getPlatformSpecs.js'
@@ -13,6 +13,9 @@ export function getInterfaceProperties(
1313
`Interface "${interfaceType.getText()}" does not have a Symbol!`
1414
)
1515
return interfaceType.getProperties().map((prop) => {
16+
const propDeclaration = prop
17+
.getDeclarations()
18+
.find((declaration) => Node.isPropertySignature(declaration))
1619
let propType = prop.getDeclaredType()
1720
if (propType.isAny() || propType.isUnknown()) {
1821
// the interface is aliased/merged - we need to look into the actual declaration
@@ -29,7 +32,8 @@ export function getInterfaceProperties(
2932
language,
3033
prop.getName(),
3134
propType,
32-
prop.isOptional() || propType.isNullable()
35+
prop.isOptional() || propType.isNullable(),
36+
propDeclaration?.getTypeNode()
3337
)
3438
return refType
3539
})

packages/react-native-nitro-test/android/src/main/java/com/margelo/nitro/test/HybridTestObjectKotlin.kt

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class HybridTestObjectKotlin : HybridTestObjectSwiftKotlinSpec() {
3030
override var optionalHybrid: HybridTestObjectSwiftKotlinSpec? = null
3131
override val thisObject: HybridTestObjectSwiftKotlinSpec
3232
get() = this
33-
override var someVariant: Variant_String_Double = Variant_String_Double.create(55.05)
33+
override var someVariant: Variant_Double_String = Variant_Double_String.create(55.05)
3434
override var optionalArray: Array<String>? = null
3535
override var optionalEnum: Powertrain? = null
3636
override var optionalOldEnum: OldEnum? = null
@@ -497,13 +497,13 @@ class HybridTestObjectKotlin : HybridTestObjectSwiftKotlinSpec() {
497497
return Promise.async { createArrayBuffer() }
498498
}
499499

500-
override fun passVariant(either: Variant_Boolean_DoubleArray_Array_String__String_Double): Variant_String_Double {
500+
override fun passVariant(either: Variant_Boolean_DoubleArray_Array_String__Double_String): Variant_Double_String {
501501
return either.match(
502-
{ bool -> Variant_String_Double.create("holds something else!") },
503-
{ doubleArray -> Variant_String_Double.create("holds something else!") },
504-
{ stringArray -> Variant_String_Double.create("holds something else!") },
505-
{ string -> Variant_String_Double.create(string) },
506-
{ double -> Variant_String_Double.create(double) },
502+
{ bool -> Variant_Double_String.create("holds something else!") },
503+
{ doubleArray -> Variant_Double_String.create("holds something else!") },
504+
{ stringArray -> Variant_Double_String.create("holds something else!") },
505+
{ double -> Variant_Double_String.create(double) },
506+
{ string -> Variant_Double_String.create(string) },
507507
)
508508
}
509509

@@ -515,15 +515,19 @@ class HybridTestObjectKotlin : HybridTestObjectSwiftKotlinSpec() {
515515
return variant
516516
}
517517

518-
override fun getVariantEnum(variant: Variant_Boolean_OldEnum): Variant_Boolean_OldEnum {
518+
override fun getVariantEnum(variant: Variant_OldEnum_Boolean): Variant_OldEnum_Boolean {
519519
return variant
520520
}
521521

522-
override fun getVariantWeirdNumbersEnum(variant: Variant_Boolean_WeirdNumbersEnum): Variant_Boolean_WeirdNumbersEnum {
522+
override fun bounceVariantUnionEnum(variant: Variant_Powertrain_Car): Variant_Powertrain_Car {
523523
return variant
524524
}
525525

526-
override fun getVariantObjects(variant: Variant_Car_Person): Variant_Car_Person {
526+
override fun getVariantWeirdNumbersEnum(variant: Variant_WeirdNumbersEnum_Boolean): Variant_WeirdNumbersEnum_Boolean {
527+
return variant
528+
}
529+
530+
override fun getVariantObjects(variant: Variant_Person_Car): Variant_Person_Car {
527531
return variant
528532
}
529533

packages/react-native-nitro-test/cpp/HybridTestObjectCpp.cpp

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,11 @@ void HybridTestObjectCpp::setOptionalHybrid(const std::optional<std::shared_ptr<
107107
_optionalHybrid = optionalHybrid;
108108
}
109109

110-
std::variant<std::string, double> HybridTestObjectCpp::getSomeVariant() {
110+
std::variant<double, std::string> HybridTestObjectCpp::getSomeVariant() {
111111
return _variant;
112112
}
113113

114-
void HybridTestObjectCpp::setSomeVariant(const std::variant<std::string, double>& variant) {
114+
void HybridTestObjectCpp::setSomeVariant(const std::variant<double, std::string>& variant) {
115115
_variant = variant;
116116
}
117117

@@ -366,8 +366,8 @@ std::chrono::system_clock::time_point HybridTestObjectCpp::currentDate() {
366366
return std::chrono::system_clock::now();
367367
}
368368

369-
std::variant<std::string, double>
370-
HybridTestObjectCpp::passVariant(const std::variant<bool, std::vector<double>, std::vector<std::string>, std::string, double>& either) {
369+
std::variant<double, std::string>
370+
HybridTestObjectCpp::passVariant(const std::variant<bool, std::vector<double>, std::vector<std::string>, double, std::string>& either) {
371371
if (std::holds_alternative<std::string>(either)) {
372372
return std::get<std::string>(either);
373373
} else if (std::holds_alternative<double>(either)) {
@@ -386,15 +386,19 @@ ComplexVariant HybridTestObjectCpp::bounceComplexVariant(const ComplexVariant& v
386386
return variant;
387387
}
388388

389-
std::variant<bool, OldEnum> HybridTestObjectCpp::getVariantEnum(const std::variant<bool, OldEnum>& variant) {
389+
std::variant<OldEnum, bool> HybridTestObjectCpp::getVariantEnum(const std::variant<OldEnum, bool>& variant) {
390390
return variant;
391391
}
392392

393-
std::variant<bool, WeirdNumbersEnum> HybridTestObjectCpp::getVariantWeirdNumbersEnum(const std::variant<bool, WeirdNumbersEnum>& variant) {
393+
std::variant<Powertrain, Car> HybridTestObjectCpp::bounceVariantUnionEnum(const std::variant<Powertrain, Car>& variant) {
394394
return variant;
395395
}
396396

397-
std::variant<Car, Person> HybridTestObjectCpp::getVariantObjects(const std::variant<Car, Person>& variant) {
397+
std::variant<WeirdNumbersEnum, bool> HybridTestObjectCpp::getVariantWeirdNumbersEnum(const std::variant<WeirdNumbersEnum, bool>& variant) {
398+
return variant;
399+
}
400+
401+
std::variant<Person, Car> HybridTestObjectCpp::getVariantObjects(const std::variant<Person, Car>& variant) {
398402
return variant;
399403
}
400404

@@ -403,6 +407,11 @@ HybridTestObjectCpp::getVariantHybrid(const std::variant<std::shared_ptr<HybridT
403407
return variant;
404408
}
405409

410+
std::variant<std::shared_ptr<HybridTestObjectCppSpec>, Powertrain>
411+
HybridTestObjectCpp::getVariantHybridEnum(const std::variant<std::shared_ptr<HybridTestObjectCppSpec>, Powertrain>& variant) {
412+
return variant;
413+
}
414+
406415
std::variant<std::tuple<double, double>, std::tuple<double, double, double>>
407416
HybridTestObjectCpp::getVariantTuple(const std::variant<std::tuple<double, double>, std::tuple<double, double, double>>& variant) {
408417
return variant;

0 commit comments

Comments
 (0)