fix(javascript): recover GraphQL selection prefixes and validate names (#10065)

fix(javascript): align GraphQL operation grammar
This commit is contained in:
morluto authored and GitHub committed 2026-11-11 18:16:50 +08:00
1 parent f7d564f436
commit d1895841a4
4 files changed
+170 -10

No files matched your search

@@ -110,6 +110,30 @@ const graphqlNameAt = (
: { value: name, end: index + name.length };
};
/** GraphQL selections may start with a field, named spread, or inline fragment. */
const graphqlSelectionAt = (text: string, start: number): boolean => {
let index = skipGraphqlIgnored(text, start);
for (;;) {
if (graphqlNameAt(text, index) !== undefined) return true;
if (!text.startsWith("...", index)) return false;
index = skipGraphqlIgnored(text, index + 3);
const fragment = graphqlNameAt(text, index);
if (fragment !== undefined)
return (
fragment.value !== "on" ||
graphqlNameAt(text, skipGraphqlIgnored(text, fragment.end)) !==
undefined
);
if (text.charAt(index) === "@")
return (
graphqlNameAt(text, skipGraphqlIgnored(text, index + 1)) !== undefined
);
if (text.charAt(index) !== "{") return false;
// Untyped inline fragments can nest; scan their prefix without recursion.
index = skipGraphqlIgnored(text, index + 1);
}
};
/** Recover a named operation from GraphQL document text. */
const graphqlOperationFromText = (
text: string,
@@ -122,12 +146,13 @@ const graphqlOperationFromText = (
const index = skipGraphqlIgnored(text, name.end);
const next = text.charAt(index);
// Keep the existing bounded recognition scope: a named operation followed
// by variable definitions, a directive, or a selection set with a field.
// by variable definitions, a directive, or a recognized selection prefix.
const hasHeader =
next === "("
? text.charAt(skipGraphqlIgnored(text, index + 1)) === "$"
: (next === "@" || next === "{") &&
graphqlNameAt(text, skipGraphqlIgnored(text, index + 1)) !== undefined;
: next === "@"
? graphqlNameAt(text, skipGraphqlIgnored(text, index + 1)) !== undefined
: next === "{" && graphqlSelectionAt(text, index + 1);
return hasHeader
? { value: name.value, mechanism: `graphql:${type.value}` }
: undefined;
@@ -149,9 +174,10 @@ const graphqlOperationFromDocumentNode = (
)
return undefined;
const value = semanticFinalObjectStringProperty(name, "value");
return value === undefined
? undefined
: { value, mechanism: `graphql:${type}` };
if (value === undefined) return undefined;
return graphqlNameAt(value, 0)?.end === value.length
? { value, mechanism: `graphql:${type}` }
: undefined;
};
interface GraphqlSourceDependency {
@@ -415,7 +415,10 @@ describe("GraphQL operation recognition", () => {
"query GetUser # prose\nparameters",
"query Get# split name\nUser { user }",
"query GetUser @ # missing directive name",
"query GetUser { # fragment\n...UserFields }",
"query GetUser { # missing fragment name\n... }",
"query GetUser { ... on }",
"query GetUser { ... @ }",
"query GetUser { ....UserFields }",
"query\u00a0GetUser { user }",
"query\vGetUser { user }",
])("keeps incomplete or unsupported header text unknown: %s", (text) => {