···11+---
22+'@0no-co/graphqlsp': minor
33+---
44+55+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
···11+import { ts } from '../ts';
22+import { templates } from './templates';
33+44+/** Checks for an immediately-invoked function expression */
55+export const isIIFE = (node: ts.Node): boolean =>
66+ ts.isCallExpression(node) &&
77+ node.arguments.length === 0 &&
88+ (ts.isFunctionExpression(node.expression) ||
99+ ts.isArrowFunction(node.expression)) &&
1010+ !node.expression.asteriskToken &&
1111+ !node.expression.modifiers?.length;
1212+1313+/** Checks if node is a known identifier of graphql functions ('graphql' or 'gql') */
1414+export const isGraphQLFunctionIdentifier = (
1515+ node: ts.Node
1616+): node is ts.Identifier =>
1717+ ts.isIdentifier(node) && templates.has(node.escapedText as string);
1818+1919+/** If `checker` is passed, checks if node (as identifier/expression) is a gql.tada graphql() function */
2020+export const isTadaGraphQLFunction = (
2121+ node: ts.Node,
2222+ checker: ts.TypeChecker | undefined
2323+): node is ts.LeftHandSideExpression => {
2424+ if (!ts.isLeftHandSideExpression(node)) return false;
2525+ const type = checker?.getTypeAtLocation(node);
2626+ // Any function that has both a `scalar` and `persisted` property
2727+ // is automatically considered a gql.tada graphql() function.
2828+ return (
2929+ type != null &&
3030+ type.getProperty('scalar') != null &&
3131+ type.getProperty('persisted') != null
3232+ );
3333+};
3434+3535+/** If `checker` is passed, checks if node is a gql.tada graphql() call */
3636+export const isTadaGraphQLCall = (
3737+ node: ts.CallExpression,
3838+ checker: ts.TypeChecker | undefined
3939+): boolean => {
4040+ // We expect graphql() to be called with either a string literal
4141+ // or a string literal and an array of fragments
4242+ if (!ts.isCallExpression(node)) {
4343+ return false;
4444+ } else if (node.arguments.length < 1 || node.arguments.length > 2) {
4545+ return false;
4646+ } else if (!ts.isStringLiteralLike(node.arguments[0])) {
4747+ return false;
4848+ }
4949+ return checker ? isTadaGraphQLFunction(node.expression, checker) : false;
5050+};
5151+5252+/** Checks if node is a gql.tada graphql.persisted() call */
5353+export const isTadaPersistedCall = (
5454+ node: ts.Node,
5555+ checker: ts.TypeChecker | undefined
5656+): node is ts.CallExpression => {
5757+ if (!ts.isCallExpression(node)) {
5858+ return false;
5959+ } else if (!ts.isPropertyAccessExpression(node.expression)) {
6060+ return false; // rejecting non property access calls: <expression>.<name>()
6161+ } else if (
6262+ !ts.isIdentifier(node.expression.name) ||
6363+ node.expression.name.escapedText !== 'persisted'
6464+ ) {
6565+ return false; // rejecting calls on anyting but 'persisted': <expression>.persisted()
6666+ } else if (isGraphQLFunctionIdentifier(node.expression.expression)) {
6767+ return true;
6868+ } else {
6969+ return isTadaGraphQLFunction(node.expression.expression, checker);
7070+ }
7171+};
7272+7373+/** Checks if node is a gql.tada or regular graphql() call */
7474+export const isGraphQLCall = (
7575+ node: ts.Node,
7676+ checker: ts.TypeChecker | undefined
7777+): node is ts.CallExpression => {
7878+ return (
7979+ ts.isCallExpression(node) &&
8080+ node.arguments.length >= 1 &&
8181+ node.arguments.length <= 2 &&
8282+ (isGraphQLFunctionIdentifier(node.expression) ||
8383+ isTadaGraphQLCall(node, checker))
8484+ );
8585+};
8686+8787+/** Checks if node is a gql/graphql tagged template literal */
8888+export const isGraphQLTag = (
8989+ node: ts.Node
9090+): node is ts.TaggedTemplateExpression =>
9191+ ts.isTaggedTemplateExpression(node) && isGraphQLFunctionIdentifier(node.tag);
9292+9393+/** Retrieves the `__name` branded tag from gql.tada `graphql()` or `graphql.persisted()` calls */
9494+export const getSchemaName = (
9595+ node: ts.CallExpression,
9696+ typeChecker: ts.TypeChecker | undefined
9797+): string | null => {
9898+ if (!typeChecker) return null;
9999+ const expression = ts.isPropertyAccessExpression(node.expression)
100100+ ? node.expression.expression
101101+ : node.expression;
102102+ const type = typeChecker.getTypeAtLocation(expression);
103103+ if (type) {
104104+ const brandTypeSymbol = type.getProperty('__name');
105105+ if (brandTypeSymbol) {
106106+ const brand = typeChecker.getTypeOfSymbol(brandTypeSymbol);
107107+ if (brand.isUnionOrIntersection()) {
108108+ const found = brand.types.find(x => x.isStringLiteral());
109109+ return found && found.isStringLiteral() ? found.value : null;
110110+ } else if (brand.isStringLiteral()) {
111111+ return brand.value;
112112+ }
113113+ }
114114+ }
115115+ return null;
116116+};