From a8a607d2e57bea1435a28203e8f6a9c0a5cc896d Mon Sep 17 00:00:00 2001 From: Nidhi Manu Date: Wed, 4 Oct 2023 15:25:51 -0400 Subject: [PATCH] Support inheritance when parsing interfaces --- .../parsers/helpers/StaticParsingHelpers.ts | 7 ++- .../parsers/helpers/TypeNodeParsingHelper.ts | 39 +++++++++++++- .../parsers/StudioSourceFileParser.test.ts | 53 +++++++++++++++++++ .../TypeNodeParsingHelper.test.ts | 6 +-- 4 files changed, 100 insertions(+), 5 deletions(-) rename packages/studio-plugin/tests/parsers/{ => helpers}/TypeNodeParsingHelper.test.ts (96%) diff --git a/packages/studio-plugin/src/parsers/helpers/StaticParsingHelpers.ts b/packages/studio-plugin/src/parsers/helpers/StaticParsingHelpers.ts index 6598d447d..423ff7e66 100644 --- a/packages/studio-plugin/src/parsers/helpers/StaticParsingHelpers.ts +++ b/packages/studio-plugin/src/parsers/helpers/StaticParsingHelpers.ts @@ -18,6 +18,7 @@ import { JsxExpression, TypeAliasDeclaration, JsxAttribute, + LeftHandSideExpression, } from "ts-morph"; import { ArrayProp, @@ -203,7 +204,11 @@ export default class StaticParsingHelpers { } static getEscapedName( - p: PropertySignature | PropertyAssignment | TypeAliasDeclaration + p: + | PropertySignature + | PropertyAssignment + | TypeAliasDeclaration + | LeftHandSideExpression ): string { return p.getSymbolOrThrow().getEscapedName(); } diff --git a/packages/studio-plugin/src/parsers/helpers/TypeNodeParsingHelper.ts b/packages/studio-plugin/src/parsers/helpers/TypeNodeParsingHelper.ts index 4bb66b693..b481e8fc3 100644 --- a/packages/studio-plugin/src/parsers/helpers/TypeNodeParsingHelper.ts +++ b/packages/studio-plugin/src/parsers/helpers/TypeNodeParsingHelper.ts @@ -137,12 +137,49 @@ export default class TypeNodeParsingHelper { } } + private static getInheritedShape( + shapeDeclaration: InterfaceDeclaration | TypeLiteralNode, + parseTypeReference: (identifier: string) => ParsedType | undefined + ): ParsedShape { + if (!shapeDeclaration.isKind(SyntaxKind.InterfaceDeclaration)) { + return {}; + } + + const heritageClause = shapeDeclaration.getHeritageClauseByKind( + SyntaxKind.ExtendsKeyword + ); + if (!heritageClause) { + return {}; + } + + const inheritedTypes = heritageClause.getTypeNodes(); + return inheritedTypes.reduce((inheritedShape, inheritedType) => { + const typeName = StaticParsingHelpers.getEscapedName( + inheritedType.getExpression() + ); + const parsedType = parseTypeReference(typeName); + if (parsedType?.kind === ParsedTypeKind.Object) { + return { + ...inheritedShape, + ...parsedType.type, + }; + } else { + throw new Error( + `Error parsing interface: Unable to resolve inherited type ${typeName} to an object type.` + ); + } + }, {} as ParsedShape); + } + private static handleObjectType( shapeDeclaration: InterfaceDeclaration | TypeLiteralNode, parseTypeReference: (identifier: string) => ParsedType | undefined ): ObjectParsedType { const properties = shapeDeclaration.getProperties(); - const parsedShape: ParsedShape = {}; + const parsedShape = this.getInheritedShape( + shapeDeclaration, + parseTypeReference + ); properties.forEach((p) => { const propertyName = StaticParsingHelpers.getEscapedName(p); diff --git a/packages/studio-plugin/tests/parsers/StudioSourceFileParser.test.ts b/packages/studio-plugin/tests/parsers/StudioSourceFileParser.test.ts index e40eeb1c8..8bae7e622 100644 --- a/packages/studio-plugin/tests/parsers/StudioSourceFileParser.test.ts +++ b/packages/studio-plugin/tests/parsers/StudioSourceFileParser.test.ts @@ -200,6 +200,59 @@ describe("parseShape", () => { }); }); + it("can parse an interface with inherited properties", () => { + const parser = createParser( + `interface BaseProps { + title?: string + } + + interface HelloProps { + hello: { + world?: string + } + } + + export interface Props extends BaseProps, HelloProps { + hello: { + world: string + } + }` + ); + + expect(parser.parseTypeReference("Props")?.type).toEqual({ + title: { + kind: ParsedTypeKind.Simple, + required: false, + type: "string", + }, + hello: { + kind: ParsedTypeKind.Object, + required: true, + type: { + world: { + kind: ParsedTypeKind.Simple, + required: true, + type: "string", + }, + }, + }, + }); + }); + + it("throws an error if the inherited type is not an object type", () => { + const parser = createParser( + `import { MyString } from "../__fixtures__/StudioSourceFileParser/exportedTypes"; + + export interface Props extends MyString { + title: string; + }` + ); + + expect(() => parser.parseTypeReference("Props")).toThrowError( + "Error parsing interface: Unable to resolve inherited type MyString to an object type." + ); + }); + it("can parse a sub-property with a renamed nested type from an external package", () => { const parser = createParser( `import { ApplyFiltersButtonProps } from "@yext/search-ui-react"; diff --git a/packages/studio-plugin/tests/parsers/TypeNodeParsingHelper.test.ts b/packages/studio-plugin/tests/parsers/helpers/TypeNodeParsingHelper.test.ts similarity index 96% rename from packages/studio-plugin/tests/parsers/TypeNodeParsingHelper.test.ts rename to packages/studio-plugin/tests/parsers/helpers/TypeNodeParsingHelper.test.ts index af57cb136..e7922d55e 100644 --- a/packages/studio-plugin/tests/parsers/TypeNodeParsingHelper.test.ts +++ b/packages/studio-plugin/tests/parsers/helpers/TypeNodeParsingHelper.test.ts @@ -1,9 +1,9 @@ import { SyntaxKind } from "ts-morph"; import TypeNodeParsingHelper, { ParsedTypeKind, -} from "../../src/parsers/helpers/TypeNodeParsingHelper"; -import createTestSourceFile from "../__utils__/createTestSourceFile"; -import { PropValueType } from "../../src/types"; +} from "../../../src/parsers/helpers/TypeNodeParsingHelper"; +import createTestSourceFile from "../../__utils__/createTestSourceFile"; +import { PropValueType } from "../../../src/types"; const externalShapeParser = jest.fn();