Skip to content

Commit eaffca7

Browse files
feat: add unity RequireComponent compatibility (#114)
* feat: add RequireComponent class decorator functionally * feat(transformer): more consistent unity like decorator * refactor: change decorator to type parameters
1 parent 2f16e03 commit eaffca7

File tree

2 files changed

+127
-32
lines changed

2 files changed

+127
-32
lines changed

src/Shared/diagnostics.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,34 @@ export const errors = {
244244
];
245245
}),
246246

247+
requiredComponentTypeParameterRequired: errorWithContext((className: string) => {
248+
return [
249+
`@RequireComponent decorator on class '${className}' requires at least one type parameter`,
250+
suggestion("Use @RequireComponent<ComponentType>() where ComponentType is a Unity component or AirshipBehaviour"),
251+
];
252+
}),
253+
254+
requiredComponentArgumentRequired: errorWithContext((className: string) => {
255+
return [
256+
`@RequireComponent decorator on class '${className}' requires at least one type parameter`,
257+
suggestion("Use @RequireComponent<ComponentType>() where ComponentType is a Unity component or AirshipBehaviour"),
258+
];
259+
}),
260+
261+
requiredComponentInvalidType: errorWithContext((className: string, typeName: string) => {
262+
return [
263+
`@RequireComponent decorator on class '${className}' received invalid component type '${typeName}'`,
264+
suggestion("Component type must be a Unity component or AirshipBehaviour. Try @RequireComponent<ValidComponentType>()"),
265+
];
266+
}),
267+
268+
requiredComponentInvalidArgument: errorWithContext((className: string, argumentType: string) => {
269+
return [
270+
`@RequireComponent decorator on class '${className}' received invalid argument of type '${argumentType}'`,
271+
suggestion("Use @RequireComponent<ComponentType>() where ComponentType is a Unity component or AirshipBehaviour"),
272+
];
273+
}),
274+
247275
unityMacroTypeArgumentRequired: errorWithContext((methodName: string) => {
248276
return [
249277
`Macro ${methodName}<T>() requires a type argument at T`,
@@ -252,7 +280,7 @@ export const errors = {
252280
}),
253281

254282
decoratorParamsLiteralsOnly: error(
255-
"Airship Behaviour decorators only accept literal `string`, `boolean` or `number` values",
283+
"Airship Behaviour decorators only accept literal `string`, `boolean` or `number` values. For RequireComponent, use `typeof(ComponentType)` syntax.",
256284
),
257285

258286
// files

src/TSTransformer/nodes/class/transformClassLikeDeclaration.ts

Lines changed: 98 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -615,47 +615,113 @@ function createAirshipProperty(
615615
return prop;
616616
}
617617

618-
export function getClassDecorators(state: TransformState, classNode: ts.ClassLikeDeclaration) {
619-
const decorators = ts.hasDecorators(classNode) ? ts.getDecorators(classNode) : undefined;
620-
if (decorators) {
621-
const items = new Array<AirshipBehaviourClassDecorator>();
618+
function processRequireComponentDecorator(
619+
state: TransformState,
620+
classNode: ts.ClassLikeDeclaration,
621+
expression: ts.CallExpression,
622+
): AirshipBehaviourClassDecorator | undefined {
623+
const typeChecker = state.typeChecker;
624+
const typeArguments = expression.typeArguments;
625+
626+
if (!typeArguments || typeArguments.length === 0) {
627+
DiagnosticService.addDiagnostic(errors.requiredComponentTypeParameterRequired(classNode, classNode.name?.text || "<anonymous>"));
628+
return undefined;
629+
}
622630

623-
for (const decorator of decorators) {
624-
const expression = decorator.expression;
625-
if (!ts.isCallExpression(expression)) continue;
631+
const componentParameters = new Array<AirshipBehaviourFieldDecoratorParameter>();
632+
for (const typeArgument of typeArguments) {
633+
const type = typeChecker.getTypeFromTypeNode(typeArgument);
626634

627-
const aliasSymbol = state.typeChecker.getTypeAtLocation(expression).aliasSymbol;
628-
if (!aliasSymbol) continue;
635+
if (isAirshipBehaviourType(state, type)) {
636+
const typeString = typeChecker.typeToString(type);
637+
componentParameters.push({
638+
type: "AirshipBehaviour",
639+
value: typeString,
640+
});
641+
} else if (isUnityObjectType(state, type)) {
642+
const nonNullableType = typeChecker.getNonNullableType(type);
643+
const nonNullableTypeString = typeChecker.typeToString(nonNullableType);
644+
componentParameters.push({
645+
type: "object",
646+
value: nonNullableTypeString,
647+
});
648+
} else {
649+
const typeString = typeChecker.typeToString(type);
650+
DiagnosticService.addDiagnostic(errors.requiredComponentInvalidType(classNode, classNode.name?.text ?? "<anonymous>", typeString));
651+
}
652+
}
629653

630-
const airshipFieldSymbol = state.services.airshipSymbolManager.getSymbolOrThrow("AirshipDecorator");
654+
if (componentParameters.length === 0) {
655+
return undefined;
656+
}
631657

632-
if (aliasSymbol === airshipFieldSymbol) {
633-
items.push({
634-
name: expression.expression.getText(),
635-
typeParameters: expression.typeArguments?.map(typeNode => {
636-
return state.typeChecker.typeToString(state.typeChecker.getTypeFromTypeNode(typeNode));
637-
}),
638-
parameters: expression.arguments.map((argument, i): AirshipBehaviourFieldDecoratorParameter => {
639-
const value = getLiteralFromNode(state, argument);
658+
return {
659+
name: "RequireComponent",
660+
typeParameters: expression.typeArguments?.map(typeNode => {
661+
return state.typeChecker.typeToString(state.typeChecker.getTypeFromTypeNode(typeNode));
662+
}),
663+
parameters: componentParameters,
664+
};
665+
}
640666

641-
if (value) {
642-
return value;
643-
} else {
644-
DiagnosticService.addDiagnostic(
645-
errors.decoratorParamsLiteralsOnly(expression.arguments[i]),
646-
);
667+
function processGenericDecorator(
668+
state: TransformState,
669+
expression: ts.CallExpression,
670+
decoratorName: string,
671+
): AirshipBehaviourClassDecorator {
672+
return {
673+
name: decoratorName,
674+
typeParameters: expression.typeArguments?.map(typeNode => {
675+
return state.typeChecker.typeToString(state.typeChecker.getTypeFromTypeNode(typeNode));
676+
}),
677+
parameters: expression.arguments.map((argument, i): AirshipBehaviourFieldDecoratorParameter => {
678+
const value = getLiteralFromNode(state, argument);
647679

648-
return { type: "invalid", value: undefined };
649-
}
650-
}),
651-
});
680+
if (value) {
681+
return value;
682+
} else {
683+
DiagnosticService.addDiagnostic(
684+
errors.decoratorParamsLiteralsOnly(expression.arguments[i]),
685+
);
686+
687+
return { type: "invalid", value: undefined };
652688
}
653-
}
689+
}),
690+
};
691+
}
654692

655-
return items;
656-
} else {
693+
export function getClassDecorators(state: TransformState, classNode: ts.ClassLikeDeclaration) {
694+
const decorators = ts.hasDecorators(classNode) ? ts.getDecorators(classNode) : undefined;
695+
if (!decorators) {
657696
return [];
658697
}
698+
699+
const items = new Array<AirshipBehaviourClassDecorator>();
700+
701+
for (const decorator of decorators) {
702+
const expression = decorator.expression;
703+
if (!ts.isCallExpression(expression)) continue;
704+
705+
const aliasSymbol = state.typeChecker.getTypeAtLocation(expression).aliasSymbol;
706+
if (!aliasSymbol) continue;
707+
708+
const airshipFieldSymbol = state.services.airshipSymbolManager.getSymbolOrThrow("AirshipDecorator");
709+
if (aliasSymbol !== airshipFieldSymbol) continue;
710+
711+
const decoratorName = expression.expression.getText();
712+
713+
if (decoratorName === "RequireComponent") {
714+
const processedDecorator = processRequireComponentDecorator(state, classNode, expression);
715+
if (processedDecorator) {
716+
items.push(processedDecorator);
717+
}
718+
} else {
719+
const processedDecorator = processGenericDecorator(state, expression, decoratorName);
720+
items.push(processedDecorator);
721+
}
722+
}
723+
724+
return items;
659725
}
660726

661727
function getPropertyDecorators(
@@ -1056,3 +1122,4 @@ export function transformClassLikeDeclaration(state: TransformState, node: ts.Cl
10561122

10571123
return { statements, name: returnVar };
10581124
}
1125+

0 commit comments

Comments
 (0)