Skip to content

Commit 6654e77

Browse files
committed
Add lenient generation mode to ignore unsupported lang features
1 parent 203a03b commit 6654e77

15 files changed

Lines changed: 141 additions & 61 deletions

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ Usage:
105105
-e jsonld Extension for components files (without .), defaults to 'jsonld'
106106
-i ignore-classes.json Relative path to an optional file with class names to ignore
107107
-r prefix Optional custom JSON-LD module prefix
108+
--lenient If unsupported language features must produce a warning instead of an error
108109
--debugState If a 'componentsjs-generator-debug-state.json' file should be created with debug information
109110
--help Show information about this command
110111
```
@@ -127,7 +128,8 @@ The following shows an example of the possible options:
127128
"ignoreComponents": [ "Class1", "Class2" ],
128129
"logLevel": "info",
129130
"modulePrefix": "myprefix",
130-
"debugState": "true"
131+
"debugState": "true",
132+
"hardErrorUnsupported": false
131133
}
132134
```
133135

bin/componentsjs-generator.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Usage:
1717
-i ignore-classes.json Relative path to an optional file with class names to ignore
1818
-l info The logger level
1919
-r prefix Optional custom JSON-LD module prefix
20+
--lenient If unsupported language features must produce a warning instead of an error
2021
--debugState If a 'componentsjs-generator-debug-state.json' file should be created with debug information
2122
--help Show information about this command
2223
`);

lib/config/GeneratorConfig.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,9 @@ export interface GeneratorConfig {
3636
* If a 'componentsjs-generator-debug-state.json' file should be created with debug information.
3737
*/
3838
debugState: boolean;
39+
/**
40+
* If unsupported language features should cause a hard crash.
41+
* Otherwise they are emitted as warning instead of error.
42+
*/
43+
hardErrorUnsupported: boolean;
3944
}

lib/config/GeneratorFactory.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export class GeneratorFactory {
4444
acc[entry] = true;
4545
return acc;
4646
}, {}),
47+
hardErrorUnsupported: config.hardErrorUnsupported,
4748
});
4849
}
4950

@@ -69,6 +70,7 @@ export class GeneratorFactory {
6970
logLevel: 'info',
7071
modulePrefix: undefined,
7172
debugState: false,
73+
hardErrorUnsupported: true,
7274
};
7375
}
7476

@@ -95,6 +97,9 @@ export class GeneratorFactory {
9597
if (cliArgs.debugState) {
9698
config.debugState = cliArgs.debugState;
9799
}
100+
if (cliArgs.lenient) {
101+
config.hardErrorUnsupported = false;
102+
}
98103
return config;
99104
}
100105
}

lib/generate/Generator.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export class Generator {
3131
private readonly logLevel: LogLevel;
3232
private readonly debugState: boolean;
3333
private readonly prefixes?: string | Record<string, string>;
34+
private readonly hardErrorUnsupported: boolean;
3435

3536
public constructor(args: GeneratorArgs) {
3637
this.resolutionContext = args.resolutionContext;
@@ -40,6 +41,7 @@ export class Generator {
4041
this.logLevel = args.logLevel;
4142
this.debugState = args.debugState;
4243
this.prefixes = args.prefixes;
44+
this.hardErrorUnsupported = args.hardErrorUnsupported;
4345
}
4446

4547
public async generateComponents(): Promise<void> {
@@ -49,10 +51,14 @@ export class Generator {
4951
const classLoader = new ClassLoader({ resolutionContext: this.resolutionContext, logger, commentLoader });
5052
const classFinder = new ClassFinder({ classLoader });
5153
const classIndexer = new ClassIndexer({ classLoader, classFinder, ignoreClasses: this.ignoreClasses, logger });
52-
const parameterLoader = new ParameterLoader({ commentLoader });
54+
const parameterLoader = new ParameterLoader({
55+
commentLoader,
56+
hardErrorUnsupported: this.hardErrorUnsupported,
57+
logger,
58+
});
5359
const parameterResolver = new ParameterResolver({
5460
classLoader,
55-
commentLoader,
61+
parameterLoader,
5662
ignoreClasses: this.ignoreClasses,
5763
});
5864

@@ -80,7 +86,7 @@ export class Generator {
8086
const classAndInterfaceIndex = await classIndexer.createIndex(packageExports);
8187

8288
// Load constructor data
83-
const constructorsUnresolved = new ConstructorLoader({ commentLoader }).getConstructors(classAndInterfaceIndex);
89+
const constructorsUnresolved = new ConstructorLoader({ parameterLoader }).getConstructors(classAndInterfaceIndex);
8490
const constructors = await parameterResolver.resolveAllConstructorParameters(constructorsUnresolved);
8591

8692
// Load generics data
@@ -151,4 +157,5 @@ export interface GeneratorArgs {
151157
logLevel: LogLevel;
152158
debugState: boolean;
153159
prefixes?: string | Record<string, string>;
160+
hardErrorUnsupported: boolean;
154161
}

lib/parse/ConstructorLoader.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,16 @@ import type { ClassDeclaration, MethodDefinition } from '@typescript-eslint/type
22
import type { AST, TSESTreeOptions } from '@typescript-eslint/typescript-estree';
33
import { AST_NODE_TYPES } from '@typescript-eslint/typescript-estree';
44
import type { ClassIndex, ClassLoaded, ClassReferenceLoaded, GenericallyTyped } from './ClassIndex';
5-
import type { CommentLoader } from './CommentLoader';
6-
import type { ParameterDataField, ParameterRangeUnresolved } from './ParameterLoader';
7-
import { ParameterLoader } from './ParameterLoader';
5+
import type { ParameterDataField, ParameterRangeUnresolved, ParameterLoader } from './ParameterLoader';
86

97
/**
108
* Loads the constructor data of classes.
119
*/
1210
export class ConstructorLoader {
13-
private readonly commentLoader: CommentLoader;
11+
private readonly parameterLoader: ParameterLoader;
1412

1513
public constructor(args: ConstructorLoaderArgs) {
16-
this.commentLoader = args.commentLoader;
14+
this.parameterLoader = args.parameterLoader;
1715
}
1816

1917
/**
@@ -33,10 +31,9 @@ export class ConstructorLoader {
3331

3432
// Fill in constructor data if we're loading a class, and we find a constructor in the inheritance chain.
3533
if (classLoadedRoot.type === 'class') {
36-
const parameterLoader = new ParameterLoader({ commentLoader: this.commentLoader });
3734
const constructorChain = this.getConstructorChain({ value: classLoadedRoot });
3835
if (constructorChain.length > 0) {
39-
constructorDataIndex[className] = parameterLoader.loadConstructorFields(constructorChain);
36+
constructorDataIndex[className] = this.parameterLoader.loadConstructorFields(constructorChain);
4037
}
4138
}
4239
}
@@ -124,7 +121,7 @@ export class ConstructorLoader {
124121
}
125122

126123
export interface ConstructorLoaderArgs {
127-
commentLoader: CommentLoader;
124+
parameterLoader: ParameterLoader;
128125
}
129126

130127
/**

lib/parse/ParameterLoader.ts

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { Identifier,
88
Parameter,
99
EntityName, TSTypeParameterInstantiation } from '@typescript-eslint/types/dist/ts-estree';
1010
import { AST_NODE_TYPES } from '@typescript-eslint/typescript-estree';
11+
import type { Logger } from 'winston';
1112
import type { ClassReferenceLoaded, InterfaceLoaded, ClassReference,
1213
ClassReferenceLoadedClassOrInterface, ClassIndex } from './ClassIndex';
1314
import type { CommentData, ConstructorCommentData, CommentLoader } from './CommentLoader';
@@ -25,9 +26,13 @@ export class ParameterLoader {
2526
];
2627

2728
private readonly commentLoader: CommentLoader;
29+
private readonly hardErrorUnsupported: boolean;
30+
private readonly logger: Logger;
2831

2932
public constructor(args: ParameterLoaderArgs) {
3033
this.commentLoader = args.commentLoader;
34+
this.hardErrorUnsupported = args.hardErrorUnsupported;
35+
this.logger = args.logger;
3136
}
3237

3338
/**
@@ -181,7 +186,7 @@ export class ParameterLoader {
181186
} else if (field.type === AST_NODE_TYPES.TSParameterProperty) {
182187
this.loadConstructorField(classLoaded, parameters, constructorCommentData, field.parameter);
183188
} else {
184-
throw new Error(`Could not understand constructor parameter type ${field.type} in ${classLoaded.localName} at ${classLoaded.fileName}`);
189+
this.throwOrWarn(new Error(`Could not understand constructor parameter type ${field.type} in ${classLoaded.localName} at ${classLoaded.fileName}`));
185190
}
186191
}
187192

@@ -240,7 +245,7 @@ export class ParameterLoader {
240245
}
241246
return;
242247
default:
243-
throw new Error(`Unsupported field type ${typeElement.type} in ${classLoaded.localName} in ${classLoaded.fileName}`);
248+
this.throwOrWarn(new Error(`Unsupported field type ${typeElement.type} in ${classLoaded.localName} in ${classLoaded.fileName}`));
244249
}
245250
}
246251

@@ -321,8 +326,9 @@ export class ParameterLoader {
321326
value: this.getRangeFromTypeNode(classLoaded, typeNode.typeParameters.params[0], errorIdentifier),
322327
};
323328
}
324-
throw new Error(`Found invalid Array field type at ${errorIdentifier
325-
} in ${classLoaded.localName} at ${classLoaded.fileName}`);
329+
this.throwOrWarn(new Error(`Found invalid Array field type at ${errorIdentifier
330+
} in ${classLoaded.localName} at ${classLoaded.fileName}`));
331+
return { type: 'wildcard' };
326332
default:
327333
// First check if the type is a direct generic type
328334
if (classLoaded.type !== 'enum' && typeNode.typeName.name in classLoaded.generics) {
@@ -418,8 +424,9 @@ export class ParameterLoader {
418424
};
419425
}
420426
}
421-
throw new Error(`Could not understand parameter type ${typeNode.type} of ${errorIdentifier
422-
} in ${classLoaded.localName} at ${classLoaded.fileName}`);
427+
this.throwOrWarn(new Error(`Could not understand parameter type ${typeNode.type} of ${errorIdentifier
428+
} in ${classLoaded.localName} at ${classLoaded.fileName}`));
429+
return { type: 'wildcard' };
423430
}
424431

425432
protected getGenericTypeParameterInstantiations(
@@ -461,8 +468,9 @@ export class ParameterLoader {
461468

462469
// Throw if no range was found
463470
if (!range) {
464-
throw new Error(`Missing field type on ${this.getFieldName(classLoaded, field)
465-
} in ${classLoaded.localName} at ${classLoaded.fileName}`);
471+
this.throwOrWarn(new Error(`Missing field type on ${this.getFieldName(classLoaded, field)
472+
} in ${classLoaded.localName} at ${classLoaded.fileName}`));
473+
return { type: 'wildcard' };
466474
}
467475

468476
// If the field has the '?' annotation, explicitly allow undefined as value to make it be considered optional.
@@ -610,8 +618,9 @@ export class ParameterLoader {
610618
);
611619
}
612620

613-
throw new Error(`Missing field type on ${this.getErrorIdentifierIndex()
614-
} in ${classLoaded.localName} at ${classLoaded.fileName}`);
621+
this.throwOrWarn(new Error(`Missing field type on ${this.getErrorIdentifierIndex()
622+
} in ${classLoaded.localName} at ${classLoaded.fileName}`));
623+
return { type: 'wildcard' };
615624
}
616625

617626
/**
@@ -626,10 +635,20 @@ export class ParameterLoader {
626635
}
627636
}
628637
}
638+
639+
protected throwOrWarn(error: Error): void {
640+
if (this.hardErrorUnsupported) {
641+
throw error;
642+
} else {
643+
this.logger.error(error.message);
644+
}
645+
}
629646
}
630647

631648
export interface ParameterLoaderArgs {
632649
commentLoader: CommentLoader;
650+
hardErrorUnsupported: boolean;
651+
logger: Logger;
633652
}
634653

635654
export type ParameterData<R> = ParameterDataField<R> | ParameterDataIndex<R>;

lib/parse/ParameterResolver.ts

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,25 @@ import type {
88
InterfaceLoaded,
99
} from './ClassIndex';
1010
import type { ClassLoader } from './ClassLoader';
11-
import type { CommentLoader } from './CommentLoader';
1211
import type { ConstructorData } from './ConstructorLoader';
1312
import type { GenericsData } from './GenericsLoader';
14-
import type {
15-
ExtensionData,
13+
import type { ExtensionData,
1614
GenericTypeParameterData,
1715
ParameterData,
1816
ParameterDataField,
1917
ParameterRangeResolved,
2018
ParameterRangeUnresolved,
21-
} from './ParameterLoader';
22-
import { ParameterLoader } from './ParameterLoader';
19+
ParameterLoader } from './ParameterLoader';
2320

2421
export class ParameterResolver {
2522
private readonly classLoader: ClassLoader;
26-
private readonly commentLoader: CommentLoader;
23+
private readonly parameterLoader: ParameterLoader;
2724
private readonly ignoreClasses: Record<string, boolean>;
2825
private readonly cacheInterfaceRange: LRUCache<string, Promise<ParameterRangeResolved>>;
2926

3027
public constructor(args: ParameterResolverArgs) {
3128
this.classLoader = args.classLoader;
32-
this.commentLoader = args.commentLoader;
29+
this.parameterLoader = args.parameterLoader;
3330
this.ignoreClasses = args.ignoreClasses;
3431
this.cacheInterfaceRange = new LRUCache(2_048);
3532
}
@@ -401,8 +398,7 @@ export class ParameterResolver {
401398

402399
// If we find a type alias, just interpret the type directly
403400
if (classOrInterface.type === 'type') {
404-
const parameterLoader = new ParameterLoader({ commentLoader: this.commentLoader });
405-
const unresolvedFields = parameterLoader.getRangeFromTypeNode(
401+
const unresolvedFields = this.parameterLoader.getRangeFromTypeNode(
406402
classOrInterface,
407403
classOrInterface.declaration.typeAnnotation,
408404
`type alias ${classOrInterface.localName} in ${classOrInterface.fileName}`,
@@ -413,11 +409,10 @@ export class ParameterResolver {
413409

414410
// If we find an enum, just interpret the enum value, and return as union type
415411
if (classOrInterface.type === 'enum') {
416-
const parameterLoader = new ParameterLoader({ commentLoader: this.commentLoader });
417412
const enumRangeTypes = await Promise.all(classOrInterface.declaration.members
418413
.map((enumMember, i) => {
419414
if (enumMember.initializer && enumMember.initializer.type === AST_NODE_TYPES.Literal) {
420-
return this.resolveRange(parameterLoader.getRangeFromTypeNode(
415+
return this.resolveRange(this.parameterLoader.getRangeFromTypeNode(
421416
classOrInterface,
422417
{
423418
type: AST_NODE_TYPES.TSLiteralType,
@@ -533,8 +528,7 @@ export class ParameterResolver {
533528
genericTypeRemappings: Record<string, ParameterRangeUnresolved>,
534529
handlingInterfaces: Set<string>,
535530
): Promise<ParameterData<ParameterRangeResolved>[]> {
536-
const parameterLoader = new ParameterLoader({ commentLoader: this.commentLoader });
537-
const unresolvedFields = parameterLoader.loadInterfaceFields(iface);
531+
const unresolvedFields = this.parameterLoader.loadInterfaceFields(iface);
538532
return await this.resolveParameterData(unresolvedFields, owningClass, genericTypeRemappings, handlingInterfaces);
539533
}
540534

@@ -551,14 +545,13 @@ export class ParameterResolver {
551545
genericTypeRemappings: Record<string, ParameterRangeUnresolved>,
552546
handlingInterfaces: Set<string>,
553547
): Promise<ParameterData<ParameterRangeResolved>[]> {
554-
const parameterLoader = new ParameterLoader({ commentLoader: this.commentLoader });
555-
const unresolvedFields = parameterLoader.loadHashFields(owningClass, hash);
548+
const unresolvedFields = this.parameterLoader.loadHashFields(owningClass, hash);
556549
return this.resolveParameterData(unresolvedFields, owningClass, genericTypeRemappings, handlingInterfaces);
557550
}
558551
}
559552

560553
export interface ParameterResolverArgs {
561554
classLoader: ClassLoader;
562-
commentLoader: CommentLoader;
555+
parameterLoader: ParameterLoader;
563556
ignoreClasses: Record<string, boolean>;
564557
}

0 commit comments

Comments
 (0)