Skip to content

Commit bf24e51

Browse files
authored
fix(ts-interface-generator): no error when parent class has no c'tor (#474)
This in particular addresses the common case when Component.js inherits from the sap/fe/core/AppComponent, which has no constructor. This used to cause the error 'Component inherits from ManagedObject and has metadata but the parent class "sap/fe/core/AppComponent".AppComponent seems to have no settings type. It might have no constructors, this is where the settings type is used. Or the settings type used there and its inheritance chain could not be resolved.' Actually, AppComponent does have a settings type, but it is not found without constructor and it is never used and unneeded, because it inherits from the UIComponent settings type without adding anything, like this: export interface $AppComponentSettings extends $UIComponentSettings {}
1 parent 096aee3 commit bf24e51

File tree

1 file changed

+56
-21
lines changed

1 file changed

+56
-21
lines changed

packages/ts-interface-generator/src/interfaceGenerationHelper.ts

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,8 @@ Or is there a different reason why this type would not be known?`,
269269
return;
270270
}
271271

272+
// invariant: there is exactly one metadata block with an initializer
273+
272274
// now check whether there is a settings type in the superclass
273275
// (which the generated settings type needs to inherit from)
274276
// There really should be, because all descendants of ManagedObject should have one!
@@ -308,11 +310,11 @@ Or is there a different reason why this type would not be known?`,
308310
}
309311
} else if (metadata) {
310312
throw new Error(
311-
`${
313+
`'${
312314
statement.name ? statement.name.text : ""
313-
} inherits from ${interestingBaseClass} and has metadata but the parent class ${typeChecker.getFullyQualifiedName(
315+
}' inherits from '${interestingBaseClass}' and has metadata, but the parent class '${typeChecker.getFullyQualifiedName(
314316
type.getSymbol(),
315-
)} seems to have no settings type. It might have no constructors, this is where the settings type is used. Or the settings type used there and its inheritance chain could not be resolved.
317+
)}' seems to have no settings type. It might have no constructors - this is where the settings type is used. Or the settings type used there and its inheritance chain could not be resolved.
316318
317319
In case this parent class is also in your project, make sure to add its constructors, then try again. A comment with instructions might be in the console output above.
318320
Otherwise, you can temporarily remove this file (${
@@ -432,7 +434,10 @@ function isOneAStringAndTheOtherASettingsObject(
432434
* Returns the type of the settings object used in the constructor of the given type
433435
* Needed to derive the new settings object type for the subclass from it.
434436
*/
435-
function getSettingsType(type: ts.Type, typeChecker: ts.TypeChecker) {
437+
function getSettingsType(
438+
type: ts.Type,
439+
typeChecker: ts.TypeChecker,
440+
): ts.TypeNode | undefined {
436441
const declarations = type.getSymbol().getDeclarations();
437442
for (let i = 0; i < declarations.length; i++) {
438443
const declaration = declarations[i] as ts.ClassDeclaration;
@@ -448,7 +453,26 @@ function getSettingsType(type: ts.Type, typeChecker: ts.TypeChecker) {
448453
}
449454
}
450455
}
456+
457+
// if no constructor is found, check the base type
458+
// TODO: it would be better to try to find the settings type directly, by guessing its name, in this case.
459+
// If present, it should be used even if it does not add anything to the parent class' settings type,
460+
// because in the future it could add something.
461+
const heritageClauses = declaration.heritageClauses;
462+
if (heritageClauses) {
463+
for (const clause of heritageClauses) {
464+
if (clause.token === ts.SyntaxKind.ExtendsKeyword) {
465+
const baseTypeNode = clause.types[0];
466+
const baseType = typeChecker.getTypeAtLocation(baseTypeNode);
467+
const baseSettingsType = getSettingsType(baseType, typeChecker);
468+
if (baseSettingsType) {
469+
return baseSettingsType;
470+
}
471+
}
472+
}
473+
}
451474
}
475+
return undefined;
452476
}
453477

454478
/**
@@ -680,19 +704,21 @@ function generateInterface(
680704
interestingBaseClass,
681705
constructorSignaturesAvailable,
682706
metadata,
683-
}: {
684-
sourceFile: ts.SourceFile;
685-
className: string;
686-
settingsTypeFullName: string;
687-
interestingBaseClass:
688-
| "ManagedObject"
689-
| "Element"
690-
| "Control"
691-
| "WebComponent"
692-
| undefined;
693-
constructorSignaturesAvailable: boolean;
694-
metadata: ts.PropertyDeclaration[];
695-
},
707+
}:
708+
| {
709+
sourceFile: ts.SourceFile;
710+
className: string;
711+
settingsTypeFullName: string;
712+
interestingBaseClass:
713+
| "ManagedObject"
714+
| "Element"
715+
| "Control"
716+
| "WebComponent"
717+
| undefined;
718+
constructorSignaturesAvailable: boolean;
719+
metadata: ts.PropertyDeclaration[];
720+
}
721+
| undefined,
696722
allKnownGlobals: GlobalToModuleMapping,
697723
options: { generateEventWithGenerics: boolean },
698724
) {
@@ -701,10 +727,19 @@ function generateInterface(
701727
// by now we have something that looks pretty much like a ManagedObject metadata object
702728

703729
const metadataObject: ClassInfo = {};
704-
const objectLiteralExpression = metadata[0]
705-
.initializer as ts.ObjectLiteralExpression; // the entire object literal defining all the control metadata
730+
const initializer = metadata[0].initializer;
731+
if (!initializer || !ts.isObjectLiteralExpression(initializer)) {
732+
// no initializer? => no interface needed
733+
// TODO: but it could be that the metadata is not an object literal, but a variable or a function call, which returns the metadata object.
734+
// This is not supported yet, but could be in the future. Warn the user about this.
735+
log.warn(
736+
`Class '${className}' inside '${fileName}' inherits from ${interestingBaseClass} and has a 'metadata' property, but no object literal is assigned to this property. This is not supported (yet?), hence no TypeScript interface is generated for this class by @ui5/ts-interface-generator.`,
737+
);
738+
return;
739+
}
706740

707-
objectLiteralExpression.properties.forEach((propertyAssignment) => {
741+
// initializer is now known to be a ts.ObjectLiteralExpression - the entire object literal defining all the control metadata... loop it
742+
initializer.properties.forEach((propertyAssignment) => {
708743
// each propertyAssignment is something like properties: {...} and aggregations: {...}
709744
if (!ts.isPropertyAssignment(propertyAssignment)) {
710745
return; // huh, not a property assignment? => does not look like something we are interested in
@@ -753,7 +788,7 @@ function generateInterface(
753788
!metadataObject.associations &&
754789
!metadataObject.events
755790
) {
756-
// No API for which accessors are generated? => no interface needed
791+
// No API for which accessors need to be generated? => no interface needed
757792
return;
758793
}
759794

0 commit comments

Comments
 (0)