From 95640fafd0362c4c12e4a8d65af29edd67aae222 Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Fri, 17 Apr 2026 17:09:33 -0700 Subject: [PATCH] Fix crash on invalid directive locations in playground The playground crashed with an unhandled GraphQLError when using invalid directive locations like `DIRECTIVE_DEFINITION`. This happened because `instanceof GraphQLError` failed in the bundled environment due to duplicate module instances. Fix by replacing `parser.parseDirectiveLocation()` with `parseName()` and validating against the DirectiveLocation enum after parsing. Also use `err.name === "GraphQLError"` instead of `instanceof` for robustness in bundled environments. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Errors.ts | 5 ++++ src/Extractor.ts | 18 ++++++++++--- ...tiveLocationInvalid.invalid.ts.expected.md | 2 +- ...veOnDirectiveDefinitionLocation.invalid.ts | 8 ++++++ ...eDefinitionLocation.invalid.ts.expected.md | 27 +++++++++++++++++++ 5 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 src/tests/fixtures/directives/directiveOnDirectiveDefinitionLocation.invalid.ts create mode 100644 src/tests/fixtures/directives/directiveOnDirectiveDefinitionLocation.invalid.ts.expected.md diff --git a/src/Errors.ts b/src/Errors.ts index 29ef015d..8055da10 100644 --- a/src/Errors.ts +++ b/src/Errors.ts @@ -14,6 +14,7 @@ import { INFO_TAG, DIRECTIVE_TAG, } from "./Extractor.js"; +import { DirectiveLocation } from "graphql"; export const ISSUE_URL = "https://github.com/captbaritone/grats/issues"; @@ -654,6 +655,10 @@ export function directiveTagNoComment() { return "Expected `@gqlDirective` tag to specify at least one location."; } +export function invalidDirectiveLocation(name: string) { + return `"${name}" is not a valid directive location. Valid locations are: ${Object.values(DirectiveLocation).join(", ")}.`; +} + export function directiveFunctionNotNamed() { return "Expected `@gqlDirective` function to be named."; } diff --git a/src/Extractor.ts b/src/Extractor.ts index 0c2d9d3d..d49e4063 100644 --- a/src/Extractor.ts +++ b/src/Extractor.ts @@ -17,7 +17,7 @@ import { DefinitionNode, version as graphqlJSVersion, TokenKind, - GraphQLError, + DirectiveLocation, } from "graphql"; import { gte as semverGte } from "semver"; import { @@ -536,13 +536,21 @@ class Extractor { } const locations = parser - .delimitedMany(TokenKind.PIPE, () => parser.parseDirectiveLocation()) + .delimitedMany(TokenKind.PIPE, () => parser.parseName()) .map((location) => ({ ...location, loc: loc(tag) })); return { name, repeatable, locations }; }); if (tagData == null) return; + const validLocations = Object.values(DirectiveLocation) as string[]; + for (const location of tagData.locations) { + if (!validLocations.includes(location.value)) { + this.report(tag, E.invalidDirectiveLocation(location.value)); + return; + } + } + let name = tagData.name; // If there wasn't a name in the directive tag, we expect the function @@ -909,7 +917,11 @@ class Extractor { parser.expectToken(TokenKind.EOF); return result; } catch (err) { - if (err instanceof GraphQLError) { + if (err instanceof Error && err.name === "GraphQLError") { + // Note: We use a name check instead of `instanceof GraphQLError` + // because in bundled environments (e.g. the playground), the + // GraphQLError class from the parser may be a different instance + // than the one we imported, causing `instanceof` to fail. this.report(node, err.message); } else { throw err; diff --git a/src/tests/fixtures/directives/defineCustomDirectiveLocationInvalid.invalid.ts.expected.md b/src/tests/fixtures/directives/defineCustomDirectiveLocationInvalid.invalid.ts.expected.md index 155ba82e..3f491d05 100644 --- a/src/tests/fixtures/directives/defineCustomDirectiveLocationInvalid.invalid.ts.expected.md +++ b/src/tests/fixtures/directives/defineCustomDirectiveLocationInvalid.invalid.ts.expected.md @@ -15,7 +15,7 @@ export function customDirective() {} ### Error Report ```text -src/tests/fixtures/directives/defineCustomDirectiveLocationInvalid.invalid.ts:3:4 - error: Syntax Error: Unexpected Name "WHOOPS". +src/tests/fixtures/directives/defineCustomDirectiveLocationInvalid.invalid.ts:3:4 - error: "WHOOPS" is not a valid directive location. Valid locations are: QUERY, MUTATION, SUBSCRIPTION, FIELD, FRAGMENT_DEFINITION, FRAGMENT_SPREAD, INLINE_FRAGMENT, VARIABLE_DEFINITION, SCHEMA, SCALAR, OBJECT, FIELD_DEFINITION, ARGUMENT_DEFINITION, INTERFACE, UNION, ENUM, ENUM_VALUE, INPUT_OBJECT, INPUT_FIELD_DEFINITION. 3 * @gqlDirective on WHOOPS ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/tests/fixtures/directives/directiveOnDirectiveDefinitionLocation.invalid.ts b/src/tests/fixtures/directives/directiveOnDirectiveDefinitionLocation.invalid.ts new file mode 100644 index 00000000..2498da10 --- /dev/null +++ b/src/tests/fixtures/directives/directiveOnDirectiveDefinitionLocation.invalid.ts @@ -0,0 +1,8 @@ +import { Int } from "../../../Types"; +/** + * @gqlDirective on DIRECTIVE_DEFINITION + * @gqlAnnotate + */ +export function myDirective(args: { credits: Int }) { + // ... +} diff --git a/src/tests/fixtures/directives/directiveOnDirectiveDefinitionLocation.invalid.ts.expected.md b/src/tests/fixtures/directives/directiveOnDirectiveDefinitionLocation.invalid.ts.expected.md new file mode 100644 index 00000000..f234d2fd --- /dev/null +++ b/src/tests/fixtures/directives/directiveOnDirectiveDefinitionLocation.invalid.ts.expected.md @@ -0,0 +1,27 @@ +# directives/directiveOnDirectiveDefinitionLocation.invalid.ts + +## Input + +```ts title="directives/directiveOnDirectiveDefinitionLocation.invalid.ts" +import { Int } from "../../../Types"; +/** + * @gqlDirective on DIRECTIVE_DEFINITION + * @gqlAnnotate + */ +export function myDirective(args: { credits: Int }) { + // ... +} +``` + +## Output + +### Error Report + +```text +src/tests/fixtures/directives/directiveOnDirectiveDefinitionLocation.invalid.ts:3:4 - error: "DIRECTIVE_DEFINITION" is not a valid directive location. Valid locations are: QUERY, MUTATION, SUBSCRIPTION, FIELD, FRAGMENT_DEFINITION, FRAGMENT_SPREAD, INLINE_FRAGMENT, VARIABLE_DEFINITION, SCHEMA, SCALAR, OBJECT, FIELD_DEFINITION, ARGUMENT_DEFINITION, INTERFACE, UNION, ENUM, ENUM_VALUE, INPUT_OBJECT, INPUT_FIELD_DEFINITION. + +3 * @gqlDirective on DIRECTIVE_DEFINITION + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +4 * @gqlAnnotate + ~~~ +``` \ No newline at end of file