Skip to content

Commit aba0aa1

Browse files
committed
Generate interface-based components
Interfaces used to be ignored, which could be worked around by using abstract classes. This is now not required anymore, as components will be generated for interfaces as well. Complex interfaces that are type-based (such as unions) are still ignored.
1 parent dceb57c commit aba0aa1

19 files changed

+1231
-314
lines changed

lib/generate/Generator.ts

+9-8
Original file line numberDiff line numberDiff line change
@@ -36,31 +36,32 @@ export class Generator {
3636
}
3737

3838
public async generateComponents(): Promise<void> {
39+
const logger = ComponentsManagerBuilder.createLogger(this.logLevel);
40+
3941
// Load package metadata
4042
const packageMetadata = await new PackageMetadataLoader({ resolutionContext: this.resolutionContext })
4143
.load(this.pathDestination.packageRootDirectory);
4244

43-
const classLoader = new ClassLoader({ resolutionContext: this.resolutionContext });
45+
const classLoader = new ClassLoader({ resolutionContext: this.resolutionContext, logger });
4446
const classFinder = new ClassFinder({ classLoader });
45-
const classIndexer = new ClassIndexer({ classLoader, classFinder, ignoreClasses: this.ignoreClasses });
47+
const classIndexer = new ClassIndexer({ classLoader, classFinder, ignoreClasses: this.ignoreClasses, logger });
4648

4749
// Find all relevant classes
4850
const packageExports = await classFinder.getPackageExports(packageMetadata.name, packageMetadata.typesPath);
49-
const classIndex = await classIndexer.createIndex(packageExports);
51+
const classAndInterfaceIndex = await classIndexer.createIndex(packageExports);
5052

5153
// Load constructor data
52-
const constructorsUnresolved = new ConstructorLoader().getConstructors(classIndex);
54+
const constructorsUnresolved = new ConstructorLoader().getConstructors(classAndInterfaceIndex);
5355
const constructors = await new ParameterResolver({ classLoader, ignoreClasses: this.ignoreClasses })
54-
.resolveAllConstructorParameters(constructorsUnresolved, classIndex);
56+
.resolveAllConstructorParameters(constructorsUnresolved, classAndInterfaceIndex);
5557

5658
// Load external components
57-
const logger = ComponentsManagerBuilder.createLogger(this.logLevel);
5859
const externalModulesLoader = new ExternalModulesLoader({
5960
pathDestination: this.pathDestination,
6061
packageMetadata,
6162
logger,
6263
});
63-
const externalPackages = externalModulesLoader.findExternalPackages(classIndex, constructors);
64+
const externalPackages = externalModulesLoader.findExternalPackages(classAndInterfaceIndex, constructors);
6465
const externalComponents = await externalModulesLoader.loadExternalComponents(require, externalPackages);
6566

6667
// Create components
@@ -72,7 +73,7 @@ export class Generator {
7273
packageMetadata,
7374
contextConstructor,
7475
pathDestination: this.pathDestination,
75-
classReferences: classIndex,
76+
classAndInterfaceIndex,
7677
classConstructors: constructors,
7778
externalComponents,
7879
contextParser: new ContextParser({

lib/parse/ClassFinder.ts

+19
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,12 @@ export class ClassFinder {
4343
// Load the elements of the class
4444
const {
4545
exportedClasses,
46+
exportedInterfaces,
4647
exportedImportedElements,
4748
exportedImportedAll,
4849
exportedUnknowns,
4950
declaredClasses,
51+
declaredInterfaces,
5052
importedElements,
5153
} = await this.classLoader.loadClassElements(packageName, fileName);
5254
const exportDefinitions:
@@ -61,6 +63,13 @@ export class ClassFinder {
6163
fileName,
6264
};
6365
}
66+
for (const localName in exportedInterfaces) {
67+
exportDefinitions.named[localName] = {
68+
packageName,
69+
localName,
70+
fileName,
71+
};
72+
}
6473

6574
// Get all named exports from other files
6675
for (const [ exportedName, { localName, fileName: importedFileName }] of Object.entries(exportedImportedElements)) {
@@ -85,6 +94,16 @@ export class ClassFinder {
8594
break;
8695
}
8796

97+
// First check declared interfaces
98+
if (localName in declaredInterfaces) {
99+
exportDefinitions.named[exportedName] = {
100+
packageName,
101+
localName,
102+
fileName,
103+
};
104+
break;
105+
}
106+
88107
// Next, check imports
89108
if (localName in importedElements) {
90109
exportDefinitions.named[exportedName] = importedElements[localName];

lib/parse/ClassIndex.ts

+2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ export interface ClassLoaded extends ClassReference {
3838
ast: AST<TSESTreeOptions>;
3939
// A super class reference if the class has one
4040
superClass?: ClassLoaded;
41+
// Interface or (abstract) class references if the class implements them
42+
implementsInterfaces?: ClassReferenceLoaded[];
4143
// If this class is an abstract class that can not be instantiated directly
4244
abstract?: boolean;
4345
// The tsdoc comment of this class

lib/parse/ClassIndexer.ts

+76-18
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,30 @@
11
/**
22
* Creates an index of classes in a certain package.
33
*/
4+
import type { Logger } from 'winston';
45
import type { ClassFinder } from './ClassFinder';
5-
import type { ClassIndex, ClassLoaded, ClassReference } from './ClassIndex';
6+
import type { ClassIndex, ClassReference, ClassReferenceLoaded, InterfaceLoaded } from './ClassIndex';
67
import type { ClassLoader } from './ClassLoader';
78

89
export class ClassIndexer {
910
private readonly classLoader: ClassLoader;
1011
private readonly classFinder: ClassFinder;
1112
private readonly ignoreClasses: Record<string, boolean>;
13+
private readonly logger: Logger;
1214

1315
public constructor(args: ClassIndexerArgs) {
1416
this.classLoader = args.classLoader;
1517
this.classFinder = args.classFinder;
1618
this.ignoreClasses = args.ignoreClasses;
19+
this.logger = args.logger;
1720
}
1821

1922
/**
2023
* Load all class references in the given class index.
2124
* @param classReferences An index of class references.
2225
*/
23-
public async createIndex(classReferences: ClassIndex<ClassReference>): Promise<ClassIndex<ClassLoaded>> {
24-
const classIndex: ClassIndex<ClassLoaded> = {};
26+
public async createIndex(classReferences: ClassIndex<ClassReference>): Promise<ClassIndex<ClassReferenceLoaded>> {
27+
const classIndex: ClassIndex<ClassReferenceLoaded> = {};
2528

2629
for (const [ className, classReference ] of Object.entries(classReferences)) {
2730
if (!(className in this.ignoreClasses)) {
@@ -35,25 +38,79 @@ export class ClassIndexer {
3538
/**
3639
* Load the referenced class, and obtain all required information,
3740
* such as its declaration and loaded super class referenced.
38-
* @param classReference The reference to a class.
41+
* @param classReference The reference to a class or interface.
3942
*/
40-
public async loadClassChain(classReference: ClassReference): Promise<ClassLoaded> {
43+
public async loadClassChain(classReference: ClassReference): Promise<ClassReferenceLoaded> {
4144
// Load the class declaration
42-
const classReferenceLoaded: ClassLoaded = await this.classLoader.loadClassDeclaration(classReference, false);
45+
const classReferenceLoaded: ClassReferenceLoaded = await this.classLoader
46+
.loadClassDeclaration(classReference, true);
4347

44-
// If the class has a super class, load it recursively
45-
const superClassName = this.classLoader.getSuperClassName(classReferenceLoaded.declaration,
46-
classReferenceLoaded.fileName);
47-
if (superClassName && !(superClassName in this.ignoreClasses)) {
48-
try {
49-
classReferenceLoaded.superClass = await this.loadClassChain({
50-
packageName: classReferenceLoaded.packageName,
51-
localName: superClassName,
52-
fileName: classReferenceLoaded.fileName,
53-
});
54-
} catch (error: unknown) {
55-
throw new Error(`Failed to load super class ${superClassName} of ${classReference.localName} in ${classReference.fileName}:\n${(<Error> error).message}`);
48+
if (classReferenceLoaded.type === 'class') {
49+
// If the class has a super class, load it recursively
50+
const superClassName = this.classLoader.getSuperClassName(classReferenceLoaded.declaration,
51+
classReferenceLoaded.fileName);
52+
if (superClassName && !(superClassName in this.ignoreClasses)) {
53+
let superClassLoaded;
54+
try {
55+
superClassLoaded = await this.loadClassChain({
56+
packageName: classReferenceLoaded.packageName,
57+
localName: superClassName,
58+
fileName: classReferenceLoaded.fileName,
59+
});
60+
} catch (error: unknown) {
61+
throw new Error(`Failed to load super class ${superClassName} of ${classReference.localName} in ${classReference.fileName}:\n${(<Error>error).message}`);
62+
}
63+
if (superClassLoaded.type !== 'class') {
64+
throw new Error(`Detected non-class ${superClassName} extending from a class ${classReference.localName} in ${classReference.fileName}`);
65+
}
66+
classReferenceLoaded.superClass = superClassLoaded;
5667
}
68+
69+
// If the class implements interfaces, load them
70+
const interfaceNames = this.classLoader.getClassInterfaceNames(classReferenceLoaded.declaration,
71+
classReferenceLoaded.fileName);
72+
classReferenceLoaded.implementsInterfaces = <ClassReferenceLoaded[]> (await Promise.all(interfaceNames
73+
.filter(interfaceName => !(interfaceName in this.ignoreClasses))
74+
.map(async interfaceName => {
75+
let interfaceOrClassLoaded;
76+
try {
77+
interfaceOrClassLoaded = await this.classLoader.loadClassDeclaration({
78+
packageName: classReferenceLoaded.packageName,
79+
localName: interfaceName,
80+
fileName: classReferenceLoaded.fileName,
81+
}, true);
82+
} catch (error: unknown) {
83+
// Ignore interfaces that we don't understand
84+
this.logger.debug(`Ignored interface ${interfaceName} implemented by ${classReference.localName} in ${classReference.fileName}:\n${(<Error> error).message}`);
85+
return;
86+
}
87+
return interfaceOrClassLoaded;
88+
})))
89+
.filter(iface => Boolean(iface));
90+
} else {
91+
const superInterfaceNames = this.classLoader
92+
.getSuperInterfaceNames(classReferenceLoaded.declaration, classReferenceLoaded.fileName);
93+
classReferenceLoaded.superInterfaces = <InterfaceLoaded[]> (await Promise.all(superInterfaceNames
94+
.filter(interfaceName => !(interfaceName in this.ignoreClasses))
95+
.map(async interfaceName => {
96+
let superInterface;
97+
try {
98+
superInterface = await this.loadClassChain({
99+
packageName: classReferenceLoaded.packageName,
100+
localName: interfaceName,
101+
fileName: classReferenceLoaded.fileName,
102+
});
103+
} catch (error: unknown) {
104+
// Ignore interfaces that we don't understand
105+
this.logger.debug(`Ignored interface ${interfaceName} extended by ${classReference.localName} in ${classReference.fileName}:\n${(<Error> error).message}`);
106+
return;
107+
}
108+
if (superInterface.type !== 'interface') {
109+
throw new Error(`Detected non-interface ${classReferenceLoaded.localName} extending from a class ${interfaceName} in ${classReference.fileName}`);
110+
}
111+
return superInterface;
112+
})))
113+
.filter(iface => Boolean(iface));
57114
}
58115

59116
return classReferenceLoaded;
@@ -64,4 +121,5 @@ export interface ClassIndexerArgs {
64121
classLoader: ClassLoader;
65122
classFinder: ClassFinder;
66123
ignoreClasses: Record<string, boolean>;
124+
logger: Logger;
67125
}

lib/parse/ClassLoader.ts

+28-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as Path from 'path';
22
import type { ClassDeclaration, TSInterfaceDeclaration } from '@typescript-eslint/types/dist/ts-estree';
33
import type { AST, TSESTreeOptions } from '@typescript-eslint/typescript-estree';
44
import { AST_NODE_TYPES } from '@typescript-eslint/typescript-estree';
5+
import type { Logger } from 'winston';
56
import type { ResolutionContext } from '../resolution/ResolutionContext';
67
import type { ClassLoaded, ClassReference, ClassReferenceLoaded, GenericTypes, InterfaceLoaded } from './ClassIndex';
78
import { CommentLoader } from './CommentLoader';
@@ -11,9 +12,11 @@ import { CommentLoader } from './CommentLoader';
1112
*/
1213
export class ClassLoader {
1314
private readonly resolutionContext: ResolutionContext;
15+
private readonly logger: Logger;
1416

1517
public constructor(args: ClassLoaderArgs) {
1618
this.resolutionContext = args.resolutionContext;
19+
this.logger = args.logger;
1720
}
1821

1922
/**
@@ -46,15 +49,36 @@ export class ClassLoader {
4649
* @param fileName The file name of the current class.
4750
*/
4851
public getSuperInterfaceNames(declaration: TSInterfaceDeclaration, fileName: string): string[] {
49-
return (declaration.extends || [])
52+
return <string[]> (declaration.extends || [])
53+
// eslint-disable-next-line array-callback-return
5054
.map(extendsExpression => {
5155
if (extendsExpression.type === AST_NODE_TYPES.TSInterfaceHeritage &&
5256
extendsExpression.expression.type === AST_NODE_TYPES.Identifier) {
5357
// Extensions in the form of `interface A extends B`
5458
return extendsExpression.expression.name;
5559
}
56-
throw new Error(`Could not interpret type of super interface in ${fileName} on line ${extendsExpression.loc.start.line} column ${extendsExpression.loc.start.column}`);
57-
});
60+
// Ignore interfaces that we don't understand
61+
this.logger.debug(`Ignored an interface expression of unknown type ${extendsExpression.expression.type} on ${declaration.id.name}`);
62+
})
63+
.filter(iface => Boolean(iface));
64+
}
65+
66+
/**
67+
* Find the interface names of the given class.
68+
* @param declaration A class declaration.
69+
* @param fileName The file name of the current class.
70+
*/
71+
public getClassInterfaceNames(declaration: ClassDeclaration, fileName: string): string[] {
72+
const interfaceNames = [];
73+
if (declaration.implements) {
74+
for (const implement of declaration.implements) {
75+
if (implement.expression.type !== AST_NODE_TYPES.Identifier) {
76+
throw new Error(`Could not interpret the implements type on a class in ${fileName} on line ${implement.expression.loc.start.line} column ${implement.expression.loc.start.column}`);
77+
}
78+
interfaceNames.push(implement.expression.name);
79+
}
80+
}
81+
return interfaceNames;
5882
}
5983

6084
/**
@@ -343,6 +367,7 @@ export class ClassLoader {
343367

344368
export interface ClassLoaderArgs {
345369
resolutionContext: ResolutionContext;
370+
logger: Logger;
346371
}
347372

348373
/**

lib/parse/ConstructorLoader.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ClassDeclaration, MethodDefinition } from '@typescript-eslint/types/dist/ts-estree';
22
import type { AST, TSESTreeOptions } from '@typescript-eslint/typescript-estree';
33
import { AST_NODE_TYPES } from '@typescript-eslint/typescript-estree';
4-
import type { ClassIndex, ClassLoaded } from './ClassIndex';
4+
import type { ClassIndex, ClassLoaded, ClassReferenceLoaded } from './ClassIndex';
55
import type { ParameterDataField, ParameterRangeUnresolved } from './ParameterLoader';
66
import { ParameterLoader } from './ParameterLoader';
77

@@ -13,10 +13,12 @@ export class ConstructorLoader {
1313
* Create a class index containing all constructor data from the classes in the given index.
1414
* @param classIndex An index of loaded classes.
1515
*/
16-
public getConstructors(classIndex: ClassIndex<ClassLoaded>): ClassIndex<ConstructorData<ParameterRangeUnresolved>> {
16+
public getConstructors(
17+
classIndex: ClassIndex<ClassReferenceLoaded>,
18+
): ClassIndex<ConstructorData<ParameterRangeUnresolved>> {
1719
const constructorDataIndex: ClassIndex<ConstructorData<ParameterRangeUnresolved>> = {};
1820
for (const [ className, classLoaded ] of Object.entries(classIndex)) {
19-
const constructor = this.getConstructor(classLoaded);
21+
const constructor = classLoaded.type === 'class' ? this.getConstructor(classLoaded) : undefined;
2022
if (constructor) {
2123
const parameterLoader = new ParameterLoader({ classLoaded });
2224
constructorDataIndex[className] = parameterLoader.loadConstructorFields(constructor);

lib/parse/ParameterResolver.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,17 @@ export class ParameterResolver {
2727
*/
2828
public async resolveAllConstructorParameters(
2929
unresolvedParametersIndex: ClassIndex<ConstructorData<ParameterRangeUnresolved>>,
30-
classIndex: ClassIndex<ClassLoaded>,
30+
classIndex: ClassIndex<ClassReferenceLoaded>,
3131
): Promise<ClassIndex<ConstructorData<ParameterRangeResolved>>> {
3232
const resolvedParametersIndex: ClassIndex<ConstructorData<ParameterRangeResolved>> = {};
3333

3434
// Resolve parameters for the different constructors in parallel
3535
await Promise.all(Object.entries(unresolvedParametersIndex)
3636
.map(async([ className, parameters ]) => {
37-
resolvedParametersIndex[className] = await this.resolveConstructorParameters(parameters, classIndex[className]);
37+
const classOrInterface = classIndex[className];
38+
if (classOrInterface.type === 'class') {
39+
resolvedParametersIndex[className] = await this.resolveConstructorParameters(parameters, classOrInterface);
40+
}
3841
}));
3942

4043
return resolvedParametersIndex;

lib/resolution/ExternalModulesLoader.ts

+11-4
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
} from 'componentsjs';
88
import type { Resource } from 'rdf-object';
99
import type { Logger } from 'winston';
10-
import type { ClassIndex, ClassLoaded, ClassReferenceLoaded } from '../parse/ClassIndex';
10+
import type { ClassIndex, ClassReferenceLoaded } from '../parse/ClassIndex';
1111
import type { ConstructorData } from '../parse/ConstructorLoader';
1212
import type { PackageMetadata } from '../parse/PackageMetadataLoader';
1313
import type { ParameterData, ParameterRangeResolved } from '../parse/ParameterLoader';
@@ -33,7 +33,7 @@ export class ExternalModulesLoader {
3333
* @param constructors The available constructors.
3434
*/
3535
public findExternalPackages(
36-
classIndex: ClassIndex<ClassLoaded>,
36+
classIndex: ClassIndex<ClassReferenceLoaded>,
3737
constructors: ClassIndex<ConstructorData<ParameterRangeResolved>>,
3838
): string[] {
3939
const externalPackages: Record<string, boolean> = {};
@@ -60,8 +60,15 @@ export class ExternalModulesLoader {
6060
if (classReference.packageName !== this.packageMetadata.name) {
6161
externalPackages[classReference.packageName] = true;
6262
}
63-
if (classReference.type === 'class' && classReference.superClass) {
64-
this.indexClassInExternalPackage(classReference.superClass, externalPackages);
63+
if (classReference.type === 'class') {
64+
if (classReference.superClass) {
65+
this.indexClassInExternalPackage(classReference.superClass, externalPackages);
66+
}
67+
if (classReference.implementsInterfaces) {
68+
for (const iface of classReference.implementsInterfaces) {
69+
this.indexClassInExternalPackage(iface, externalPackages);
70+
}
71+
}
6572
}
6673
}
6774

0 commit comments

Comments
 (0)