Skip to content

Commit

Permalink
feat(dts-generator): change how enums are referenced in the generated… (
Browse files Browse the repository at this point in the history
#478)

feat(dts-generator): add special generation for deprecated enums

To improve compatibility with existing code, deprecated enums that are
aliases for new, non-deprecated enums are now generated as a re-export.

 - a directives file (.dtsgenrc) may now contain a map
   `deprecatedEnumAliases` which maps from deprecated enum name to
   the new name
 - when ESM types are generated, such deprecated enums are generated as
   a re-export of the new enum. A re-export preserves both, the type and
   the object nature of the enum.
 - when global types are generated, nothing changed as there's no
   equivalent to the re-export when using namespaces
 - additionally, the type alternative of using literals instead of enum
   values is no longer generated for the return type of a function,
   but it's now also generated for the type aliases, as long as they're
   not used as return type
---------

Co-authored-by: akudev <andreas.kunz@sap.com>
  • Loading branch information
codeworrior and akudev authored Jan 8, 2025
1 parent b05fbcf commit 42fc290
Show file tree
Hide file tree
Showing 15 changed files with 498 additions and 554 deletions.
3 changes: 3 additions & 0 deletions packages/dts-generator/api-report/dts-generator.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ export interface Directives {
badInterfaces: string[];
badMethods: string[];
badSymbols: string[];
deprecatedEnumAliases: {
[fqn: string]: string;
};
forwardDeclarations: {
[libraryName: string]: ConcreteSymbol[];
};
Expand Down
1 change: 1 addition & 0 deletions packages/dts-generator/docs/TECHNICAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ Using `directives` as input and the information whether modules or globals shoul
- `convertNamespacesIntoTypedefsOrInterfaces` converts namespaces which are used as substitute for static objects into something else (static object or typedef); contains hardcoded knowledge about sap.ui.Device and uses "namespacesToInterfaces" directive; examples: 'jQuery.sap.PseudoEvents', 'jQuery.sap.storage', 'module:sap/base/Log', 'sap.ui.base.BindingInfo', 'sap.ui.core.AppCacheBuster', 'sap.ui.core.BusyIndicator' to object and 'sap.ui.core.AbsoluteCSSSize', 'sap.ui.core.Collision', 'sap.ui.core.CSSColor', 'sap.ui.core.Dock', 'sap.ui.core.URI' to typedef.
- `determineMissingExportsForTypes` adds exports for typedefs and interfaces where they are needed for the designtime - they are so far omitted because not needed at runtime
- `parseTypeExpressions` converts all type expressions into a TypeScript AST
- `markDeprecatedAliasesForEnums` marks enums (based on the directives) which are deprecated and should only be generated as alias for the new replacement enum
- `addForwardDeclarations` (from directives) - this relates to inverse dependencies
- `addInterfaceWithModuleNames` adds all visible modules to `sap.IUI5DefineDependencyNames`, which is merged across libraries from TypeScript perspective and can be used to type module imports
- `addConstructorSettingsInterfaces` and `addEventParameterInterfaces` create two additional interfaces defining important structures:
Expand Down
11 changes: 11 additions & 0 deletions packages/dts-generator/src/generate-from-objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,16 @@ export interface Directives {
overlays: {
[libraryName: string]: ConcreteSymbol[];
};

/**
* If a symbol of kind "enum" is used as key in this map, this enum is a deprecated
* alias for another enum, whose name is given as value in the map.
* For such deprecated aliases for enums, a different type signature is generated,
* see method `genDeprecatedAliasForEnum` in dts-code-gen.ts.
*/
deprecatedEnumAliases: {
[fqn: string]: string;
};
}

/**
Expand Down Expand Up @@ -124,6 +134,7 @@ const defaultOptions: GenerateFromObjectsConfig = {
forwardDeclarations: {},
fqnToIgnore: {},
overlays: {},
deprecatedEnumAliases: {},
},
generateGlobals: false,
};
Expand Down
1 change: 1 addition & 0 deletions packages/dts-generator/src/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ async function loadDirectives(directivesPaths: string[]) {
forwardDeclarations: {},
fqnToIgnore: {},
overlays: {},
deprecatedEnumAliases: {},
};

function mergeDirectives(loadedDirectives: Directives) {
Expand Down
122 changes: 73 additions & 49 deletions packages/dts-generator/src/phases/dts-code-gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,43 +286,23 @@ function genImport(entity: Import) {

function genExport(_export: Export) {
if (_export.expression) {
const options = {
export: _export.export,
exportAsDefault: _export.asDefault,
};
switch (_export.expression.kind) {
case "Class":
return genClass(_export.expression, {
export: _export.export,
exportAsDefault: _export.asDefault,
});
return genClass(_export.expression, options);
case "Enum":
if (_export.asDefault) {
// TS does not allow export of enums as default
// see https://github.com/microsoft/TypeScript/issues/3320
return (
genEnum(_export.expression) +
NL +
`export default ${_export.expression.name}`
);
}
return genEnum(_export.expression, { export: _export.export });
return genEnum(_export.expression, options);
case "TypeAliasDeclaration":
return genTypeDefinition(_export.expression, {
export: _export.export,
exportAsDefault: _export.asDefault,
});
return genTypeDefinition(_export.expression, options);
case "Interface":
return genInterface(_export.expression, {
export: _export.export,
exportAsDefault: _export.asDefault,
});
return genInterface(_export.expression, options);
case "FunctionDesc":
return genFunction(_export.expression, {
export: _export.export,
exportAsDefault: _export.asDefault,
});
return genFunction(_export.expression, options);
case "Variable":
return genConstExport(_export.expression, {
export: _export.export,
exportAsDefault: _export.asDefault,
});
return genConstExport(_export.expression, options);
case "Namespace":
return genNamespace(_export.expression, {
export:
Expand Down Expand Up @@ -559,7 +539,7 @@ function genMethodOrFunction(
text += ")";

let hasReturnType = ast.returns !== undefined && ast.returns.type;
text += `: ${hasReturnType ? genType(ast.returns.type) : "void"}`;
text += `: ${hasReturnType ? genType(ast.returns.type, "returnValue") : "void"}`;

return text;
}
Expand Down Expand Up @@ -609,7 +589,7 @@ function genInterfaceProperty(ast: Variable) {
text += applyTsIgnore(ast);
text +=
`${ast.name}${ast.optional ? "?" : ""} : ${
ast.type ? genType(ast.type) : "any"
ast.type ? genType(ast.type, "property") : "any"
}` + NL;
return text;
}
Expand All @@ -626,14 +606,16 @@ function genConstExport(
if (options.export && options.exportAsDefault) {
text += JSDOC(ast) + NL;
text += applyTsIgnore(ast);
text += `const ${ast.name} : ${ast.type ? genType(ast.type) : "any"};` + NL;
text +=
`const ${ast.name} : ${ast.type ? genType(ast.type, "const") : "any"};` +
NL;
text += NL;
text += `export default ${ast.name};` + NL;
} else if (options.export) {
text += JSDOC(ast) + NL;
text += applyTsIgnore(ast);
text +=
`export const ${ast.name} : ${ast.type ? genType(ast.type) : "any"};` +
`export const ${ast.name} : ${ast.type ? genType(ast.type, "const") : "any"};` +
NL;
}
return text;
Expand All @@ -648,7 +630,8 @@ function genField(ast: Variable) {
text += JSDOC(ast) + NL;
text += applyTsIgnore(ast);
text += ast.static ? "static " : "";
text += `${ast.name} : ${ast.type ? genType(ast.type) : "any"}` + NL;
text +=
`${ast.name} : ${ast.type ? genType(ast.type, "property") : "any"}` + NL;
return text;
}

Expand Down Expand Up @@ -677,7 +660,7 @@ function genParameter(ast: Parameter) {
});
text += "}" + NL;
} else {
text += `: ${ast.type ? genType(ast.type) : "any"}`;
text += `: ${ast.type ? genType(ast.type, "parameter") : "any"}`;
}

return text;
Expand All @@ -694,15 +677,55 @@ function genEnum(
exportAsDefault: false,
},
) {
if (options.export && ast.deprecatedAliasFor) {
return genDeprecatedAliasForEnum(ast, options);
}

let text = "";
text += JSDOC(ast) + NL;
text +=
`${options.export ? "export " + (options.exportAsDefault ? "default " : "") : ""}enum ${ast.name} {` +
`${options.export && !options.exportAsDefault ? "export " : ""}enum ${ast.name} {` +
NL;
text += APPEND_ITEMS(ast.values, (prop: Variable) =>
genEnumValue(prop, ast.withValues),
);
text += "}";
if (options.export && options.exportAsDefault) {
// TS does not allow export of enums as default
// see https://github.com/microsoft/TypeScript/issues/3320
text += NL + `export default ${ast.name}`;
}
return text;
}

/**
* @param ast
* @return
*/
function genDeprecatedAliasForEnum(
ast: Enum,
options: { export: boolean; exportAsDefault?: boolean } = {
export: undefined,
exportAsDefault: false,
},
) {
if (!options.export) {
console.error(
"deprecated alias is only supported for exported enums",
ast,
options,
);
throw new TypeError(
`deprecated alias is only supported for exported enums (${ast.name})`,
);
}
let text = "";
text += "export {";
text += JSDOC(ast) + NL;
text +=
`${ast.deprecatedAliasFor} as ${options.exportAsDefault ? "default " : ast.name}` +
NL;
text += "}" + NL;

return text;
}
Expand Down Expand Up @@ -731,7 +754,7 @@ function genEnumValue(ast: Variable, withValue = false) {
function genVariable(ast: Variable) {
let text = "";
text += JSDOC(ast) + NL;
text += `export const ${ast.name} : ${genType(ast.type)};` + NL;
text += `export const ${ast.name} : ${genType(ast.type, "const")};` + NL;

return text;
}
Expand Down Expand Up @@ -781,9 +804,10 @@ function hasSimpleElementType(ast: ArrayType): boolean {

/**
* @param ast
* @param usage Context in which the type is used
* @returns
*/
function genType(ast: Type): string {
function genType(ast: Type, usage: string = "unknown"): string {
let text;
switch (ast.kind) {
case "TypeReference":
Expand All @@ -799,49 +823,49 @@ function genType(ast: Type): string {
if (ast.nullable) {
text += `|null`;
}
if (ast.isStandardEnum) {
if (ast.isStandardEnum && usage !== "returnValue") {
text = `(${text} | keyof typeof ${ast.typeName})`; // TODO parentheses not always required
}
return text;
case "ArrayType":
if (hasSimpleElementType(ast)) {
return `${genType(ast.elementType)}[]`;
return `${genType(ast.elementType, usage)}[]`;
}
return `Array<${genType(ast.elementType)}>`;
return `Array<${genType(ast.elementType, usage)}>`;
case "LiteralType":
return String(ast.literal);
case "TypeLiteral":
return `{${NL}${_.map(ast.members, (prop) => {
let ptext = "";
ptext += JSDOC(prop) + NL;
ptext +=
`${prop.name}${prop.optional ? "?" : ""}: ${genType(prop.type)},` +
`${prop.name}${prop.optional ? "?" : ""}: ${genType(prop.type, usage)},` +
NL;
return ptext;
}).join("")}}`;
case "UnionType":
const unionTypes: string[] = _.map(ast.types, (variantType) => {
if (variantType.kind === "FunctionType") {
return `(${genType(variantType)})`;
return `(${genType(variantType, usage)})`;
}
return genType(variantType);
return genType(variantType, usage);
});
return unionTypes.join(" | ");
case "IntersectionType":
const intersectionTypes: string[] = _.map(ast.types, (variantType) => {
if (variantType.kind === "FunctionType") {
return `(${genType(variantType)})`;
return `(${genType(variantType, usage)})`;
}
return genType(variantType);
return genType(variantType, usage);
});
return intersectionTypes.join(" & ");
case "FunctionType":
text = "";
if (!_.isEmpty(ast.typeParameters)) {
text += `<${_.map(ast.typeParameters, (param) => param.name).join(", ")}>`; // TODO defaults, constraints, expressions
}
text += `(${_.map(ast.parameters, (param) => `${param.name}: ${genType(param.type)}`).join(", ")})`;
text += ` => ${ast.type ? genType(ast.type) : "void"}`;
text += `(${_.map(ast.parameters, (param) => `${param.name}: ${genType(param.type, "parameter")}`).join(", ")})`;
text += ` => ${ast.type ? genType(ast.type, "returnValue") : "void"}`;
return text;
case "NativeTSTypeExpression":
// native TS type expression, emit the 'type' string "as is"
Expand Down
23 changes: 23 additions & 0 deletions packages/dts-generator/src/phases/json-fixer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -839,6 +839,28 @@ function removeRestrictedMembers(json: ApiJSON) {
});
}

/**
* The map `deprecatedEnumAliases`, which is part of the directives, can list deprecated enums
* for which a special type alias should be generated.
*
* In this method, the aliases are added to those enums, both as a marker and as input for
* later generation of the alias.
*
* @param symbols Array of symbols for a library
* @param directives Directives for all libraries
*/
function markDeprecatedAliasesForEnums(
symbols: ConcreteSymbol[],
directives: Directives,
) {
const deprecatedEnumAliases = directives.deprecatedEnumAliases;
symbols.forEach((symbol) => {
if (symbol.kind === "enum" && symbol.name in deprecatedEnumAliases) {
symbol.deprecatedAliasFor = deprecatedEnumAliases[symbol.name];
}
});
}

function _prepareApiJson(
json: ApiJSON,
directives: Directives,
Expand All @@ -853,6 +875,7 @@ function _prepareApiJson(
convertNamespacesIntoTypedefsOrInterfaces(json.symbols, directives);
determineMissingExportsForTypes(json.symbols);
parseTypeExpressions(json.symbols);
markDeprecatedAliasesForEnums(json.symbols, directives);
if (options.mainLibrary) {
addForwardDeclarations(json, directives);
addInterfaceWithModuleNames(json.symbols);
Expand Down
26 changes: 21 additions & 5 deletions packages/dts-generator/src/phases/json-to-ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,17 +391,29 @@ class ConvertGlobalsToImports extends ASTVisitor {
this.#scopeBuilder.topLevelScope as ModuleBuilder
).typeUniverse.get(type);
return (
symbolForType != null &&
symbolForType["ui5-metadata"] != null &&
symbolForType["ui5-metadata"].stereotype === "enum"
(symbolForType != null &&
symbolForType["ui5-metadata"] != null &&
symbolForType["ui5-metadata"].stereotype === "enum") ||
(generateGlobals === false &&
symbolForType != null &&
symbolForType.deprecatedAliasFor &&
this._isStandardEnum(symbolForType.deprecatedAliasFor))
);
}
_visitTypeName(typeName: string, usage: "extends" | "implements") {
_visitTypeName(typeName: string, usage: string) {
return this._import(
typeName,
usage !== "extends" && usage !== "implements",
);
}
_visitEnum(_enum: Enum) {
if (_enum.deprecatedAliasFor) {
_enum.deprecatedAliasFor = this._visitTypeName(
_enum.deprecatedAliasFor,
"alias",
);
}
}
_visitTypeReference(type) {
if (this.mode !== "type-alias") {
type.isStandardEnum = this._isStandardEnum(type.typeName);
Expand Down Expand Up @@ -1639,7 +1651,10 @@ function buildInterfaceFromObject(ui5Object): Interface {
* @returns
*/
function buildEnum(ui5Enum: EnumSymbol) {
assertKnownProps(["name", "basename", "properties"], ui5Enum);
assertKnownProps(
["name", "basename", "properties", "deprecatedAliasFor"],
ui5Enum,
);

const isStandardEnum =
ui5Enum["ui5-metadata"] != null &&
Expand All @@ -1650,6 +1665,7 @@ function buildEnum(ui5Enum: EnumSymbol) {
name: ui5Enum.basename,
withValues: true,
isLibraryEnum: ui5Enum.module.endsWith("/library"),
deprecatedAliasFor: ui5Enum.deprecatedAliasFor,
values: _.map(ui5Enum.properties, (prop) =>
buildVariableWithValue(prop, isStandardEnum),
),
Expand Down
1 change: 1 addition & 0 deletions packages/dts-generator/src/types/api-json.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ export type EnumSymbol = SymbolBase & {
"ui5-metadata"?: {
stereotype?: "enum";
};
deprecatedAliasFor?: string;
[k: string]: any;
};
/**
Expand Down
Loading

0 comments on commit 42fc290

Please sign in to comment.