Mirror: TypeScript LSP plugin that finds GraphQL documents in your code and provides diagnostics, auto-complete and hover-information.

patch: Increase support of harder to detect `gql.tada` API usage patterns (#309)

+5
.changeset/young-phones-search.md
···
+
---
+
'@0no-co/graphqlsp': minor
+
---
+
+
Expand support for `gql.tada` API. GraphQLSP will now recognize `graphql()`/`graphql.persisted()` calls regardless of variable naming and support more obscure usage patterns.
+116
packages/graphqlsp/src/ast/checks.ts
···
+
import { ts } from '../ts';
+
import { templates } from './templates';
+
+
/** Checks for an immediately-invoked function expression */
+
export const isIIFE = (node: ts.Node): boolean =>
+
ts.isCallExpression(node) &&
+
node.arguments.length === 0 &&
+
(ts.isFunctionExpression(node.expression) ||
+
ts.isArrowFunction(node.expression)) &&
+
!node.expression.asteriskToken &&
+
!node.expression.modifiers?.length;
+
+
/** Checks if node is a known identifier of graphql functions ('graphql' or 'gql') */
+
export const isGraphQLFunctionIdentifier = (
+
node: ts.Node
+
): node is ts.Identifier =>
+
ts.isIdentifier(node) && templates.has(node.escapedText as string);
+
+
/** If `checker` is passed, checks if node (as identifier/expression) is a gql.tada graphql() function */
+
export const isTadaGraphQLFunction = (
+
node: ts.Node,
+
checker: ts.TypeChecker | undefined
+
): node is ts.LeftHandSideExpression => {
+
if (!ts.isLeftHandSideExpression(node)) return false;
+
const type = checker?.getTypeAtLocation(node);
+
// Any function that has both a `scalar` and `persisted` property
+
// is automatically considered a gql.tada graphql() function.
+
return (
+
type != null &&
+
type.getProperty('scalar') != null &&
+
type.getProperty('persisted') != null
+
);
+
};
+
+
/** If `checker` is passed, checks if node is a gql.tada graphql() call */
+
export const isTadaGraphQLCall = (
+
node: ts.CallExpression,
+
checker: ts.TypeChecker | undefined
+
): boolean => {
+
// We expect graphql() to be called with either a string literal
+
// or a string literal and an array of fragments
+
if (!ts.isCallExpression(node)) {
+
return false;
+
} else if (node.arguments.length < 1 || node.arguments.length > 2) {
+
return false;
+
} else if (!ts.isStringLiteralLike(node.arguments[0])) {
+
return false;
+
}
+
return checker ? isTadaGraphQLFunction(node.expression, checker) : false;
+
};
+
+
/** Checks if node is a gql.tada graphql.persisted() call */
+
export const isTadaPersistedCall = (
+
node: ts.Node,
+
checker: ts.TypeChecker | undefined
+
): node is ts.CallExpression => {
+
if (!ts.isCallExpression(node)) {
+
return false;
+
} else if (!ts.isPropertyAccessExpression(node.expression)) {
+
return false; // rejecting non property access calls: <expression>.<name>()
+
} else if (
+
!ts.isIdentifier(node.expression.name) ||
+
node.expression.name.escapedText !== 'persisted'
+
) {
+
return false; // rejecting calls on anyting but 'persisted': <expression>.persisted()
+
} else if (isGraphQLFunctionIdentifier(node.expression.expression)) {
+
return true;
+
} else {
+
return isTadaGraphQLFunction(node.expression.expression, checker);
+
}
+
};
+
+
/** Checks if node is a gql.tada or regular graphql() call */
+
export const isGraphQLCall = (
+
node: ts.Node,
+
checker: ts.TypeChecker | undefined
+
): node is ts.CallExpression => {
+
return (
+
ts.isCallExpression(node) &&
+
node.arguments.length >= 1 &&
+
node.arguments.length <= 2 &&
+
(isGraphQLFunctionIdentifier(node.expression) ||
+
isTadaGraphQLCall(node, checker))
+
);
+
};
+
+
/** Checks if node is a gql/graphql tagged template literal */
+
export const isGraphQLTag = (
+
node: ts.Node
+
): node is ts.TaggedTemplateExpression =>
+
ts.isTaggedTemplateExpression(node) && isGraphQLFunctionIdentifier(node.tag);
+
+
/** Retrieves the `__name` branded tag from gql.tada `graphql()` or `graphql.persisted()` calls */
+
export const getSchemaName = (
+
node: ts.CallExpression,
+
typeChecker: ts.TypeChecker | undefined
+
): string | null => {
+
if (!typeChecker) return null;
+
const expression = ts.isPropertyAccessExpression(node.expression)
+
? node.expression.expression
+
: node.expression;
+
const type = typeChecker.getTypeAtLocation(expression);
+
if (type) {
+
const brandTypeSymbol = type.getProperty('__name');
+
if (brandTypeSymbol) {
+
const brand = typeChecker.getTypeOfSymbol(brandTypeSymbol);
+
if (brand.isUnionOrIntersection()) {
+
const found = brand.types.find(x => x.isStringLiteral());
+
return found && found.isStringLiteral() ? found.value : null;
+
} else if (brand.isStringLiteral()) {
+
return brand.value;
+
}
+
}
+
}
+
return null;
+
};
+84 -113
packages/graphqlsp/src/ast/index.ts
···
import { ts } from '../ts';
import { FragmentDefinitionNode, parse } from 'graphql';
-
import { templates } from './templates';
+
import * as checks from './checks';
+
import { resolveTadaFragmentArray } from './resolve';
+
+
export { getSchemaName } from './checks';
export function getSource(info: ts.server.PluginCreateInfo, filename: string) {
const program = info.languageService.getProgram();
···
> = [];
function find(node: ts.Node) {
if (
-
(ts.isTaggedTemplateExpression(node) &&
-
templates.has(node.tag.getText())) ||
+
checks.isGraphQLTag(node) ||
(ts.isNoSubstitutionTemplateLiteral(node) &&
-
ts.isTaggedTemplateExpression(node.parent) &&
-
templates.has(node.parent.tag.getText()))
+
checks.isGraphQLTag(node.parent))
) {
result.push(node);
return;
···
function unrollFragment(
element: ts.Identifier,
-
info: ts.server.PluginCreateInfo
+
info: ts.server.PluginCreateInfo,
+
typeChecker: ts.TypeChecker | undefined
): Array<FragmentDefinitionNode> {
const fragments: Array<FragmentDefinitionNode> = [];
const definitions = info.languageService.getDefinitionAtPosition(
···
found = found.parent.initializer;
}
-
if (ts.isCallExpression(found) && templates.has(found.expression.getText())) {
-
const [arg, arg2] = found.arguments;
-
if (arg2 && ts.isArrayLiteralExpression(arg2)) {
-
arg2.elements.forEach(element => {
-
if (ts.isIdentifier(element)) {
-
fragments.push(...unrollFragment(element, info));
-
}
-
});
+
// Check whether we've got a `graphql()` or `gql()` call, by the
+
// call expression's identifier
+
if (!checks.isGraphQLCall(found, typeChecker)) {
+
return fragments;
+
}
+
+
try {
+
const text = found.arguments[0];
+
const fragmentRefs = resolveTadaFragmentArray(found.arguments[1]);
+
if (fragmentRefs) {
+
for (const identifier of fragmentRefs) {
+
fragments.push(...unrollFragment(identifier, info, typeChecker));
+
}
}
-
-
try {
-
const parsed = parse(arg.getText().slice(1, -1), { noLocation: true });
-
parsed.definitions.forEach(definition => {
-
if (definition.kind === 'FragmentDefinition') {
-
fragments.push(definition);
-
}
-
});
-
} catch (e) {}
-
}
+
const parsed = parse(text.getText().slice(1, -1), { noLocation: true });
+
parsed.definitions.forEach(definition => {
+
if (definition.kind === 'FragmentDefinition') {
+
fragments.push(definition);
+
}
+
});
+
} catch (e) {}
return fragments;
}
···
wip: FragmentDefinitionNode[],
info: ts.server.PluginCreateInfo
): FragmentDefinitionNode[] {
+
const typeChecker = info.languageService.getProgram()?.getTypeChecker();
fragmentsArray.elements.forEach(element => {
if (ts.isIdentifier(element)) {
-
wip.push(...unrollFragment(element, info));
+
wip.push(...unrollFragment(element, info, typeChecker));
} else if (ts.isPropertyAccessExpression(element)) {
let el = element;
-
while (ts.isPropertyAccessExpression(el.expression)) {
-
el = el.expression;
-
}
-
+
while (ts.isPropertyAccessExpression(el.expression)) el = el.expression;
if (ts.isIdentifier(el.name)) {
-
wip.push(...unrollFragment(el.name, info));
+
wip.push(...unrollFragment(el.name, info, typeChecker));
}
}
});
···
return wip;
}
-
export const getSchemaName = (
-
node: ts.CallExpression,
-
typeChecker?: ts.TypeChecker
-
): string | null => {
-
if (!typeChecker) return null;
-
-
const expression = ts.isPropertyAccessExpression(node.expression)
-
? node.expression.expression
-
: node.expression;
-
const type = typeChecker.getTypeAtLocation(expression);
-
if (type) {
-
const brandTypeSymbol = type.getProperty('__name');
-
if (brandTypeSymbol) {
-
const brand = typeChecker.getTypeOfSymbol(brandTypeSymbol);
-
if (brand.isUnionOrIntersection()) {
-
const found = brand.types.find(x => x.isStringLiteral());
-
return found && found.isStringLiteral() ? found.value : null;
-
} else if (brand.isStringLiteral()) {
-
return brand.value;
-
}
-
}
-
}
-
-
return null;
-
};
-
export function findAllCallExpressions(
sourceFile: ts.SourceFile,
info: ts.server.PluginCreateInfo,
shouldSearchFragments: boolean = true
): {
nodes: Array<{
-
node: ts.NoSubstitutionTemplateLiteral;
+
node: ts.StringLiteralLike;
schema: string | null;
}>;
fragments: Array<FragmentDefinitionNode>;
} {
const typeChecker = info.languageService.getProgram()?.getTypeChecker();
const result: Array<{
-
node: ts.NoSubstitutionTemplateLiteral;
+
node: ts.StringLiteralLike;
schema: string | null;
}> = [];
let fragments: Array<FragmentDefinitionNode> = [];
let hasTriedToFindFragments = shouldSearchFragments ? false : true;
-
function find(node: ts.Node) {
-
if (ts.isCallExpression(node) && templates.has(node.expression.getText())) {
-
const name = getSchemaName(node, typeChecker);
+
+
function find(node: ts.Node): void {
+
if (!ts.isCallExpression(node) || checks.isIIFE(node)) {
+
return ts.forEachChild(node, find);
+
}
-
const [arg, arg2] = node.arguments;
+
// Check whether we've got a `graphql()` or `gql()` call, by the
+
// call expression's identifier
+
if (!checks.isGraphQLCall(node, typeChecker)) {
+
return;
+
}
-
if (!hasTriedToFindFragments && !arg2) {
-
hasTriedToFindFragments = true;
-
fragments.push(...getAllFragments(sourceFile.fileName, node, info));
-
} else if (arg2 && ts.isArrayLiteralExpression(arg2)) {
-
arg2.elements.forEach(element => {
-
if (ts.isIdentifier(element)) {
-
fragments.push(...unrollFragment(element, info));
-
} else if (ts.isPropertyAccessExpression(element)) {
-
let el = element;
-
while (ts.isPropertyAccessExpression(el.expression)) {
-
el = el.expression;
-
}
+
const name = checks.getSchemaName(node, typeChecker);
+
const text = node.arguments[0];
+
const fragmentRefs = resolveTadaFragmentArray(node.arguments[1]);
-
if (ts.isIdentifier(el.name)) {
-
fragments.push(...unrollFragment(el.name, info));
-
}
-
}
-
});
+
if (!hasTriedToFindFragments && !fragmentRefs) {
+
hasTriedToFindFragments = true;
+
fragments.push(...getAllFragments(sourceFile.fileName, node, info));
+
} else if (fragmentRefs) {
+
for (const identifier of fragmentRefs) {
+
fragments.push(...unrollFragment(identifier, info, typeChecker));
}
+
}
-
if (arg && ts.isNoSubstitutionTemplateLiteral(arg)) {
-
result.push({ node: arg, schema: name });
-
}
-
return;
-
} else {
-
ts.forEachChild(node, find);
+
if (text && ts.isStringLiteralLike(text)) {
+
result.push({ node: text, schema: name });
}
}
find(sourceFile);
···
ts.CallExpression | { node: ts.CallExpression; schema: string | null }
> = [];
const typeChecker = info?.languageService.getProgram()?.getTypeChecker();
-
function find(node: ts.Node) {
-
if (node && ts.isCallExpression(node)) {
-
// This expression ideally for us looks like <template>.persisted
-
const expression = node.expression.getText();
-
const parts = expression.split('.');
-
if (parts.length !== 2) return;
-
-
const [template, method] = parts;
-
if (!templates.has(template) || method !== 'persisted') return;
+
function find(node: ts.Node): void {
+
if (!ts.isCallExpression(node) || checks.isIIFE(node)) {
+
return ts.forEachChild(node, find);
+
}
-
if (info) {
-
const name = getSchemaName(node, typeChecker);
-
result.push({ node, schema: name });
-
} else {
-
result.push(node);
-
}
+
if (!checks.isTadaPersistedCall(node, typeChecker)) {
+
return;
+
} else if (info) {
+
const name = checks.getSchemaName(node, typeChecker);
+
result.push({ node, schema: name });
} else {
-
ts.forEachChild(node, find);
+
result.push(node);
}
}
find(sourceFile);
···
export function getAllFragments(
fileName: string,
-
node: ts.CallExpression,
+
node: ts.Node,
info: ts.server.PluginCreateInfo
) {
let fragments: Array<FragmentDefinitionNode> = [];
+
const typeChecker = info.languageService.getProgram()?.getTypeChecker();
+
if (!ts.isCallExpression(node)) {
+
return fragments;
+
}
+
+
const fragmentRefs = resolveTadaFragmentArray(node.arguments[1]);
+
if (fragmentRefs) {
+
const typeChecker = info.languageService.getProgram()?.getTypeChecker();
+
for (const identifier of fragmentRefs) {
+
fragments.push(...unrollFragment(identifier, info, typeChecker));
+
}
+
return fragments;
+
} else if (checks.isTadaGraphQLCall(node, typeChecker)) {
+
return fragments;
+
}
+
const definitions = info.languageService.getDefinitionAtPosition(
fileName,
node.expression.getStart()
);
if (!definitions || !definitions.length) return fragments;
-
-
if (node.arguments[1] && ts.isArrayLiteralExpression(node.arguments[1])) {
-
const arg2 = node.arguments[1] as ts.ArrayLiteralExpression;
-
arg2.elements.forEach(element => {
-
if (ts.isIdentifier(element)) {
-
fragments.push(...unrollFragment(element, info));
-
}
-
});
-
return fragments;
-
}
const def = definitions[0];
if (!def) return fragments;
···
export function bubbleUpCallExpression(node: ts.Node): ts.Node {
while (
-
ts.isNoSubstitutionTemplateLiteral(node) ||
+
ts.isStringLiteralLike(node) ||
ts.isToken(node) ||
ts.isTemplateExpression(node) ||
ts.isTemplateSpan(node)
+19 -2
packages/graphqlsp/src/ast/resolve.ts
···
};
export function resolveTemplate(
-
node: ts.TaggedTemplateExpression | ts.NoSubstitutionTemplateLiteral,
+
node: ts.TaggedTemplateExpression | ts.StringLiteralLike,
filename: string,
info: ts.server.PluginCreateInfo
): TemplateResult {
-
if (ts.isNoSubstitutionTemplateLiteral(node)) {
+
if (ts.isStringLiteralLike(node)) {
return { combinedText: node.getText().slice(1, -1), resolvedSpans: [] };
}
···
return { combinedText: templateText, resolvedSpans };
}
+
+
export const resolveTadaFragmentArray = (
+
node: ts.Expression | undefined
+
): undefined | readonly ts.Identifier[] => {
+
if (!node) return undefined;
+
// NOTE: Remove `as T`, users may commonly use `as const` for no reason
+
while (ts.isAsExpression(node)) node = node.expression;
+
if (!ts.isArrayLiteralExpression(node)) return undefined;
+
// NOTE: Let's avoid the allocation of another array here if we can
+
if (node.elements.every(ts.isIdentifier)) return node.elements;
+
const identifiers: ts.Identifier[] = [];
+
for (let element of node.elements) {
+
while (ts.isPropertyAccessExpression(element)) element = element.expression;
+
if (ts.isIdentifier(element)) identifiers.push(element);
+
}
+
return identifiers;
+
};
+5 -1
packages/graphqlsp/src/ast/token.ts
···
}
export const getToken = (
-
template: ts.TemplateLiteral,
+
template: ts.Expression,
cursorPosition: number
): Token | undefined => {
+
if (!ts.isTemplateLiteral(template) && !ts.isStringLiteralLike(template)) {
+
return undefined;
+
}
+
const text = template.getText().slice(1, -1);
const input = text.split('\n');
const parser = onlineParser();
+6 -18
packages/graphqlsp/src/autoComplete.ts
···
import { FragmentDefinitionNode, GraphQLSchema, Kind, parse } from 'graphql';
import { print } from '@0no-co/graphql.web';
+
import * as checks from './ast/checks';
import {
bubbleUpCallExpression,
bubbleUpTemplate,
findNode,
getAllFragments,
-
getSchemaName,
getSource,
} from './ast';
import { Cursor } from './ast/cursor';
import { resolveTemplate } from './ast/resolve';
import { getToken } from './ast/token';
import { getSuggestionsForFragmentSpread } from './graphql/getFragmentSpreadSuggestions';
-
import { templates } from './ast/templates';
import { SchemaRef } from './graphql/getSchema';
export function getGraphQLCompletions(
···
info: ts.server.PluginCreateInfo
): ts.WithMetadata<ts.CompletionInfo> | undefined {
const isCallExpression = info.config.templateIsCallExpression ?? true;
-
+
const typeChecker = info.languageService.getProgram()?.getTypeChecker();
const source = getSource(info, filename);
if (!source) return undefined;
···
: bubbleUpTemplate(node);
let text, cursor, schemaToUse: GraphQLSchema | undefined;
-
if (
-
ts.isCallExpression(node) &&
-
isCallExpression &&
-
templates.has(node.expression.getText()) &&
-
node.arguments.length > 0 &&
-
ts.isNoSubstitutionTemplateLiteral(node.arguments[0])
-
) {
-
const typeChecker = info.languageService.getProgram()?.getTypeChecker();
-
const schemaName = getSchemaName(node, typeChecker);
+
if (isCallExpression && checks.isGraphQLCall(node, typeChecker)) {
+
const schemaName = checks.getSchemaName(node, typeChecker);
schemaToUse =
schemaName && schema.multi[schemaName]
···
text = `${queryText}\n${fragments.map(x => print(x)).join('\n')}`;
cursor = new Cursor(foundToken.line, foundToken.start - 1);
-
} else if (ts.isTaggedTemplateExpression(node)) {
-
const { template, tag } = node;
-
-
if (!ts.isIdentifier(tag) || !templates.has(tag.text)) return undefined;
-
-
const foundToken = getToken(template, cursorPosition);
+
} else if (!isCallExpression && checks.isGraphQLTag(node)) {
+
const foundToken = getToken(node.template, cursorPosition);
if (!foundToken || !schema.current) return undefined;
const { combinedText, resolvedSpans } = resolveTemplate(
+3 -3
packages/graphqlsp/src/diagnostics.ts
···
let fragments: Array<FragmentDefinitionNode> = [],
nodes: {
-
node: ts.NoSubstitutionTemplateLiteral | ts.TaggedTemplateExpression;
+
node: ts.StringLiteralLike | ts.TaggedTemplateExpression;
schema: string | null;
}[];
if (isCallExpression) {
···
if (
!initializer ||
!ts.isCallExpression(initializer) ||
-
!ts.isNoSubstitutionTemplateLiteral(initializer.arguments[0])
+
!ts.isStringLiteralLike(initializer.arguments[0])
) {
// TODO: we can make this check more stringent where we also parse and resolve
// the accompanying template.
···
fragments,
}: {
nodes: {
-
node: ts.TaggedTemplateExpression | ts.NoSubstitutionTemplateLiteral;
+
node: ts.TaggedTemplateExpression | ts.StringLiteralLike;
schema: string | null;
}[];
fragments: FragmentDefinitionNode[];
+11 -22
packages/graphqlsp/src/persisted.ts
···
import { createHash } from 'crypto';
+
import * as checks from './ast/checks';
import { findAllCallExpressions, findNode, getSource } from './ast';
import { resolveTemplate } from './ast/resolve';
-
import { templates } from './ast/templates';
import { parse, print, visit } from '@0no-co/graphql.web';
type PersistedAction = {
···
replacement: string;
};
-
const isPersistedCall = (expr: ts.LeftHandSideExpression) => {
-
const expressionText = expr.getText();
-
const [template, method] = expressionText.split('.');
-
return templates.has(template) && method === 'persisted';
-
};
-
export function getPersistedCodeFixAtPosition(
filename: string,
position: number,
info: ts.server.PluginCreateInfo
): PersistedAction | undefined {
const isCallExpression = info.config.templateIsCallExpression ?? true;
-
+
const typeChecker = info.languageService.getProgram()?.getTypeChecker();
if (!isCallExpression) return undefined;
let source = getSource(info, filename);
···
// like "graphql.persisted", in a future iteration when the API surface
// is more defined we will need to use the ts.Symbol to support re-exporting
// this function by means of "export const peristed = graphql.persisted".
-
if (
-
(callExpression && !ts.isCallExpression(callExpression)) ||
-
!isPersistedCall(callExpression.expression) ||
-
(!callExpression.typeArguments && !callExpression.arguments[1])
-
)
+
if (!checks.isTadaPersistedCall(callExpression, typeChecker)) {
return undefined;
+
}
let foundNode,
foundFilename = filename;
···
if (
!initializer ||
!ts.isCallExpression(initializer) ||
-
!ts.isNoSubstitutionTemplateLiteral(initializer.arguments[0])
+
!ts.isStringLiteralLike(initializer.arguments[0])
)
return undefined;
···
export const generateHashForDocument = (
info: ts.server.PluginCreateInfo,
-
templateLiteral:
-
| ts.NoSubstitutionTemplateLiteral
-
| ts.TaggedTemplateExpression,
+
templateLiteral: ts.StringLiteralLike | ts.TaggedTemplateExpression,
foundFilename: string
): string | undefined => {
const externalSource = getSource(info, foundFilename)!;
···
[...spreads].forEach(spreadName => {
const fragmentDefinition = fragments.find(x => x.name.value === spreadName);
if (!fragmentDefinition) {
-
console.warn(
+
info.project.projectService.logger.info(
`[GraphQLSP] could not find fragment for spread ${spreadName}!`
);
return;
···
if (!references) return { node: null, filename };
+
const typeChecker = info.languageService.getProgram()?.getTypeChecker();
let found: ts.CallExpression | null = null;
let foundFilename = filename;
references.forEach(ref => {
···
if (
ts.isVariableDeclaration(foundNode.parent) &&
foundNode.parent.initializer &&
-
ts.isCallExpression(foundNode.parent.initializer) &&
-
templates.has(foundNode.parent.initializer.expression.getText())
+
checks.isGraphQLCall(foundNode.parent.initializer, typeChecker)
) {
found = foundNode.parent.initializer;
foundFilename = ref.fileName;
···
if (!references) return { node: null, filename };
+
const typeChecker = info.languageService.getProgram()?.getTypeChecker();
let found: ts.CallExpression | null = null;
let foundFilename = filename;
references.forEach(ref => {
···
if (
ts.isVariableDeclaration(foundNode.parent) &&
foundNode.parent.initializer &&
-
ts.isCallExpression(foundNode.parent.initializer) &&
-
templates.has(foundNode.parent.initializer.expression.getText())
+
checks.isGraphQLCall(foundNode.parent.initializer, typeChecker)
) {
found = foundNode.parent.initializer;
foundFilename = ref.fileName;
+6 -13
packages/graphqlsp/src/quickInfo.ts
···
getSchemaName,
getSource,
} from './ast';
+
+
import * as checks from './ast/checks';
import { resolveTemplate } from './ast/resolve';
import { getToken } from './ast/token';
import { Cursor } from './ast/cursor';
···
info: ts.server.PluginCreateInfo
): ts.QuickInfo | undefined {
const isCallExpression = info.config.templateIsCallExpression ?? true;
+
const typeChecker = info.languageService.getProgram()?.getTypeChecker();
const source = getSource(info, filename);
if (!source) return undefined;
···
: bubbleUpTemplate(node);
let cursor, text, schemaToUse: GraphQLSchema | undefined;
-
if (
-
ts.isCallExpression(node) &&
-
isCallExpression &&
-
templates.has(node.expression.getText()) &&
-
node.arguments.length > 0 &&
-
ts.isNoSubstitutionTemplateLiteral(node.arguments[0])
-
) {
+
if (isCallExpression && checks.isGraphQLCall(node, typeChecker)) {
const typeChecker = info.languageService.getProgram()?.getTypeChecker();
const schemaName = getSchemaName(node, typeChecker);
···
text = node.arguments[0].getText();
cursor = new Cursor(foundToken.line, foundToken.start - 1);
-
} else if (ts.isTaggedTemplateExpression(node)) {
-
const { template, tag } = node;
-
if (!ts.isIdentifier(tag) || !templates.has(tag.text)) return undefined;
-
-
const foundToken = getToken(template, cursorPosition);
-
+
} else if (!isCallExpression && checks.isGraphQLTag(node)) {
+
const foundToken = getToken(node.template, cursorPosition);
if (!foundToken || !schema.current) return undefined;
const { combinedText, resolvedSpans } = resolveTemplate(