Skip to content

Commit

Permalink
Transform numeric unions to types like string unions
Browse files Browse the repository at this point in the history
  • Loading branch information
Vampire committed Sep 17, 2024
1 parent 7a937ac commit d64c2c2
Show file tree
Hide file tree
Showing 8 changed files with 264 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/converter/plugins/NullableUnionTypePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {Render, renderResolvedNullable} from "../render.js";
import {TypeScriptService, typeScriptServiceKey} from "./TypeScriptPlugin.js";
import {resolveParenthesizedType} from "../../utils/resolveParenthesizedType.js";
import {isNullableStringUnionType} from "./StringUnionTypePlugin.js";
import {isNullableNumericUnionType} from "./NumericUnionTypePlugin.js";
import {GeneratedFile} from "../generated.js";

const isNull = (type: Node) => ts.isLiteralTypeNode(type) && type.literal.kind === ts.SyntaxKind.NullKeyword
Expand Down Expand Up @@ -134,6 +135,7 @@ export class NullableUnionTypePlugin implements ConverterPlugin {
}

if (isNullableStringUnionType(node, context)) return null
if (isNullableNumericUnionType(node, context)) return null

if (isNullableUnionType(node, context)) {
const types = flatUnionTypes(node, context)
Expand Down
175 changes: 175 additions & 0 deletions src/converter/plugins/NumericUnionTypePlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import ts, {LiteralTypeNode, Node, NumericLiteral, PrefixUnaryExpression, SyntaxKind, UnionTypeNode} from "typescript";
import {escapeIdentifier} from "../../utils/strings.js";
import {CheckCoverageService, checkCoverageServiceKey} from "./CheckCoveragePlugin.js";
import {ConfigurationService, configurationServiceKey} from "./ConfigurationPlugin.js";
import {ConverterContext} from "../context.js";
import {createAnonymousDeclarationPlugin} from "./AnonymousDeclarationPlugin.js";
import {flatUnionTypes, isNullableType} from "./NullableUnionTypePlugin.js";
import {ifPresent, Render} from "../render.js";
import {InjectionService, injectionServiceKey} from "./InjectionPlugin.js";
import {InjectionType} from "../injection.js";
import {TypeScriptService, typeScriptServiceKey} from "./TypeScriptPlugin.js";
import {NamespaceInfoService, namespaceInfoServiceKey} from "./NamespaceInfoPlugin.js";

export function isNumericUnionType(node: ts.Node, context: ConverterContext): node is UnionTypeNode {
return (
ts.isUnionTypeNode(node)
&& flatUnionTypes(node, context).every(type => (
ts.isLiteralTypeNode(type)
&& (ts.isNumericLiteral(type.literal)
|| (ts.isPrefixUnaryExpression(type.literal)
&& ts.isNumericLiteral(type.literal.operand)))
))
)
}

export function isNullableNumericUnionType(node: ts.Node, context: ConverterContext): node is UnionTypeNode {
if (!ts.isUnionTypeNode(node)) return false

const types = flatUnionTypes(node, context)
const nonNullableTypes = types.filter(type => !isNullableType(type))

return (
types.every(type => (
isNullableType(type)
|| (
ts.isLiteralTypeNode(type)
&& (ts.isNumericLiteral(type.literal)
|| (ts.isPrefixUnaryExpression(type.literal)
&& ts.isNumericLiteral(type.literal.operand)))
)
))
&& nonNullableTypes.length > 1
)
}

export function convertNumericUnionType(
node: UnionTypeNode,
name: string,
isInlined: boolean,
context: ConverterContext,
render: Render,
) {
const checkCoverageService = context.lookupService<CheckCoverageService>(checkCoverageServiceKey)
checkCoverageService?.cover(node)

const configurationService = context.lookupService<ConfigurationService>(configurationServiceKey)
if (configurationService === undefined) throw new Error("ConfigurationService required")
const typeScriptService = context.lookupService<TypeScriptService>(typeScriptServiceKey)
if (typeScriptService === undefined) throw new Error("TypeScriptService required")
const namespaceInfoService = context.lookupService<NamespaceInfoService>(namespaceInfoServiceKey)
const injectionService = context.lookupService<InjectionService>(injectionServiceKey)

const types = flatUnionTypes(node, context)

const nonNullableTypes = types.filter(type => !isNullableType(type))
const nullableTypes = types.filter(type => isNullableType(type))

const {unionNameMapper} = configurationService.configuration

const entries = nonNullableTypes
.filter((type): type is LiteralTypeNode => ts.isLiteralTypeNode(type))
.map(type => {
checkCoverageService?.cover(type)

return type.literal
})
.filter((literal): literal is NumericLiteral | PrefixUnaryExpression =>
ts.isNumericLiteral(literal)
|| (ts.isPrefixUnaryExpression(literal)
&& ts.isNumericLiteral(literal.operand))
)
.map(literal => {
checkCoverageService?.cover(literal)

const value = typeScriptService.printNode(literal)

for (const [namePattern, valueMapping] of Object.entries(unionNameMapper)) {
const nameRegexp = new RegExp(namePattern)
if (nameRegexp.test(name)) {
for (const [valuePattern, key] of Object.entries(valueMapping)) {
const valueRegexp = new RegExp(valuePattern)
if (valueRegexp.test(value)) {
if (!key) {
throw new Error("Configured key in unionNameMapper must not be empty")
}
return [key, value] as const
}
}
}
}

if (ts.isNumericLiteral(literal)) {
return [`VALUE_${toIdentifierPart(typeScriptService, literal)}`, value] as const
} else {
return [`VALUE_MINUS_${toIdentifierPart(typeScriptService, literal.operand)}`, value] as const
}
})

const keyDisambiguators: Map<string, number> = new Map()
const disambiguatedEntries = entries.map(([key, value]) => {
const keyDisambiguator = (keyDisambiguators.get(key) ?? 0) + 1
keyDisambiguators.set(key, keyDisambiguator)
if (keyDisambiguator > 1) {
return [escapeIdentifier(`${key}_${keyDisambiguator}`), value] as const;
} else {
return [escapeIdentifier(key), value] as const;
}
});

const body = disambiguatedEntries
.map(([key, value]) => (
`
@seskar.js.JsValue("${value}")
val ${key}: ${name}
`.trim()
))
.join("\n")

const heritageInjections = injectionService?.resolveInjections(node, InjectionType.HERITAGE_CLAUSE, context, render)

const namespace = typeScriptService.findClosest(node, ts.isModuleDeclaration)

let externalModifier = "external "

if (isInlined && namespace !== undefined && namespaceInfoService?.resolveNamespaceStrategy(namespace) === "object") {
externalModifier = ""
}

const injectedHeritageClauses = heritageInjections
?.filter(Boolean)
?.join(", ")

const declaration = `
sealed ${externalModifier}interface ${name}${ifPresent(injectedHeritageClauses, it => ` : ${it}`)} {
companion object {
${body}
}
}
`.trim()

const nullable = nullableTypes.length > 0

return {
declaration,
nullable,
}
}

function toIdentifierPart(typeScriptService: TypeScriptService, node: Node): string {
return typeScriptService.printNode(node).replaceAll(".", "_")
}

export const numericUnionTypePlugin = createAnonymousDeclarationPlugin(
(node, context, render) => {
if (!isNullableNumericUnionType(node, context)) return null

const name = context.resolveName(node)

const {declaration, nullable} = convertNumericUnionType(node, name, false, context, render)

const reference = nullable ? `${name}?` : name

return {name, declaration, reference};
}
)
2 changes: 2 additions & 0 deletions src/converter/plugins/convertParameterDeclaration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {createSimplePlugin} from "../plugin.js";
import {CheckCoverageService, checkCoverageServiceKey} from "./CheckCoveragePlugin.js";
import {flatUnionTypes, isNullableType, isNullableUnionType} from "./NullableUnionTypePlugin.js";
import {isNullableStringUnionType} from "./StringUnionTypePlugin.js";
import {isNullableNumericUnionType} from "./NumericUnionTypePlugin.js";
import {ConverterContext} from "../context.js";
import {Render, renderNullable} from "../render.js";
import {escapeIdentifier} from "../../utils/strings.js";
Expand Down Expand Up @@ -209,6 +210,7 @@ const expandUnions = (
if (isThisParameter(parameter)) continue

if (type && isNullableStringUnionType(type, context)) continue
if (type && isNullableNumericUnionType(type, context)) continue

if (type && ts.isUnionTypeNode(type)) {
checkCoverageService?.cover(type)
Expand Down
5 changes: 5 additions & 0 deletions src/converter/plugins/convertTypeAliasDeclaration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {createSimplePlugin} from "../plugin.js";
import {ifPresent} from "../render.js";
import {CheckCoverageService, checkCoverageServiceKey} from "./CheckCoveragePlugin.js";
import {convertStringUnionType, isStringUnionType} from "./StringUnionTypePlugin.js";
import {convertNumericUnionType, isNumericUnionType} from "./NumericUnionTypePlugin.js";
import {convertTypeLiteral} from "./TypeLiteralPlugin.js";
import {convertInheritedTypeLiteral, isInheritedTypeLiteral} from "./InheritedTypeLiteralPlugin.js";
import {convertMappedType} from "./MappedTypePlugin.js";
Expand Down Expand Up @@ -39,6 +40,10 @@ export const convertTypeAliasDeclaration = createSimplePlugin((node, context, re
return convertStringUnionType(node.type, name, true, context, render).declaration
}

if (isNumericUnionType(node.type, context)) {
return convertNumericUnionType(node.type, name, true, context, render).declaration
}

if (isInheritedTypeLiteral(node.type)) {
return convertInheritedTypeLiteral(node.type, name, typeParameters, true, context, render)
}
Expand Down
2 changes: 2 additions & 0 deletions src/defaultPlugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import {convertIndexedSignatureDeclaration} from "./converter/plugins/convertInd
import {DeclarationMergingPlugin} from "./converter/plugins/DeclarationMergingPlugin.js";
import {convertTypeQuery} from "./converter/plugins/convertTypeQuery.js";
import {stringUnionTypePlugin} from "./converter/plugins/StringUnionTypePlugin.js";
import {numericUnionTypePlugin} from "./converter/plugins/NumericUnionTypePlugin.js";
import {convertLiteral} from "./converter/plugins/convertLiteral.js";
import {inheritedTypeLiteralPlugin} from "./converter/plugins/InheritedTypeLiteralPlugin.js";
import {Injection} from "./converter/injection.js";
Expand Down Expand Up @@ -96,6 +97,7 @@ export const createPlugins = (
typeLiteralPlugin,
mappedTypePlugin,
stringUnionTypePlugin,
numericUnionTypePlugin,
inheritedTypeLiteralPlugin,

convertPrimitive(hasKind(ts.SyntaxKind.AnyKeyword), () => "Any?"),
Expand Down
3 changes: 3 additions & 0 deletions test/functional/base/base.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ testGeneration("base", import.meta.url, output => ({
},
"^Operator": {
"^>=$": "GTE"
},
"^AnotherCompareResult2$": {
"": "RESULT"
}
},
verbose: true,
Expand Down
66 changes: 66 additions & 0 deletions test/functional/base/generated/union/numericEnum.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Generated by Karakum - do not modify it manually!

@file:JsModule("sandbox-base/union/numericEnum")
@file:Suppress(
"NON_EXTERNAL_DECLARATION_IN_INAPPROPRIATE_FILE",
)

package sandbox.base.union

external fun compare(): CompareResult

sealed external interface AnotherCompareResult {
companion object {
@seskar.js.JsValue("-1")
val VALUE_MINUS_1: AnotherCompareResult
@seskar.js.JsValue("0")
val VALUE_0: AnotherCompareResult
@seskar.js.JsValue("1")
val VALUE_1: AnotherCompareResult
}
}

external fun foo(param: FooParam)

sealed external interface AnotherCompareResult2 {
companion object {
@seskar.js.JsValue("-1")
val RESULT: AnotherCompareResult2
@seskar.js.JsValue("0")
val RESULT_2: AnotherCompareResult2
@seskar.js.JsValue("1")
val RESULT_3: AnotherCompareResult2
}
}

sealed external interface AnotherCompareResult3 {
companion object {
@seskar.js.JsValue("-1.0")
val VALUE_MINUS_1_0: AnotherCompareResult3
@seskar.js.JsValue("0")
val VALUE_0: AnotherCompareResult3
@seskar.js.JsValue("1.")
val VALUE_1_: AnotherCompareResult3
}
}
sealed external interface CompareResult {
companion object {
@seskar.js.JsValue("-1")
val VALUE_MINUS_1: CompareResult
@seskar.js.JsValue("0")
val VALUE_0: CompareResult
@seskar.js.JsValue("1")
val VALUE_1: CompareResult
}
}

sealed external interface FooParam {
companion object {
@seskar.js.JsValue("-1")
val VALUE_MINUS_1: FooParam
@seskar.js.JsValue("0")
val VALUE_0: FooParam
@seskar.js.JsValue("1")
val VALUE_1: FooParam
}
}
9 changes: 9 additions & 0 deletions test/functional/base/lib/union/numericEnum.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export declare function compare(): -1 | 0 | 1

export declare type AnotherCompareResult = -1 | 0 | 1

export function foo(param: -1 | 0 | 1);

export declare type AnotherCompareResult2 = -1 | 0 | 1

export declare type AnotherCompareResult3 = -1.0 | 0 | 1.

0 comments on commit d64c2c2

Please sign in to comment.