Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 19 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/young-phones-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@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 changes: 116 additions & 0 deletions packages/graphqlsp/src/ast/checks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
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
);
};
kitten marked this conversation as resolved.
Show resolved Hide resolved

/** 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;
};
Loading