From 61e8e28b2c85d53440049c354061b9a03c5e031c Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Sat, 20 Jul 2024 13:28:49 -0700 Subject: [PATCH] Require interfaces to use abstract classes --- src/Errors.ts | 4 + src/Extractor.ts | 6 + src/TypeContext.ts | 54 ++---- .../addStringFieldToInterface.ts.expected | 117 ++++-------- ...nterfaceImplementedByInterface.ts.expected | 169 +++++++++--------- ...ticClassMethodOnClassInterface.ts.expected | 51 ++---- ...lassInheritsFieldFromInterface.ts.expected | 59 ++---- ...assInheritsInterfaceFromParent.ts.expected | 115 ++++-------- ...ntButIsMissingTypeName.invalid.ts.expected | 115 ++++-------- ...erfaceInheritsFieldFromParents.ts.expected | 96 ++++------ ...ssInterfaceImplementsInterface.ts.expected | 76 +------- ...assInterfaceNamedQuery.invalid.ts.expected | 2 +- src/transforms/propagateHeritage.ts | 2 +- 13 files changed, 296 insertions(+), 570 deletions(-) diff --git a/src/Errors.ts b/src/Errors.ts index 20980325..4607c27a 100644 --- a/src/Errors.ts +++ b/src/Errors.ts @@ -64,6 +64,10 @@ export function invalidInterfaceTagUsage() { return `\`@${INTERFACE_TAG}\` can only be used on interface or abstract class declarations. e.g. \`interface MyInterface {}\` or \`abstract class MyInterface {}\``; } +export function interfaceClassNotAbstract() { + return `Expected \`@${INTERFACE_TAG}\` class to be abstract. \`@${INTERFACE_TAG}\` can only be used on interface or abstract class declarations. e.g. \`interface MyInterface {}\` or \`abstract class MyInterface {}\``; +} + export function invalidEnumTagUsage() { return `\`@${ENUM_TAG}\` can only be used on enum declarations or TypeScript unions. e.g. \`enum MyEnum {}\` or \`type MyEnum = "foo" | "bar"\``; } diff --git a/src/Extractor.ts b/src/Extractor.ts index 248fb410..3d6a6aba 100644 --- a/src/Extractor.ts +++ b/src/Extractor.ts @@ -1127,6 +1127,12 @@ class Extractor { } interfaceClassDeclaration(node: ts.ClassDeclaration, tag: ts.JSDocTag) { + const isAbstract = node.modifiers?.some((modifier) => { + return modifier.kind === ts.SyntaxKind.AbstractKeyword; + }); + if (!isAbstract) { + return this.report(node, E.interfaceClassNotAbstract()); + } if (node.name == null) { return this.report(node, E.typeTagOnUnnamedClass()); } diff --git a/src/TypeContext.ts b/src/TypeContext.ts index ef9d7556..26443cb3 100644 --- a/src/TypeContext.ts +++ b/src/TypeContext.ts @@ -219,39 +219,18 @@ export class TypeContext { // Given the name of a class or interface, return all the parent classes and // interfaces. - getAllParentsForName(name: ts.Identifier): Set { + getAllParentClassesForName(name: ts.Identifier): Set { const symbol = this.checker.getSymbolAtLocation(name); if (symbol == null) { return new Set(); } - return this.getAllParents(symbol); + return this.getAllParentClasses(symbol); } /* - * Walk the inheritance chain and collect all the parent classes and - * interfaces. - * - * NOTE! Recursion order here is important and part of our documented - * behavior. We do an ordered breadth-first traversal to ensure that - * if a class implements multiple interfaces, and those interfaces each - * implement the same field, we'll inherit the field implementation from - * the first interface in the list. - * - * Normally JavaScript/TypeScript avoid this issue by implementing only - * _single_ inheritance, but because we allow inheriting fields from - * interfaces, and TypeScript allows classes (and interfaces!) to - * implement/extend multiple interfaces, we must handle this multi-inheritance - * issue. - * - * The approach taken here is modeled after Python's MRO (Method Resolution - * Order) algorithm. - * - * https://docs.python.org/3/howto/mro.html - * - * TODO: Actually read that paper and see if that's the approach we want to - * take then implement it. + * Walk the inheritance chain and collect all the parent classes. */ - getAllParents( + getAllParentClasses( symbol: ts.Symbol, parents: Set = new Set(), ): Set { @@ -260,11 +239,11 @@ export class TypeContext { } for (const declaration of symbol.declarations) { - const heritage = getHeritage(declaration); - if (heritage == null) { + const extendsClauses = getClassExtendClauses(declaration); + if (extendsClauses == null) { continue; } - for (const heritageClause of heritage) { + for (const heritageClause of extendsClauses) { for (const type of heritageClause.types) { const typeSymbol = this.checker.getSymbolAtLocation(type.expression); if (typeSymbol == null || typeSymbol.declarations == null) { @@ -277,7 +256,7 @@ export class TypeContext { } } // Recurse to find the parents of the parent. - this.getAllParents(typeSymbol, parents); + this.getAllParentClasses(typeSymbol, parents); } } } @@ -285,14 +264,17 @@ export class TypeContext { } } -function getHeritage( +function getClassExtendClauses( declaration: ts.Declaration, -): ts.NodeArray | null { - if ( - ts.isClassDeclaration(declaration) || - ts.isInterfaceDeclaration(declaration) - ) { - return declaration.heritageClauses ?? null; +): ts.HeritageClause[] | null { + if (ts.isClassDeclaration(declaration)) { + const { heritageClauses } = declaration; + if (heritageClauses == null) { + return null; + } + return heritageClauses.filter( + (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword, + ); } return null; } diff --git a/src/tests/fixtures/extend_interface/addStringFieldToInterface.ts.expected b/src/tests/fixtures/extend_interface/addStringFieldToInterface.ts.expected index 89134476..99466313 100644 --- a/src/tests/fixtures/extend_interface/addStringFieldToInterface.ts.expected +++ b/src/tests/fixtures/extend_interface/addStringFieldToInterface.ts.expected @@ -32,84 +32,43 @@ class Admin implements IPerson { ----------------- OUTPUT ----------------- --- SDL -- -interface IPerson { - greeting: String @metadata(argCount: 1, exportName: "greeting", tsModulePath: "grats/src/tests/fixtures/extend_interface/addStringFieldToInterface.ts") - hello: String @metadata -} +src/tests/fixtures/extend_interface/addStringFieldToInterface.ts:9:1 - error: Interface field IPerson.greeting expected but Admin does not provide it. -type Admin implements IPerson { - greeting: String @metadata(argCount: 1, exportName: "greeting", tsModulePath: "grats/src/tests/fixtures/extend_interface/addStringFieldToInterface.ts") - hello: String @metadata -} + 9 export function greeting(person: IPerson): string { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +10 return `Hello ${person.name}!`; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +11 } + ~ -type User implements IPerson { - greeting: String @metadata(argCount: 1, exportName: "greeting", tsModulePath: "grats/src/tests/fixtures/extend_interface/addStringFieldToInterface.ts") - hello: String @metadata -} --- TypeScript -- -import { greeting as adminGreetingResolver } from "./addStringFieldToInterface"; -import { greeting as userGreetingResolver } from "./addStringFieldToInterface"; -import { GraphQLSchema, GraphQLInterfaceType, GraphQLString, GraphQLObjectType } from "graphql"; -export function getSchema(): GraphQLSchema { - const IPersonType: GraphQLInterfaceType = new GraphQLInterfaceType({ - name: "IPerson", - fields() { - return { - greeting: { - name: "greeting", - type: GraphQLString - }, - hello: { - name: "hello", - type: GraphQLString - } - }; - } - }); - const AdminType: GraphQLObjectType = new GraphQLObjectType({ - name: "Admin", - fields() { - return { - greeting: { - name: "greeting", - type: GraphQLString, - resolve(source) { - return adminGreetingResolver(source); - } - }, - hello: { - name: "hello", - type: GraphQLString - } - }; - }, - interfaces() { - return [IPersonType]; - } - }); - const UserType: GraphQLObjectType = new GraphQLObjectType({ - name: "User", - fields() { - return { - greeting: { - name: "greeting", - type: GraphQLString, - resolve(source) { - return userGreetingResolver(source); - } - }, - hello: { - name: "hello", - type: GraphQLString - } - }; - }, - interfaces() { - return [IPersonType]; - } - }); - return new GraphQLSchema({ - types: [IPersonType, AdminType, UserType] - }); -} + src/tests/fixtures/extend_interface/addStringFieldToInterface.ts:22:1 + 22 class Admin implements IPerson { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 23 __typename: "Admin"; + ~~~~~~~~~~~~~~~~~~~~~~ + ... + 26 hello: string; + ~~~~~~~~~~~~~~~~ + 27 } + ~ + Related location +src/tests/fixtures/extend_interface/addStringFieldToInterface.ts:9:1 - error: Interface field IPerson.greeting expected but User does not provide it. + + 9 export function greeting(person: IPerson): string { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +10 return `Hello ${person.name}!`; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +11 } + ~ + + src/tests/fixtures/extend_interface/addStringFieldToInterface.ts:14:1 + 14 class User implements IPerson { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 15 __typename: "User"; + ~~~~~~~~~~~~~~~~~~~~~ + ... + 18 hello: string; + ~~~~~~~~~~~~~~~~ + 19 } + ~ + Related location diff --git a/src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts.expected b/src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts.expected index afb983e9..7cf15775 100644 --- a/src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts.expected +++ b/src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts.expected @@ -37,87 +37,94 @@ class Admin implements IPerson, IThing { ----------------- OUTPUT ----------------- --- SDL -- -interface IPerson implements IThing { - greeting: String @metadata(argCount: 1, exportName: "greeting", tsModulePath: "grats/src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts") -} +src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts:15:1 - error: Type IPerson must define one or more fields. -interface IThing { - greeting: String @metadata(argCount: 1, exportName: "greeting", tsModulePath: "grats/src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts") -} +15 interface IPerson extends IThing { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +16 name: string; + ~~~~~~~~~~~~~~~ +17 // Should have greeting added + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +18 } + ~ +src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts:2:1 - error: Interface field IThing.greeting expected but IPerson does not provide it. -type Admin implements IPerson & IThing { - greeting: String @metadata(argCount: 1, exportName: "greeting", tsModulePath: "grats/src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts") -} +2 export function greeting(thing: IThing): string { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +3 return `Hello ${thing.name}!`; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +4 } + ~ -type User implements IPerson & IThing { - greeting: String @metadata(argCount: 1, exportName: "greeting", tsModulePath: "grats/src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts") -} --- TypeScript -- -import { greeting as adminGreetingResolver } from "./addStringFieldToInterfaceImplementedByInterface"; -import { greeting as userGreetingResolver } from "./addStringFieldToInterfaceImplementedByInterface"; -import { GraphQLSchema, GraphQLInterfaceType, GraphQLString, GraphQLObjectType } from "graphql"; -export function getSchema(): GraphQLSchema { - const IThingType: GraphQLInterfaceType = new GraphQLInterfaceType({ - name: "IThing", - fields() { - return { - greeting: { - name: "greeting", - type: GraphQLString - } - }; - } - }); - const IPersonType: GraphQLInterfaceType = new GraphQLInterfaceType({ - name: "IPerson", - fields() { - return { - greeting: { - name: "greeting", - type: GraphQLString - } - }; - }, - interfaces() { - return [IThingType]; - } - }); - const AdminType: GraphQLObjectType = new GraphQLObjectType({ - name: "Admin", - fields() { - return { - greeting: { - name: "greeting", - type: GraphQLString, - resolve(source) { - return adminGreetingResolver(source); - } - } - }; - }, - interfaces() { - return [IPersonType, IThingType]; - } - }); - const UserType: GraphQLObjectType = new GraphQLObjectType({ - name: "User", - fields() { - return { - greeting: { - name: "greeting", - type: GraphQLString, - resolve(source) { - return userGreetingResolver(source); - } - } - }; - }, - interfaces() { - return [IPersonType, IThingType]; - } - }); - return new GraphQLSchema({ - types: [IPersonType, IThingType, AdminType, UserType] - }); -} + src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts:15:1 + 15 interface IPerson extends IThing { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 16 name: string; + ~~~~~~~~~~~~~~~ + 17 // Should have greeting added + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 18 } + ~ + Related location +src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts:28:1 - error: Type Admin must define one or more fields. + + 28 class Admin implements IPerson, IThing { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 29 __typename: "Admin"; + ~~~~~~~~~~~~~~~~~~~~~~ +... + 31 // Should have greeting added + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 32 } + ~ +src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts:2:1 - error: Interface field IThing.greeting expected but Admin does not provide it. + +2 export function greeting(thing: IThing): string { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +3 return `Hello ${thing.name}!`; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +4 } + ~ + + src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts:28:1 + 28 class Admin implements IPerson, IThing { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 29 __typename: "Admin"; + ~~~~~~~~~~~~~~~~~~~~~~ + ... + 31 // Should have greeting added + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 32 } + ~ + Related location +src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts:21:1 - error: Type User must define one or more fields. + + 21 class User implements IPerson, IThing { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 22 __typename: "User"; + ~~~~~~~~~~~~~~~~~~~~~ +... + 24 // Should have greeting added + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 25 } + ~ +src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts:2:1 - error: Interface field IThing.greeting expected but User does not provide it. + +2 export function greeting(thing: IThing): string { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +3 return `Hello ${thing.name}!`; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +4 } + ~ + + src/tests/fixtures/extend_interface/addStringFieldToInterfaceImplementedByInterface.ts:21:1 + 21 class User implements IPerson, IThing { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 22 __typename: "User"; + ~~~~~~~~~~~~~~~~~~~~~ + ... + 24 // Should have greeting added + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 25 } + ~ + Related location diff --git a/src/tests/fixtures/field_definitions/FieldAsStaticClassMethodOnClassInterface.ts.expected b/src/tests/fixtures/field_definitions/FieldAsStaticClassMethodOnClassInterface.ts.expected index f51d7649..1b0df47e 100644 --- a/src/tests/fixtures/field_definitions/FieldAsStaticClassMethodOnClassInterface.ts.expected +++ b/src/tests/fixtures/field_definitions/FieldAsStaticClassMethodOnClassInterface.ts.expected @@ -18,45 +18,14 @@ type Query = unknown; ----------------- OUTPUT ----------------- --- SDL -- -interface User { - name: String @metadata -} +src/tests/fixtures/field_definitions/FieldAsStaticClassMethodOnClassInterface.ts:2:1 - error: Expected `@gqlInterface` class to be abstract. `@gqlInterface` can only be used on interface or abstract class declarations. e.g. `interface MyInterface {}` or `abstract class MyInterface {}` -type Query { - getUser: User @metadata(argCount: 1, exportName: "User", name: "getUser", tsModulePath: "grats/src/tests/fixtures/field_definitions/FieldAsStaticClassMethodOnClassInterface.ts") -} --- TypeScript -- -import { User as queryGetUserResolver } from "./FieldAsStaticClassMethodOnClassInterface"; -import { GraphQLSchema, GraphQLObjectType, GraphQLInterfaceType, GraphQLString } from "graphql"; -export function getSchema(): GraphQLSchema { - const UserType: GraphQLInterfaceType = new GraphQLInterfaceType({ - name: "User", - fields() { - return { - name: { - name: "name", - type: GraphQLString - } - }; - } - }); - const QueryType: GraphQLObjectType = new GraphQLObjectType({ - name: "Query", - fields() { - return { - getUser: { - name: "getUser", - type: UserType, - resolve(source) { - return queryGetUserResolver.getUser(source); - } - } - }; - } - }); - return new GraphQLSchema({ - query: QueryType, - types: [UserType, QueryType] - }); -} + 2 export class User { + ~~~~~~~~~~~~~~~~~~~ + 3 /** @gqlField */ + ~~~~~~~~~~~~~~~~~~ +... + 9 } + ~~~ + 10 } + ~ diff --git a/src/tests/fixtures/inheritance/classInheritsFieldFromInterface.ts.expected b/src/tests/fixtures/inheritance/classInheritsFieldFromInterface.ts.expected index 79fcbe0e..7fa2794c 100644 --- a/src/tests/fixtures/inheritance/classInheritsFieldFromInterface.ts.expected +++ b/src/tests/fixtures/inheritance/classInheritsFieldFromInterface.ts.expected @@ -18,48 +18,19 @@ export class MyType implements MyInterface { ----------------- OUTPUT ----------------- --- SDL -- -interface MyInterface { - interfaceField: String @metadata -} +src/tests/fixtures/inheritance/classInheritsFieldFromInterface.ts:4:3 - error: Interface field MyInterface.interfaceField expected but MyType does not provide it. -type MyType implements MyInterface { - interfaceField: String @metadata - typeField: String @metadata -} --- TypeScript -- -import { GraphQLSchema, GraphQLInterfaceType, GraphQLString, GraphQLObjectType } from "graphql"; -export function getSchema(): GraphQLSchema { - const MyInterfaceType: GraphQLInterfaceType = new GraphQLInterfaceType({ - name: "MyInterface", - fields() { - return { - interfaceField: { - name: "interfaceField", - type: GraphQLString - } - }; - } - }); - const MyTypeType: GraphQLObjectType = new GraphQLObjectType({ - name: "MyType", - fields() { - return { - interfaceField: { - name: "interfaceField", - type: GraphQLString - }, - typeField: { - name: "typeField", - type: GraphQLString - } - }; - }, - interfaces() { - return [MyInterfaceType]; - } - }); - return new GraphQLSchema({ - types: [MyInterfaceType, MyTypeType] - }); -} +4 interfaceField: string; + ~~~~~~~~~~~~~~~~~~~~~~~ + + src/tests/fixtures/inheritance/classInheritsFieldFromInterface.ts:8:1 + 8 export class MyType implements MyInterface { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 9 __typename: "MyType" = "MyType"; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ... + 12 typeField: string; + ~~~~~~~~~~~~~~~~~~~~ + 13 } + ~ + Related location diff --git a/src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts.expected b/src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts.expected index 3e0bf05b..5bf22bd9 100644 --- a/src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts.expected +++ b/src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts.expected @@ -21,86 +21,39 @@ export class Child extends Parent { ----------------- OUTPUT ----------------- --- SDL -- -interface MyInterface { - parentField: String @metadata -} +src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts:4:3 - error: Interface field MyInterface.parentField expected but Child does not provide it. -type Child implements MyInterface { - childField: String @metadata - parentField: String @metadata -} +4 parentField: string; + ~~~~~~~~~~~~~~~~~~~~ -type Parent implements MyInterface { - parentField: String @metadata -} --- TypeScript -- -import { Child as ChildClass } from "./classInheritsInterfaceFromParent"; -import { Parent as ParentClass } from "./classInheritsInterfaceFromParent"; -import { GraphQLSchema, GraphQLInterfaceType, GraphQLString, GraphQLObjectType } from "graphql"; -export function getSchema(): GraphQLSchema { - const MyInterfaceType: GraphQLInterfaceType = new GraphQLInterfaceType({ - name: "MyInterface", - fields() { - return { - parentField: { - name: "parentField", - type: GraphQLString - } - }; - }, - resolveType - }); - const ChildType: GraphQLObjectType = new GraphQLObjectType({ - name: "Child", - fields() { - return { - childField: { - name: "childField", - type: GraphQLString - }, - parentField: { - name: "parentField", - type: GraphQLString - } - }; - }, - interfaces() { - return [MyInterfaceType]; - } - }); - const ParentType: GraphQLObjectType = new GraphQLObjectType({ - name: "Parent", - fields() { - return { - parentField: { - name: "parentField", - type: GraphQLString - } - }; - }, - interfaces() { - return [MyInterfaceType]; - } - }); - return new GraphQLSchema({ - types: [MyInterfaceType, ChildType, ParentType] - }); -} -const typeNameMap = new Map(); -typeNameMap.set(ChildClass, "Child"); -typeNameMap.set(ParentClass, "Parent"); -function resolveType(obj: any): string { - if (typeof obj.__typename === "string") { - return obj.__typename; - } - let prototype = Object.getPrototypeOf(obj); - while (prototype) { - const name = typeNameMap.get(prototype.constructor); - if (name != null) { - return name; - } - prototype = Object.getPrototypeOf(prototype); - } - throw new Error("Cannot find type name."); -} + src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts:13:1 + 13 export class Child extends Parent { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 14 /** @gqlField */ + ~~~~~~~~~~~~~~~~~~ + 15 childField: string; + ~~~~~~~~~~~~~~~~~~~~~ + 16 } + ~ + Related location +src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts:8:1 - error: Type Parent must define one or more fields. + + 8 export class Parent implements MyInterface { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 9 parentField: string; + ~~~~~~~~~~~~~~~~~~~~~~ +10 } + ~ +src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts:4:3 - error: Interface field MyInterface.parentField expected but Parent does not provide it. + +4 parentField: string; + ~~~~~~~~~~~~~~~~~~~~ + + src/tests/fixtures/inheritance/classInheritsInterfaceFromParent.ts:8:1 + 8 export class Parent implements MyInterface { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 9 parentField: string; + ~~~~~~~~~~~~~~~~~~~~~~ + 10 } + ~ + Related location diff --git a/src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts.expected b/src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts.expected index 014f29f1..022b3b3f 100644 --- a/src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts.expected +++ b/src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts.expected @@ -24,86 +24,39 @@ export class Child extends Parent { ----------------- OUTPUT ----------------- --- SDL -- -interface MyInterface { - parentField: String @metadata -} +src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts:4:3 - error: Interface field MyInterface.parentField expected but Child does not provide it. -type Child implements MyInterface { - childField: String @metadata - parentField: String @metadata -} +4 parentField: string; + ~~~~~~~~~~~~~~~~~~~~ -type Parent implements MyInterface { - parentField: String @metadata -} --- TypeScript -- -import { Child as ChildClass } from "./classInheritsInterfaceFromParentButIsMissingTypeName.invalid"; -import { Parent as ParentClass } from "./classInheritsInterfaceFromParentButIsMissingTypeName.invalid"; -import { GraphQLSchema, GraphQLInterfaceType, GraphQLString, GraphQLObjectType } from "graphql"; -export function getSchema(): GraphQLSchema { - const MyInterfaceType: GraphQLInterfaceType = new GraphQLInterfaceType({ - name: "MyInterface", - fields() { - return { - parentField: { - name: "parentField", - type: GraphQLString - } - }; - }, - resolveType - }); - const ChildType: GraphQLObjectType = new GraphQLObjectType({ - name: "Child", - fields() { - return { - childField: { - name: "childField", - type: GraphQLString - }, - parentField: { - name: "parentField", - type: GraphQLString - } - }; - }, - interfaces() { - return [MyInterfaceType]; - } - }); - const ParentType: GraphQLObjectType = new GraphQLObjectType({ - name: "Parent", - fields() { - return { - parentField: { - name: "parentField", - type: GraphQLString - } - }; - }, - interfaces() { - return [MyInterfaceType]; - } - }); - return new GraphQLSchema({ - types: [MyInterfaceType, ChildType, ParentType] - }); -} -const typeNameMap = new Map(); -typeNameMap.set(ChildClass, "Child"); -typeNameMap.set(ParentClass, "Parent"); -function resolveType(obj: any): string { - if (typeof obj.__typename === "string") { - return obj.__typename; - } - let prototype = Object.getPrototypeOf(obj); - while (prototype) { - const name = typeNameMap.get(prototype.constructor); - if (name != null) { - return name; - } - prototype = Object.getPrototypeOf(prototype); - } - throw new Error("Cannot find type name."); -} + src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts:13:1 + 13 export class Child extends Parent { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 14 /** @gqlField */ + ~~~~~~~~~~~~~~~~~~ + 15 childField: string; + ~~~~~~~~~~~~~~~~~~~~~ + 16 } + ~ + Related location +src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts:8:1 - error: Type Parent must define one or more fields. + + 8 export class Parent implements MyInterface { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 9 parentField: string; + ~~~~~~~~~~~~~~~~~~~~~~ +10 } + ~ +src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts:4:3 - error: Interface field MyInterface.parentField expected but Parent does not provide it. + +4 parentField: string; + ~~~~~~~~~~~~~~~~~~~~ + + src/tests/fixtures/inheritance/classInheritsInterfaceFromParentButIsMissingTypeName.invalid.ts:8:1 + 8 export class Parent implements MyInterface { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 9 parentField: string; + ~~~~~~~~~~~~~~~~~~~~~~ + 10 } + ~ + Related location diff --git a/src/tests/fixtures/inheritance/interfaceInheritsFieldFromParents.ts.expected b/src/tests/fixtures/inheritance/interfaceInheritsFieldFromParents.ts.expected index 6f3a6879..722bf2a1 100644 --- a/src/tests/fixtures/inheritance/interfaceInheritsFieldFromParents.ts.expected +++ b/src/tests/fixtures/inheritance/interfaceInheritsFieldFromParents.ts.expected @@ -22,63 +22,43 @@ interface C extends A, B { ----------------- OUTPUT ----------------- --- SDL -- -interface A { - aField: String @metadata -} +src/tests/fixtures/inheritance/interfaceInheritsFieldFromParents.ts:14:1 - error: Type C must define one or more fields. -interface B { - bField: String @metadata -} +14 interface C extends A, B { + ~~~~~~~~~~~~~~~~~~~~~~~~~~ +15 aField: string; + ~~~~~~~~~~~~~~~~~ +16 bField: string; + ~~~~~~~~~~~~~~~~~ +17 } + ~ +src/tests/fixtures/inheritance/interfaceInheritsFieldFromParents.ts:4:3 - error: Interface field A.aField expected but C does not provide it. -interface C implements A & B { - aField: String @metadata - bField: String @metadata -} --- TypeScript -- -import { GraphQLSchema, GraphQLInterfaceType, GraphQLString } from "graphql"; -export function getSchema(): GraphQLSchema { - const AType: GraphQLInterfaceType = new GraphQLInterfaceType({ - name: "A", - fields() { - return { - aField: { - name: "aField", - type: GraphQLString - } - }; - } - }); - const BType: GraphQLInterfaceType = new GraphQLInterfaceType({ - name: "B", - fields() { - return { - bField: { - name: "bField", - type: GraphQLString - } - }; - } - }); - const CType: GraphQLInterfaceType = new GraphQLInterfaceType({ - name: "C", - fields() { - return { - aField: { - name: "aField", - type: GraphQLString - }, - bField: { - name: "bField", - type: GraphQLString - } - }; - }, - interfaces() { - return [AType, BType]; - } - }); - return new GraphQLSchema({ - types: [AType, BType, CType] - }); -} +4 aField: string; + ~~~~~~~~~~~~~~~ + + src/tests/fixtures/inheritance/interfaceInheritsFieldFromParents.ts:14:1 + 14 interface C extends A, B { + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + 15 aField: string; + ~~~~~~~~~~~~~~~~~ + 16 bField: string; + ~~~~~~~~~~~~~~~~~ + 17 } + ~ + Related location +src/tests/fixtures/inheritance/interfaceInheritsFieldFromParents.ts:10:3 - error: Interface field B.bField expected but C does not provide it. + +10 bField: string; + ~~~~~~~~~~~~~~~ + + src/tests/fixtures/inheritance/interfaceInheritsFieldFromParents.ts:14:1 + 14 interface C extends A, B { + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + 15 aField: string; + ~~~~~~~~~~~~~~~~~ + 16 bField: string; + ~~~~~~~~~~~~~~~~~ + 17 } + ~ + Related location diff --git a/src/tests/fixtures/interfaces/ClassInterfaceImplementsInterface.ts.expected b/src/tests/fixtures/interfaces/ClassInterfaceImplementsInterface.ts.expected index 08fd90e1..1420820c 100644 --- a/src/tests/fixtures/interfaces/ClassInterfaceImplementsInterface.ts.expected +++ b/src/tests/fixtures/interfaces/ClassInterfaceImplementsInterface.ts.expected @@ -23,71 +23,13 @@ class Actor implements GqlNode, Person { ----------------- OUTPUT ----------------- --- SDL -- -interface Actor implements Node & Person { - id: String @metadata - name: String @metadata -} - -interface Node { - id: String @metadata -} +src/tests/fixtures/interfaces/ClassInterfaceImplementsInterface.ts:15:1 - error: Expected `@gqlInterface` class to be abstract. `@gqlInterface` can only be used on interface or abstract class declarations. e.g. `interface MyInterface {}` or `abstract class MyInterface {}` -interface Person implements Node { - id: String @metadata - name: String @metadata -} --- TypeScript -- -import { GraphQLSchema, GraphQLInterfaceType, GraphQLString } from "graphql"; -export function getSchema(): GraphQLSchema { - const NodeType: GraphQLInterfaceType = new GraphQLInterfaceType({ - name: "Node", - fields() { - return { - id: { - name: "id", - type: GraphQLString - } - }; - } - }); - const PersonType: GraphQLInterfaceType = new GraphQLInterfaceType({ - name: "Person", - fields() { - return { - id: { - name: "id", - type: GraphQLString - }, - name: { - name: "name", - type: GraphQLString - } - }; - }, - interfaces() { - return [NodeType]; - } - }); - const ActorType: GraphQLInterfaceType = new GraphQLInterfaceType({ - name: "Actor", - fields() { - return { - id: { - name: "id", - type: GraphQLString - }, - name: { - name: "name", - type: GraphQLString - } - }; - }, - interfaces() { - return [NodeType, PersonType]; - } - }); - return new GraphQLSchema({ - types: [ActorType, NodeType, PersonType] - }); -} +15 class Actor implements GqlNode, Person { + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +16 id: string; + ~~~~~~~~~~~~~ +17 name: string; + ~~~~~~~~~~~~~~~ +18 } + ~ diff --git a/src/tests/fixtures/interfaces/classInterfaceNamedQuery.invalid.ts.expected b/src/tests/fixtures/interfaces/classInterfaceNamedQuery.invalid.ts.expected index 901607ab..224231f2 100644 --- a/src/tests/fixtures/interfaces/classInterfaceNamedQuery.invalid.ts.expected +++ b/src/tests/fixtures/interfaces/classInterfaceNamedQuery.invalid.ts.expected @@ -10,7 +10,7 @@ class Query { ----------------- OUTPUT ----------------- -src/tests/fixtures/interfaces/classInterfaceNamedQuery.invalid.ts:2:1 - error: Query root type must be Object type, it cannot be Query. +src/tests/fixtures/interfaces/classInterfaceNamedQuery.invalid.ts:2:1 - error: Expected `@gqlInterface` class to be abstract. `@gqlInterface` can only be used on interface or abstract class declarations. e.g. `interface MyInterface {}` or `abstract class MyInterface {}` 2 class Query { ~~~~~~~~~~~~~ diff --git a/src/transforms/propagateHeritage.ts b/src/transforms/propagateHeritage.ts index d7e05c26..b1b4ba85 100644 --- a/src/transforms/propagateHeritage.ts +++ b/src/transforms/propagateHeritage.ts @@ -75,7 +75,7 @@ class HeritagePropagator { declaration: ts.ClassDeclaration | ts.InterfaceDeclaration, ): InterfaceTypeDefinitionNode | ObjectTypeDefinitionNode { const name = nullThrows(declaration.name); - const parentTypes = this.ctx.getAllParentsForName(name); + const parentTypes = this.ctx.getAllParentClassesForName(name); // Build up fields const fieldsMap = new Map();