From 748b1cd67ccc07c26663a0c510a1780a0119798d Mon Sep 17 00:00:00 2001 From: Rinto Jose Date: Sat, 6 Jul 2024 12:47:56 +0530 Subject: [PATCH 1/4] refactor: definion providers --- ...ema.test.ts => definition-provider-for-schema.test.ts} | 8 ++++---- ...r-from-schema.ts => definition-provider-for-schema.ts} | 2 +- ...from-source.tsx => definition-provider-for-source.tsx} | 2 +- src/definition-provider/index.ts | 8 ++++---- ...hema.test.ts => reference-provider-for-schema.test.ts} | 4 ++-- ...er-from-schema.ts => reference-provider-for-schema.ts} | 2 +- ...vider-from-schema.ts => symbol-provider-for-schema.ts} | 2 +- 7 files changed, 14 insertions(+), 14 deletions(-) rename src/definition-provider/{definition-provider-from-schema.test.ts => definition-provider-for-schema.test.ts} (84%) rename src/definition-provider/{definition-provider-from-schema.ts => definition-provider-for-schema.ts} (95%) rename src/definition-provider/{definition-provider-from-source.tsx => definition-provider-for-source.tsx} (97%) rename src/definition-provider/{reference-provider-from-schema.test.ts => reference-provider-for-schema.test.ts} (89%) rename src/definition-provider/{reference-provider-from-schema.ts => reference-provider-for-schema.ts} (95%) rename src/definition-provider/{symbol-provider-from-schema.ts => symbol-provider-for-schema.ts} (98%) diff --git a/src/definition-provider/definition-provider-from-schema.test.ts b/src/definition-provider/definition-provider-for-schema.test.ts similarity index 84% rename from src/definition-provider/definition-provider-from-schema.test.ts rename to src/definition-provider/definition-provider-for-schema.test.ts index 5a6a67f..12cecdf 100644 --- a/src/definition-provider/definition-provider-from-schema.test.ts +++ b/src/definition-provider/definition-provider-for-schema.test.ts @@ -1,6 +1,6 @@ import { Position, Range } from '../diff' import { trimSpaces } from '../util/trim-spaces' -import { provideDefinitionFromSchema } from './definition-provider-from-schema' +import { provideDefinitionForSchema } from './definition-provider-for-schema' const schema = ` type User { @@ -60,7 +60,7 @@ function getAt(schema: string, range: Range | null) { describe('provideDefinitionFromSchema', () => { test('should provide return address type', async () => { - const range = provideDefinitionFromSchema(schema, new Position(4, 16)) + const range = provideDefinitionForSchema(schema, new Position(4, 16)) const output = getAt(schema, range) expect(output).toEqual( trimSpaces(` @@ -73,7 +73,7 @@ describe('provideDefinitionFromSchema', () => { }) test('should provide user status', async () => { - const range = provideDefinitionFromSchema(schema, new Position(5, 16)) + const range = provideDefinitionForSchema(schema, new Position(5, 16)) const output = getAt(schema, range) expect(output).toEqual( trimSpaces(` @@ -85,7 +85,7 @@ describe('provideDefinitionFromSchema', () => { }) test('should provide tweet type', async () => { - const range = provideDefinitionFromSchema(schema, new Position(36, 22)) + const range = provideDefinitionForSchema(schema, new Position(36, 22)) const output = getAt(schema, range) expect(output).toEqual( trimSpaces(` diff --git a/src/definition-provider/definition-provider-from-schema.ts b/src/definition-provider/definition-provider-for-schema.ts similarity index 95% rename from src/definition-provider/definition-provider-from-schema.ts rename to src/definition-provider/definition-provider-for-schema.ts index f68f232..9eac683 100644 --- a/src/definition-provider/definition-provider-from-schema.ts +++ b/src/definition-provider/definition-provider-for-schema.ts @@ -8,7 +8,7 @@ function isInRange(node: gql.ASTNode, position: Position, offset?: Position) { return isPositionWithInRange(position, nodeRange, true) } -export function provideDefinitionFromSchema(source: string, position: Position) { +export function provideDefinitionForSchema(source: string, position: Position) { try { const fixed = makeQueryParsable(source) const document = gql.parse(fixed) diff --git a/src/definition-provider/definition-provider-from-source.tsx b/src/definition-provider/definition-provider-for-source.tsx similarity index 97% rename from src/definition-provider/definition-provider-from-source.tsx rename to src/definition-provider/definition-provider-for-source.tsx index 8d58aea..2add3aa 100644 --- a/src/definition-provider/definition-provider-from-source.tsx +++ b/src/definition-provider/definition-provider-for-source.tsx @@ -10,7 +10,7 @@ function isInRange(node: gql.ASTNode, position: Position, offset?: Position) { return isPositionWithInRange(position, nodeRange, true) } -export function provideDefinitionFromSource( +export function provideDefinitionForSource( sourceFile: ts.SourceFile, position: Position, schema: gql.GraphQLSchema, diff --git a/src/definition-provider/index.ts b/src/definition-provider/index.ts index ef85ce3..e6b9021 100644 --- a/src/definition-provider/index.ts +++ b/src/definition-provider/index.ts @@ -1,4 +1,4 @@ -export * from './definition-provider-from-schema' -export * from './definition-provider-from-source' -export * from './reference-provider-from-schema' -export * from './symbol-provider-from-schema' +export * from './definition-provider-for-schema' +export * from './definition-provider-for-source' +export * from './reference-provider-for-schema' +export * from './symbol-provider-for-schema' diff --git a/src/definition-provider/reference-provider-from-schema.test.ts b/src/definition-provider/reference-provider-for-schema.test.ts similarity index 89% rename from src/definition-provider/reference-provider-from-schema.test.ts rename to src/definition-provider/reference-provider-for-schema.test.ts index e94c36c..d5260c9 100644 --- a/src/definition-provider/reference-provider-from-schema.test.ts +++ b/src/definition-provider/reference-provider-for-schema.test.ts @@ -1,6 +1,6 @@ import { Position, Range } from '../diff' import { trimSpaces } from '../util/trim-spaces' -import { provideReferenceFromSchema } from './reference-provider-from-schema' +import { provideReferenceForSchema } from './reference-provider-for-schema' const schema = ` type User { @@ -64,7 +64,7 @@ function getAt(schema: string, range: Range | null) { describe('provideDefinitionFromSchema', () => { test('should return all lines that has User in it', async () => { - const ranges = provideReferenceFromSchema(schema, new Position(1, 5)) + const ranges = provideReferenceForSchema(schema, new Position(1, 5)) const output = ranges?.map(range => getAt(schema, range)).join('\n\n') expect(trimSpaces(output)).toEqual( trimSpaces(` diff --git a/src/definition-provider/reference-provider-from-schema.ts b/src/definition-provider/reference-provider-for-schema.ts similarity index 95% rename from src/definition-provider/reference-provider-from-schema.ts rename to src/definition-provider/reference-provider-for-schema.ts index 4a070ae..39875b7 100644 --- a/src/definition-provider/reference-provider-from-schema.ts +++ b/src/definition-provider/reference-provider-for-schema.ts @@ -8,7 +8,7 @@ function isInRange(node: gql.ASTNode, position: Position, offset?: Position) { return isPositionWithInRange(position, nodeRange, true) } -export function provideReferenceFromSchema(source: string, position: Position) { +export function provideReferenceForSchema(source: string, position: Position) { try { const fixed = makeQueryParsable(source) const document = gql.parse(fixed) diff --git a/src/definition-provider/symbol-provider-from-schema.ts b/src/definition-provider/symbol-provider-for-schema.ts similarity index 98% rename from src/definition-provider/symbol-provider-from-schema.ts rename to src/definition-provider/symbol-provider-for-schema.ts index 205b2c1..da93e6a 100644 --- a/src/definition-provider/symbol-provider-from-schema.ts +++ b/src/definition-provider/symbol-provider-for-schema.ts @@ -85,7 +85,7 @@ function parseChildren(node: gql.TypeDefinitionNode) { return symbols } -export function provideSymbolsFromSchema(source: string) { +export function provideSymbolsForSchema(source: string) { try { const document = gql.parse(source) const symbols: SymbolInformation[] = [] From 3f36f038783de50044eaad6c6ee977d6e9b97511 Mon Sep 17 00:00:00 2001 From: Rinto Jose Date: Sat, 6 Jul 2024 12:56:47 +0530 Subject: [PATCH 2/4] update reference provider to work with names instead of definitions --- .../reference-provider-for-schema.ts | 21 +++---------------- 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/src/definition-provider/reference-provider-for-schema.ts b/src/definition-provider/reference-provider-for-schema.ts index 39875b7..2aa5c3b 100644 --- a/src/definition-provider/reference-provider-for-schema.ts +++ b/src/definition-provider/reference-provider-for-schema.ts @@ -13,27 +13,12 @@ export function provideReferenceForSchema(source: string, position: Position) { const fixed = makeQueryParsable(source) const document = gql.parse(fixed) let selectedName: string | undefined - const processNode = (node: gql.TypeDefinitionNode | gql.NamedTypeNode) => { + const processNode = (node: gql.NameNode | gql.NamedTypeNode) => { if (!isInRange(node, position)) return - selectedName = node.name.value + selectedName = node.kind === gql.Kind.NAME ? node.value : node.name.value } gql.visit(document, { - EnumTypeDefinition(node) { - return processNode(node) - }, - ScalarTypeDefinition(node) { - return processNode(node) - }, - ObjectTypeDefinition(node) { - return processNode(node) - }, - InputObjectTypeDefinition(node) { - return processNode(node) - }, - UnionTypeDefinition(node) { - return processNode(node) - }, - InterfaceTypeDefinition(node) { + Name(node) { return processNode(node) }, NamedType(node) { From ffdc2dfa6986b9566aa4c0a52a1564f9344f7398 Mon Sep 17 00:00:00 2001 From: Rinto Jose Date: Sat, 6 Jul 2024 13:53:26 +0530 Subject: [PATCH 3/4] feat: refactor parse graphql document from ts as a util --- src/auto-complete/hook-auto-complete.ts | 24 ++++------ src/ts/index.ts | 1 + src/ts/parse-graphql-document-from-ts.ts | 58 ++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 16 deletions(-) create mode 100644 src/ts/parse-graphql-document-from-ts.ts diff --git a/src/auto-complete/hook-auto-complete.ts b/src/auto-complete/hook-auto-complete.ts index 55878f7..4f625bc 100644 --- a/src/auto-complete/hook-auto-complete.ts +++ b/src/auto-complete/hook-auto-complete.ts @@ -3,10 +3,10 @@ import { toNonNullArray } from 'tsds-tools' import ts from 'typescript' import { GQLAssistConfig } from '../config' import { Position } from '../diff' -import { isFieldNode, makeQueryParsable } from '../gql' +import { isFieldNode } from '../gql' import { getGQLNodeRange } from '../gql/get-gql-node-location-range' import { isPositionWithInRange } from '../position' -import { getGQLContent, getGraphQLQueryVariable, getTSNodeLocationRange } from '../ts' +import { parseGraphQLDocumentFromTS } from '../ts' export const DEFAULT_SIPPET = '{\n ${1}\n}' @@ -60,21 +60,13 @@ export function autoCompleteHook( schema: gql.GraphQLSchema, config: GQLAssistConfig, ): FieldInformation[] { - const variable = getGraphQLQueryVariable(sourceFile) - if (!variable) return [] - - const range = getTSNodeLocationRange(variable, sourceFile) - if (!isPositionWithInRange(position, range)) return [] - - const query = getGQLContent(variable) - if (!query || query?.trim() === '') return topLevelInfo(schema) - if (isEmptyQuery(query)) return topLevelInfo(schema) - - const offset = new Position(range.start.line, 0) - try { - const fixed = makeQueryParsable(query) - const document = gql.parse(fixed) + const { variable, document, source, offset } = parseGraphQLDocumentFromTS(sourceFile, { + position, + }) + if (!variable) return [] + if (!source || isEmptyQuery(source) || !document) return topLevelInfo(schema) + let schemaType: gql.GraphQLObjectType | null | undefined let existingFields: string[] = [] const typeInfo = new gql.TypeInfo(schema) diff --git a/src/ts/index.ts b/src/ts/index.ts index 2d56251..c4a1b07 100644 --- a/src/ts/index.ts +++ b/src/ts/index.ts @@ -35,6 +35,7 @@ export * from './is-nullable' export * from './is-nullable-from-decorator' export * from './is-private' export * from './organize-imports' +export * from './parse-graphql-document-from-ts' export * from './parse-ts' export * from './prettify' export * from './print-ts' diff --git a/src/ts/parse-graphql-document-from-ts.ts b/src/ts/parse-graphql-document-from-ts.ts new file mode 100644 index 0000000..4c9b227 --- /dev/null +++ b/src/ts/parse-graphql-document-from-ts.ts @@ -0,0 +1,58 @@ +import * as gql from 'graphql' +import ts from 'typescript' +import { makeQueryParsable } from '../gql/make-query-parsable' +import { isPositionWithInRange, Position } from '../position' +import { getGQLContent } from './get-gql-query-content' +import { getGraphQLQueryVariable } from './get-gql-query-variable' +import { getTSNodeLocationRange } from './get-ts-node-location-range' + +export interface ParseOptions { + position?: Position | undefined + parse?: boolean + fix?: boolean +} + +export interface GraphQLContext { + sourceFile: ts.SourceFile + variable?: ts.VariableDeclaration + source?: string + offset?: Position + document?: gql.DocumentNode +} + +export function parseGraphQLDocumentFromTS( + sourceFile: ts.SourceFile, + options?: ParseOptions, +): GraphQLContext { + const variable = getGraphQLQueryVariable(sourceFile) + if (!variable) return { sourceFile } + + const range = getTSNodeLocationRange(variable, sourceFile) + if (options?.position && !isPositionWithInRange(options?.position, range)) { + return { sourceFile } + } + const offset = new Position(range.start.line, 0) + + const source = getGQLContent(variable) + if (!source) return { sourceFile, variable, offset } + if (source.trim() === '') + return { + sourceFile, + variable, + source, + offset, + } + + const document = + options?.parse !== false + ? gql.parse(options?.fix !== false ? makeQueryParsable(source) : source) + : undefined + + return { + sourceFile, + variable, + source, + offset, + document, + } +} From e8f2abea0b603b7a6f097d216c4c29ba4f4fe6b5 Mon Sep 17 00:00:00 2001 From: Rinto Jose Date: Sat, 6 Jul 2024 15:05:37 +0530 Subject: [PATCH 4/4] fix: extract references from .gql.ts files --- .../reference-provider-for-schema.ts | 121 +++++++++++++++--- src/diff/token.ts | 12 ++ src/ts/parse-graphql-document-from-ts.ts | 4 +- 3 files changed, 120 insertions(+), 17 deletions(-) diff --git a/src/definition-provider/reference-provider-for-schema.ts b/src/definition-provider/reference-provider-for-schema.ts index 2aa5c3b..c7c7233 100644 --- a/src/definition-provider/reference-provider-for-schema.ts +++ b/src/definition-provider/reference-provider-for-schema.ts @@ -1,39 +1,130 @@ +import { globStream } from 'fast-glob' import * as gql from 'graphql' -import { Position, Range } from '../diff' +import { Location, Position } from '../diff' import { getGQLNodeRange, getGQLNodeRangeWithoutDescription, makeQueryParsable } from '../gql' import { isPositionWithInRange } from '../position/is-position-within-range' +import { parseGraphQLDocumentFromTS, readTSFile } from '../ts' + +interface SelectedField { + parent: string + name: string +} function isInRange(node: gql.ASTNode, position: Position, offset?: Position) { const nodeRange = getGQLNodeRange(node, offset) return isPositionWithInRange(position, nodeRange, true) } -export function provideReferenceForSchema(source: string, position: Position) { +function referencesByType( + document: gql.DocumentNode, + sourcePath: string, + type: string | undefined, +) { + if (!type) return [] + const locations: Location[] = [] + gql.visit(document, { + NamedType(node) { + if (node.name.value !== type) return + locations.push(new Location(sourcePath, getGQLNodeRangeWithoutDescription(node))) + }, + }) + return locations +} + +function processFile(file: string, field: SelectedField, schema: gql.GraphQLSchema) { + const locations: Location[] = [] + try { + const sourceFile = readTSFile(file) + const { document, offset } = parseGraphQLDocumentFromTS(sourceFile) + if (!document) return locations + const typeInfo = new gql.TypeInfo(schema) + gql.visit( + document, + gql.visitWithTypeInfo(typeInfo, { + Field(node) { + if (node.name.value !== field.name) return + const parent = typeInfo.getParentType() + if (!parent || parent.name !== field.parent) return + locations.push(new Location(file, getGQLNodeRange(node.name, offset))) + }, + }), + ) + } catch (e) { + console.log(`Failed to process file ${file}`, e.message) + } + return locations +} + +async function referencesByField( + schema: gql.GraphQLSchema, + field: SelectedField | undefined, + pattern: string, +) { + if (!field || !pattern) return [] + const stream = globStream(pattern) + let locations: Location[] = [] + for await (const file of stream) { + locations = locations.concat(processFile(file as string, field, schema)) + } + return locations +} + +export async function provideReferenceForSchema( + source: string, + sourcePath: string, + position: Position, + pattern: string, +) { try { const fixed = makeQueryParsable(source) const document = gql.parse(fixed) - let selectedName: string | undefined - const processNode = (node: gql.NameNode | gql.NamedTypeNode) => { + let type: string | undefined + let selectedField: SelectedField | undefined + const processNode = (node: gql.NameNode) => { if (!isInRange(node, position)) return - selectedName = node.kind === gql.Kind.NAME ? node.value : node.name.value + type = node.value + return gql.BREAK + } + const processField = (node: gql.TypeDefinitionNode) => { + switch (node.kind) { + case gql.Kind.OBJECT_TYPE_DEFINITION: + case gql.Kind.INPUT_OBJECT_TYPE_DEFINITION: + case gql.Kind.INTERFACE_TYPE_DEFINITION: + for (const field of node.fields ?? []) { + if (isInRange(field.name, position)) { + selectedField = { parent: node.name.value, name: field.name.value } + return gql.BREAK + } + } + } } gql.visit(document, { Name(node) { return processNode(node) }, - NamedType(node) { - return processNode(node) + EnumTypeDefinition(node) { + for (const value of node.values ?? []) { + if (isInRange(value.name, position)) { + selectedField = { parent: node.name.value, name: value.name.value } + return + } + } }, - }) - if (!selectedName) return [] - const ranges: Range[] = [] - gql.visit(document, { - NamedType(node) { - if (node.name.value !== selectedName) return - ranges.push(getGQLNodeRangeWithoutDescription(node)) + ObjectTypeDefinition(node) { + return processField(node) + }, + InterfaceTypeDefinition(node) { + return processField(node) + }, + InputObjectTypeDefinition(node) { + return processField(node) }, }) - return ranges + if (type) { + return referencesByType(document, sourcePath, type) + } + const schema = gql.buildSchema(source) + return await referencesByField(schema, selectedField, pattern) } catch (e) { console.error(e) return [] diff --git a/src/diff/token.ts b/src/diff/token.ts index 60f44b7..cd723ad 100644 --- a/src/diff/token.ts +++ b/src/diff/token.ts @@ -22,6 +22,7 @@ export class Range { constructor( public start: Position, public end: Position, + public path?: string, ) {} setStart(start: Position) { @@ -39,6 +40,17 @@ export class Range { } } +export class Location { + constructor( + public path: string, + public range: Range, + ) {} + + clone() { + return new Location(this.path, this.range.clone()) + } +} + export class Token { constructor( public index: number, diff --git a/src/ts/parse-graphql-document-from-ts.ts b/src/ts/parse-graphql-document-from-ts.ts index 4c9b227..7460f78 100644 --- a/src/ts/parse-graphql-document-from-ts.ts +++ b/src/ts/parse-graphql-document-from-ts.ts @@ -12,7 +12,7 @@ export interface ParseOptions { fix?: boolean } -export interface GraphQLContext { +export interface GraphQLDocumentContext { sourceFile: ts.SourceFile variable?: ts.VariableDeclaration source?: string @@ -23,7 +23,7 @@ export interface GraphQLContext { export function parseGraphQLDocumentFromTS( sourceFile: ts.SourceFile, options?: ParseOptions, -): GraphQLContext { +): GraphQLDocumentContext { const variable = getGraphQLQueryVariable(sourceFile) if (!variable) return { sourceFile }