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

refactor(graphqlsp): Add declaration helpers to replace language services (#351)

Co-authored-by: Jovi De Croock <decroockjovi@gmail.com>

+5
.changeset/puny-ghosts-clap.md
···
+
---
+
'@0no-co/graphqlsp': minor
+
---
+
+
Add new value declaration helpers to replace built-in services and to traverse TypeScript type checked AST exhaustively and efficiently.
+353
packages/graphqlsp/src/ast/declaration.ts
···
+
import { ts } from '../ts';
+
+
export type ValueDeclaration =
+
| ts.BinaryExpression
+
| ts.ArrowFunction
+
| ts.BindingElement
+
| ts.ClassDeclaration
+
| ts.ClassExpression
+
| ts.ClassStaticBlockDeclaration
+
| ts.ConstructorDeclaration
+
| ts.EnumDeclaration
+
| ts.EnumMember
+
| ts.ExportSpecifier
+
| ts.FunctionDeclaration
+
| ts.FunctionExpression
+
| ts.GetAccessorDeclaration
+
| ts.JsxAttribute
+
| ts.MethodDeclaration
+
| ts.ModuleDeclaration
+
| ts.ParameterDeclaration
+
| ts.PropertyAssignment
+
| ts.PropertyDeclaration
+
| ts.SetAccessorDeclaration
+
| ts.ShorthandPropertyAssignment
+
| ts.VariableDeclaration;
+
+
export type ValueOfDeclaration =
+
| ts.ClassExpression
+
| ts.ClassDeclaration
+
| ts.ArrowFunction
+
| ts.ClassStaticBlockDeclaration
+
| ts.ConstructorDeclaration
+
| ts.EnumDeclaration
+
| ts.FunctionDeclaration
+
| ts.GetAccessorDeclaration
+
| ts.SetAccessorDeclaration
+
| ts.MethodDeclaration
+
| ts.Expression;
+
+
/** Checks if a node is a `ts.Declaration` and a value.
+
* @remarks
+
* This checks if a given node is a value declaration only,
+
* excluding import/export specifiers, type declarations, and
+
* ambient declarations.
+
* All declarations that aren't JS(x) nodes will be discarded.
+
* This is based on `ts.isDeclarationKind`.
+
*/
+
export function isValueDeclaration(node: ts.Node): node is ValueDeclaration {
+
switch (node.kind) {
+
case ts.SyntaxKind.BinaryExpression:
+
case ts.SyntaxKind.ArrowFunction:
+
case ts.SyntaxKind.BindingElement:
+
case ts.SyntaxKind.ClassDeclaration:
+
case ts.SyntaxKind.ClassExpression:
+
case ts.SyntaxKind.ClassStaticBlockDeclaration:
+
case ts.SyntaxKind.Constructor:
+
case ts.SyntaxKind.EnumDeclaration:
+
case ts.SyntaxKind.EnumMember:
+
case ts.SyntaxKind.FunctionDeclaration:
+
case ts.SyntaxKind.FunctionExpression:
+
case ts.SyntaxKind.GetAccessor:
+
case ts.SyntaxKind.JsxAttribute:
+
case ts.SyntaxKind.MethodDeclaration:
+
case ts.SyntaxKind.Parameter:
+
case ts.SyntaxKind.PropertyAssignment:
+
case ts.SyntaxKind.PropertyDeclaration:
+
case ts.SyntaxKind.SetAccessor:
+
case ts.SyntaxKind.ShorthandPropertyAssignment:
+
case ts.SyntaxKind.VariableDeclaration:
+
return true;
+
default:
+
return false;
+
}
+
}
+
+
/** Returns true if operator assigns a value unchanged */
+
function isAssignmentOperator(token: ts.BinaryOperatorToken): boolean {
+
switch (token.kind) {
+
case ts.SyntaxKind.EqualsToken:
+
case ts.SyntaxKind.BarBarEqualsToken:
+
case ts.SyntaxKind.AmpersandAmpersandEqualsToken:
+
case ts.SyntaxKind.QuestionQuestionEqualsToken:
+
return true;
+
default:
+
return false;
+
}
+
}
+
+
/** Evaluates to the declaration's value initializer or itself if it declares a value */
+
export function getValueOfValueDeclaration(
+
node: ValueDeclaration
+
): ValueOfDeclaration | undefined {
+
switch (node.kind) {
+
case ts.SyntaxKind.ClassExpression:
+
case ts.SyntaxKind.ClassDeclaration:
+
case ts.SyntaxKind.ArrowFunction:
+
case ts.SyntaxKind.ClassStaticBlockDeclaration:
+
case ts.SyntaxKind.Constructor:
+
case ts.SyntaxKind.EnumDeclaration:
+
case ts.SyntaxKind.FunctionDeclaration:
+
case ts.SyntaxKind.FunctionExpression:
+
case ts.SyntaxKind.GetAccessor:
+
case ts.SyntaxKind.SetAccessor:
+
case ts.SyntaxKind.MethodDeclaration:
+
return node;
+
case ts.SyntaxKind.BindingElement:
+
case ts.SyntaxKind.EnumMember:
+
case ts.SyntaxKind.JsxAttribute:
+
case ts.SyntaxKind.Parameter:
+
case ts.SyntaxKind.PropertyAssignment:
+
case ts.SyntaxKind.PropertyDeclaration:
+
case ts.SyntaxKind.VariableDeclaration:
+
return node.initializer;
+
case ts.SyntaxKind.BinaryExpression:
+
return isAssignmentOperator(node.operatorToken) ? node.right : undefined;
+
case ts.SyntaxKind.ShorthandPropertyAssignment:
+
return node.objectAssignmentInitializer;
+
default:
+
return undefined;
+
}
+
}
+
+
// See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/utilities.ts#L652-L654
+
function climbPastPropertyOrElementAccess(node: ts.Node): ts.Node {
+
if (
+
node.parent &&
+
ts.isPropertyAccessExpression(node.parent) &&
+
node.parent.name === node
+
) {
+
return node.parent;
+
} else if (
+
node.parent &&
+
ts.isElementAccessExpression(node.parent) &&
+
node.parent.argumentExpression === node
+
) {
+
return node.parent;
+
} else {
+
return node;
+
}
+
}
+
+
// See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/utilities.ts#L602-L605
+
function isNewExpressionTarget(node: ts.Node): node is ts.NewExpression {
+
const target = climbPastPropertyOrElementAccess(node).parent;
+
return ts.isNewExpression(target) && target.expression === node;
+
}
+
+
// See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/utilities.ts#L607-L610
+
function isCallOrNewExpressionTarget(
+
node: ts.Node
+
): node is ts.CallExpression | ts.NewExpression {
+
const target = climbPastPropertyOrElementAccess(node).parent;
+
return ts.isCallOrNewExpression(target) && target.expression === node;
+
}
+
+
// See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/utilities.ts#L716-L719
+
function isNameOfFunctionDeclaration(node: ts.Node): boolean {
+
return (
+
ts.isIdentifier(node) &&
+
node.parent &&
+
ts.isFunctionLike(node.parent) &&
+
node.parent.name === node
+
);
+
}
+
+
// See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/utilities.ts#L2441-L2447
+
function getNameFromPropertyName(name: ts.PropertyName): string | undefined {
+
if (ts.isComputedPropertyName(name)) {
+
return ts.isStringLiteralLike(name.expression) ||
+
ts.isNumericLiteral(name.expression)
+
? name.expression.text
+
: undefined;
+
} else if (ts.isPrivateIdentifier(name) || ts.isMemberName(name)) {
+
return ts.idText(name);
+
} else {
+
return name.text;
+
}
+
}
+
+
/** Resolves the declaration of an identifier.
+
* @remarks
+
* This returns the declaration node first found for an identifier by resolving an identifier's
+
* symbol via the type checker.
+
* @privateRemarks
+
* This mirrors the implementation of `getDefinitionAtPosition` in TS' language service. However,
+
* it removes all cases that aren't applicable to identifiers and removes the intermediary positional
+
* data structure, instead returning raw AST nodes.
+
*/
+
export function getDeclarationOfIdentifier(
+
node: ts.Identifier,
+
checker: ts.TypeChecker
+
): ValueDeclaration | undefined {
+
// See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/goToDefinition.ts#L523-L540
+
let symbol = checker.getSymbolAtLocation(node);
+
if (
+
symbol?.declarations?.[0] &&
+
symbol.flags & ts.SymbolFlags.Alias &&
+
(node.parent === symbol?.declarations?.[0] ||
+
!ts.isNamespaceImport(symbol.declarations[0]))
+
) {
+
// Resolve alias symbols, excluding self-referential symbols
+
const aliased = checker.getAliasedSymbol(symbol);
+
if (aliased.declarations) symbol = aliased;
+
}
+
+
if (symbol && ts.isShorthandPropertyAssignment(node.parent)) {
+
// See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/goToDefinition.ts#L248-L257
+
// Resolve shorthand property assignments
+
const shorthandSymbol = checker.getShorthandAssignmentValueSymbol(
+
symbol.valueDeclaration
+
);
+
if (shorthandSymbol) symbol = shorthandSymbol;
+
} else if (
+
ts.isBindingElement(node.parent) &&
+
ts.isObjectBindingPattern(node.parent.parent) &&
+
node === (node.parent.propertyName || node.parent.name)
+
) {
+
// See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/goToDefinition.ts#L259-L280
+
// Resolve symbol of property in shorthand assignments
+
const name = getNameFromPropertyName(node);
+
const prop = name
+
? checker.getTypeAtLocation(node.parent.parent).getProperty(name)
+
: undefined;
+
if (prop) symbol = prop;
+
} else if (
+
ts.isObjectLiteralElement(node.parent) &&
+
(ts.isObjectLiteralExpression(node.parent.parent) ||
+
ts.isJsxAttributes(node.parent.parent)) &&
+
node.parent.name === node
+
) {
+
// See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/goToDefinition.ts#L298-L316
+
// Resolve symbol of property in object literal destructre expressions
+
const name = getNameFromPropertyName(node);
+
const prop = name
+
? checker.getContextualType(node.parent.parent)?.getProperty(name)
+
: undefined;
+
if (prop) symbol = prop;
+
}
+
+
if (symbol && symbol.declarations?.length) {
+
if (
+
symbol.flags & ts.SymbolFlags.Class &&
+
!(symbol.flags & (ts.SymbolFlags.Function | ts.SymbolFlags.Variable)) &&
+
isNewExpressionTarget(node)
+
) {
+
// See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/goToDefinition.ts#L603-L610
+
// Resolve first class-like declaration for new expressions
+
for (const declaration of symbol.declarations) {
+
if (ts.isClassLike(declaration)) return declaration;
+
}
+
} else if (
+
isCallOrNewExpressionTarget(node) ||
+
isNameOfFunctionDeclaration(node)
+
) {
+
// See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/goToDefinition.ts#L612-L616
+
// Resolve first function-like declaration for call expressions or named functions
+
for (const declaration of symbol.declarations) {
+
if (
+
ts.isFunctionLike(declaration) &&
+
!!(declaration as ts.FunctionLikeDeclaration).body &&
+
isValueDeclaration(declaration)
+
) {
+
return declaration;
+
}
+
}
+
}
+
+
// Account for assignments to property access expressions
+
// This resolves property access expressions to binding element parents
+
if (
+
symbol.valueDeclaration &&
+
ts.isPropertyAccessExpression(symbol.valueDeclaration)
+
) {
+
const parent = symbol.valueDeclaration.parent;
+
if (
+
parent &&
+
ts.isBinaryExpression(parent) &&
+
parent.left === symbol.valueDeclaration
+
) {
+
return parent;
+
}
+
}
+
+
if (
+
symbol.valueDeclaration &&
+
isValueDeclaration(symbol.valueDeclaration)
+
) {
+
// NOTE: We prefer value declarations, since the checker may have already applied conditions
+
// similar to `isValueDeclaration` and selected it beforehand
+
// Only use value declarations if they're not type/ambient declarations or imports/exports
+
return symbol.valueDeclaration;
+
}
+
+
// Selecting the first available result, if any
+
// NOTE: We left out `!isExpandoDeclaration` as a condition, since `valueDeclaration` above
+
// should handle some of these cases, and we don't have to care about this subtlety as much for identifiers
+
// See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/goToDefinition.ts#L582-L590
+
for (const declaration of symbol.declarations) {
+
// Only use declarations if they're not type/ambient declarations or imports/exports
+
if (isValueDeclaration(declaration)) return declaration;
+
}
+
}
+
+
return undefined;
+
}
+
+
/** Loops {@link getDeclarationOfIdentifier} until a value of the identifier is found */
+
export function getValueOfIdentifier(
+
node: ts.Identifier,
+
checker: ts.TypeChecker
+
): ValueOfDeclaration | undefined {
+
while (ts.isIdentifier(node)) {
+
const declaration = getDeclarationOfIdentifier(node, checker);
+
if (!declaration) {
+
return undefined;
+
} else {
+
const value = getValueOfValueDeclaration(declaration);
+
if (value && ts.isIdentifier(value) && value !== node) {
+
// If the resolved value is another identifiers, we continue searching, if the
+
// identifier isn't self-referential
+
node = value;
+
} else {
+
return value;
+
}
+
}
+
}
+
}
+
+
/** Resolves exressions that might not influence the target identifier */
+
export function getIdentifierOfChainExpression(
+
node: ts.Expression
+
): ts.Identifier | undefined {
+
let target: ts.Expression | undefined = node;
+
while (target) {
+
if (ts.isPropertyAccessExpression(target)) {
+
target = target.name;
+
} else if (
+
ts.isAsExpression(target) ||
+
ts.isSatisfiesExpression(target) ||
+
ts.isNonNullExpression(target) ||
+
ts.isParenthesizedExpression(target) ||
+
ts.isExpressionWithTypeArguments(target)
+
) {
+
target = target.expression;
+
} else if (ts.isCommaListExpression(target)) {
+
target = target.elements[target.elements.length - 1];
+
} else if (ts.isIdentifier(target)) {
+
return target;
+
} else {
+
return;
+
}
+
}
+
}
+30 -41
packages/graphqlsp/src/ast/index.ts
···
import { FragmentDefinitionNode, parse } from 'graphql';
import * as checks from './checks';
import { resolveTadaFragmentArray } from './resolve';
+
import {
+
getDeclarationOfIdentifier,
+
getValueOfIdentifier,
+
getIdentifierOfChainExpression,
+
} from './declaration';
export { getSchemaName } from './checks';
···
info: ts.server.PluginCreateInfo,
checker: ts.TypeChecker | undefined
): checks.GraphQLCallNode | null {
-
let prevElement: ts.Node | undefined;
-
let element: ts.Node | undefined = input;
-
// NOTE: Under certain circumstances, resolving an identifier can loop
-
while (ts.isIdentifier(element) && element !== prevElement) {
-
prevElement = element;
+
if (!checker) return null;
-
const definitions = info.languageService.getDefinitionAtPosition(
-
element.getSourceFile().fileName,
-
element.getStart()
-
);
-
-
const fragment = definitions && definitions[0];
-
const externalSource = fragment && getSource(info, fragment.fileName);
-
if (!fragment || !externalSource) return null;
-
-
element = findNode(externalSource, fragment.textSpan.start);
-
if (!element) return null;
-
-
while (ts.isPropertyAccessExpression(element.parent))
-
element = element.parent;
+
const value = getValueOfIdentifier(input, checker);
+
if (!value) return null;
-
if (
-
ts.isVariableDeclaration(element.parent) &&
-
element.parent.initializer &&
-
ts.isCallExpression(element.parent.initializer)
-
) {
-
element = element.parent.initializer;
-
} else if (ts.isPropertyAssignment(element.parent)) {
-
element = element.parent.initializer;
-
} else if (ts.isBinaryExpression(element.parent)) {
-
element = ts.isPropertyAccessExpression(element.parent.right)
-
? element.parent.right.name
-
: element.parent.right;
-
}
-
// If we find another Identifier, we continue resolving it
-
}
-
// Check whether we've got a `graphql()` or `gql()` call, by the
-
// call expression's identifier
-
return checks.isGraphQLCall(element, checker) ? element : null;
+
// Check whether we've got a `graphql()` or `gql()` call
+
return checks.isGraphQLCall(value, checker) ? value : null;
}
function unrollFragment(
···
return fragments;
}
-
const definitions = info.languageService.getDefinitionAtPosition(
-
fileName,
-
node.expression.getStart()
-
);
+
if (!typeChecker) return fragments;
+
+
const identifier = getIdentifierOfChainExpression(node.expression);
+
if (!identifier) return fragments;
+
+
const declaration = getDeclarationOfIdentifier(identifier, typeChecker);
+
if (!declaration) return fragments;
+
+
const sourceFile = declaration.getSourceFile();
+
if (!sourceFile) return fragments;
+
+
const definitions = [
+
{
+
fileName: sourceFile.fileName,
+
textSpan: {
+
start: declaration.getStart(),
+
length: declaration.getWidth(),
+
},
+
},
+
];
if (!definitions || !definitions.length) return fragments;
const def = definitions[0];
+27 -35
packages/graphqlsp/src/ast/resolve.ts
···
import { print } from '@0no-co/graphql.web';
import { ts } from '../ts';
-
import { findNode } from '.';
-
import { getSource } from '../ast';
+
import {
+
getDeclarationOfIdentifier,
+
getValueOfValueDeclaration,
+
} from './declaration';
type TemplateResult = {
combinedText: string;
···
const resolvedSpans = node.template.templateSpans
.map(span => {
if (ts.isIdentifier(span.expression)) {
-
const definitions = info.languageService.getDefinitionAtPosition(
-
filename,
-
span.expression.getStart()
+
const typeChecker = info.languageService.getProgram()?.getTypeChecker();
+
if (!typeChecker) return;
+
+
const declaration = getDeclarationOfIdentifier(
+
span.expression,
+
typeChecker
);
+
if (!declaration) return;
-
const def = definitions && definitions[0];
-
if (!def) return;
-
-
const src = getSource(info, def.fileName);
-
if (!src) return;
-
-
const node = findNode(src, def.textSpan.start);
-
if (!node || !node.parent) return;
-
-
const parent = node.parent;
+
const parent = declaration;
if (ts.isVariableDeclaration(parent)) {
const identifierName = span.expression.escapedText;
+
const value = getValueOfValueDeclaration(parent);
+
if (!value) return;
+
// we reduce by two to account for the "${"
const originalStart = span.expression.getStart() - 2;
const originalRange = {
···
// we add 1 to account for the "}"
length: span.expression.end - originalStart + 1,
};
-
if (
-
parent.initializer &&
-
ts.isTaggedTemplateExpression(parent.initializer)
-
) {
+
+
if (ts.isTaggedTemplateExpression(value)) {
const text = resolveTemplate(
-
parent.initializer,
-
def.fileName,
+
value,
+
parent.getSourceFile().fileName,
info
);
templateText = templateText.replace(
···
addedCharacters += text.combinedText.length - originalRange.length;
return alteredSpan;
} else if (
-
parent.initializer &&
-
ts.isAsExpression(parent.initializer) &&
-
ts.isTaggedTemplateExpression(parent.initializer.expression)
+
ts.isAsExpression(value) &&
+
ts.isTaggedTemplateExpression(value.expression)
) {
const text = resolveTemplate(
-
parent.initializer.expression,
-
def.fileName,
+
value.expression,
+
parent.getSourceFile().fileName,
info
);
templateText = templateText.replace(
···
addedCharacters += text.combinedText.length - originalRange.length;
return alteredSpan;
} else if (
-
parent.initializer &&
-
ts.isAsExpression(parent.initializer) &&
-
ts.isAsExpression(parent.initializer.expression) &&
-
ts.isObjectLiteralExpression(
-
parent.initializer.expression.expression
-
)
+
ts.isAsExpression(value) &&
+
ts.isAsExpression(value.expression) &&
+
ts.isObjectLiteralExpression(value.expression.expression)
) {
-
const astObject = JSON.parse(
-
parent.initializer.expression.expression.getText()
-
);
+
const astObject = JSON.parse(value.expression.expression.getText());
const resolvedTemplate = print(astObject);
templateText = templateText.replace(
'${' + span.expression.escapedText + '}',
+26 -27
packages/graphqlsp/src/checkImports.ts
···
import { ts } from './ts';
import { FragmentDefinitionNode, Kind, parse } from 'graphql';
-
import { findAllCallExpressions, findAllImports, getSource } from './ast';
+
import { findAllCallExpressions, findAllImports } from './ast';
import { resolveTemplate } from './ast/resolve';
-
import {
-
VariableDeclaration,
-
VariableStatement,
-
isSourceFile,
-
} from 'typescript';
+
import { getDeclarationOfIdentifier } from './ast/declaration';
export const MISSING_FRAGMENT_CODE = 52003;
···
if (!imp.importClause) return;
if (imp.importClause.name) {
-
const definitions = info.languageService.getDefinitionAtPosition(
-
source.fileName,
-
imp.importClause.name.getStart()
+
const declaration = getDeclarationOfIdentifier(
+
imp.importClause.name,
+
typeChecker
);
-
const def = definitions && definitions[0];
-
if (def) {
-
if (def.fileName.includes('node_modules')) return;
+
if (declaration) {
+
const sourceFile = declaration.getSourceFile();
+
if (sourceFile.fileName.includes('node_modules')) return;
-
const externalSource = getSource(info, def.fileName);
+
const externalSource = sourceFile;
if (!externalSource) return;
const fragmentsForImport = getFragmentsInSource(
···
imp.importClause.namedBindings &&
ts.isNamespaceImport(imp.importClause.namedBindings)
) {
-
const definitions = info.languageService.getDefinitionAtPosition(
-
source.fileName,
-
imp.importClause.namedBindings.getStart()
+
const declaration = getDeclarationOfIdentifier(
+
imp.importClause.namedBindings.name,
+
typeChecker
);
-
const def = definitions && definitions[0];
-
if (def) {
-
if (def.fileName.includes('node_modules')) return;
+
if (declaration) {
+
const sourceFile = declaration.getSourceFile();
+
if (sourceFile.fileName.includes('node_modules')) return;
-
const externalSource = getSource(info, def.fileName);
+
const externalSource = sourceFile;
if (!externalSource) return;
const fragmentsForImport = getFragmentsInSource(
···
ts.isNamedImportBindings(imp.importClause.namedBindings)
) {
imp.importClause.namedBindings.elements.forEach(el => {
-
const definitions = info.languageService.getDefinitionAtPosition(
-
source.fileName,
-
el.getStart()
+
const identifier = el.name || el.propertyName;
+
if (!identifier) return;
+
+
const declaration = getDeclarationOfIdentifier(
+
identifier,
+
typeChecker
);
-
const def = definitions && definitions[0];
-
if (def) {
-
if (def.fileName.includes('node_modules')) return;
+
if (declaration) {
+
const sourceFile = declaration.getSourceFile();
+
if (sourceFile.fileName.includes('node_modules')) return;
-
const externalSource = getSource(info, def.fileName);
+
const externalSource = sourceFile;
if (!externalSource) return;
const fragmentsForImport = getFragmentsInSource(
-1
packages/graphqlsp/src/diagnostics.ts
···
import { Diagnostic, getDiagnostics } from 'graphql-language-service';
import {
FragmentDefinitionNode,
-
GraphQLSchema,
Kind,
OperationDefinitionNode,
parse,
+8 -9
packages/graphqlsp/src/fieldUsage.ts
···
import { parse, visit } from 'graphql';
import { findNode } from './ast';
-
import { PropertyAccessExpression } from 'typescript';
+
import { getValueOfIdentifier } from './ast/declaration';
export const UNUSED_FIELD_CODE = 52005;
···
// TODO: Scope utilities in checkFieldUsageInFile to deduplicate
const checker = info.languageService.getProgram()!.getTypeChecker();
-
const declaration = checker.getSymbolAtLocation(func)?.valueDeclaration;
-
if (declaration && ts.isFunctionDeclaration(declaration)) {
-
func = declaration;
-
} else if (
-
declaration &&
-
ts.isVariableDeclaration(declaration) &&
-
declaration.initializer
+
const value = getValueOfIdentifier(func, checker);
+
if (
+
value &&
+
(ts.isFunctionDeclaration(value) ||
+
ts.isFunctionExpression(value) ||
+
ts.isArrowFunction(value))
) {
-
func = declaration.initializer;
+
func = value;
}
}
+32 -55
packages/graphqlsp/src/persisted.ts
···
print,
visit,
} from '@0no-co/graphql.web';
+
import {
+
getDeclarationOfIdentifier,
+
getValueOfIdentifier,
+
} from './ast/declaration';
type PersistedAction = {
span: {
···
filename: string,
info: ts.server.PluginCreateInfo
): { node: ts.CallExpression | null; filename: string } => {
-
// We look for the references of the generic so that we can use the document
-
// to generate the hash.
-
const references = info.languageService.getReferencesAtPosition(
-
filename,
-
typeQuery.exprName.getStart()
-
);
+
const typeChecker = info.languageService.getProgram()?.getTypeChecker();
+
if (!typeChecker) return { node: null, filename };
-
if (!references) return { node: null, filename };
+
// Handle EntityName (Identifier | QualifiedName)
+
let identifier: ts.Identifier | undefined;
+
if (ts.isIdentifier(typeQuery.exprName)) {
+
identifier = typeQuery.exprName;
+
} else if (ts.isQualifiedName(typeQuery.exprName)) {
+
// For qualified names like 'module.identifier', get the right-most identifier
+
identifier = typeQuery.exprName.right;
+
}
-
const typeChecker = info.languageService.getProgram()?.getTypeChecker();
-
let found: ts.CallExpression | null = null;
-
let foundFilename = filename;
-
references.forEach(ref => {
-
if (found) return;
+
if (!identifier) return { node: null, filename };
-
const source = getSource(info, ref.fileName);
-
if (!source) return;
-
const foundNode = findNode(source, ref.textSpan.start);
-
if (!foundNode) return;
+
const value = getValueOfIdentifier(identifier, typeChecker);
+
if (!value || !checks.isGraphQLCall(value, typeChecker)) {
+
return { node: null, filename };
+
}
-
if (
-
ts.isVariableDeclaration(foundNode.parent) &&
-
foundNode.parent.initializer &&
-
checks.isGraphQLCall(foundNode.parent.initializer, typeChecker)
-
) {
-
found = foundNode.parent.initializer;
-
foundFilename = ref.fileName;
-
}
-
});
-
-
return { node: found, filename: foundFilename };
+
return {
+
node: value as ts.CallExpression,
+
filename: value.getSourceFile().fileName,
+
};
};
export const getDocumentReferenceFromDocumentNode = (
···
info: ts.server.PluginCreateInfo
): { node: ts.CallExpression | null; filename: string } => {
if (ts.isIdentifier(documentNodeArgument)) {
-
// We look for the references of the generic so that we can use the document
-
// to generate the hash.
-
const references = info.languageService.getReferencesAtPosition(
-
filename,
-
documentNodeArgument.getStart()
-
);
-
-
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 (found) return;
+
if (!typeChecker) return { node: null, filename };
-
const source = getSource(info, ref.fileName);
-
if (!source) return;
-
const foundNode = findNode(source, ref.textSpan.start);
-
if (!foundNode) return;
-
-
if (
-
ts.isVariableDeclaration(foundNode.parent) &&
-
foundNode.parent.initializer &&
-
checks.isGraphQLCall(foundNode.parent.initializer, typeChecker)
-
) {
-
found = foundNode.parent.initializer;
-
foundFilename = ref.fileName;
-
}
-
});
+
const value = getValueOfIdentifier(documentNodeArgument, typeChecker);
+
if (!value || !checks.isGraphQLCall(value, typeChecker)) {
+
return { node: null, filename };
+
}
-
return { node: found, filename: foundFilename };
+
return {
+
node: value as ts.CallExpression,
+
filename: value.getSourceFile().fileName,
+
};
} else {
return { node: documentNodeArgument, filename };
}
-1
packages/graphqlsp/src/quickInfo.ts
···
import { resolveTemplate } from './ast/resolve';
import { getToken } from './ast/token';
import { Cursor } from './ast/cursor';
-
import { templates } from './ast/templates';
import { SchemaRef } from './graphql/getSchema';
export function getGraphQLQuickInfo(