···
import { resolveTemplate } from './ast/resolve';
import { generateTypedDocumentNodes } from './graphql/generateTypes';
export const SEMANTIC_DIAGNOSTIC_CODE = 52001;
export const MISSING_OPERATION_NAME_CODE = 52002;
···
export function getGraphQLDiagnostics(
// This is so that we don't change offsets when there are
schema: { current: GraphQLSchema | null; version: number },
info: ts.server.PluginCreateInfo
): ts.Diagnostic[] | undefined {
const tagTemplate = info.config.template || 'gql';
const isCallExpression = info.config.templateIsCallExpression ?? false;
let source = getSource(info, filename);
···
if (cache.has(cacheKey)) {
tsDiagnostics = cache.get(cacheKey)!;
+
tsDiagnostics = runDiagnostics(source, { nodes, fragments }, schema, info);
+
cache.set(cacheKey, tsDiagnostics);
+
const runDiagnostics = (
+
nodes: (ts.TaggedTemplateExpression | ts.NoSubstitutionTemplateLiteral)[];
+
fragments: FragmentDefinitionNode[];
+
schema: { current: GraphQLSchema | null; version: number },
+
info: ts.server.PluginCreateInfo
+
const tagTemplate = info.config.template || 'gql';
+
const filename = source.fileName;
+
const isCallExpression = info.config.templateIsCallExpression ?? false;
+
const diagnostics = nodes
+
let node = originalNode;
+
(ts.isNoSubstitutionTemplateLiteral(node) ||
+
ts.isTemplateExpression(node))
+
if (ts.isTaggedTemplateExpression(node.parent)) {
+
const { combinedText: text, resolvedSpans } = resolveTemplate(
+
const lines = text.split('\n');
+
let isExpression = false;
+
if (ts.isAsExpression(node.parent)) {
+
if (ts.isExpressionStatement(node.parent.parent)) {
+
} else if (ts.isExpressionStatement(node.parent)) {
+
// When we are dealing with a plain gql statement we have to add two these can be recognised
+
// by the fact that the parent is an expressionStatement
+
(isCallExpression ? 0 : tagTemplate.length + (isExpression ? 2 : 1));
+
const endPosition = startingPosition + node.getText().length;
+
let docFragments = [...fragments];
+
if (isCallExpression) {
+
const documentFragments = parse(text, {
+
}).definitions.filter(x => x.kind === Kind.FRAGMENT_DEFINITION);
+
docFragments = docFragments.filter(
+
!documentFragments.some(
+
y.kind === Kind.FRAGMENT_DEFINITION &&
+
y.name.value === x.name.value
+
const graphQLDiagnostics = getDiagnostics(
+
const { start, end } = x.range;
+
// We add the start.line to account for newline characters which are
+
let startChar = startingPosition + start.line;
+
for (let i = 0; i <= start.line; i++) {
+
if (i === start.line) startChar += start.character;
+
else startChar += lines[i].length;
+
let endChar = startingPosition + end.line;
+
for (let i = 0; i <= end.line; i++) {
+
if (i === end.line) endChar += end.character;
+
else endChar += lines[i].length;
+
const locatedInFragment = resolvedSpans.find(x => {
+
const newEnd = x.new.start + x.new.length;
+
return startChar >= x.new.start && endChar <= newEnd;
+
if (!!locatedInFragment) {
+
start: locatedInFragment.original.start,
+
length: locatedInFragment.original.length,
+
if (startChar > endPosition) {
+
// we have to calculate the added length and fix this
+
const addedCharacters = resolvedSpans
+
.filter(x => x.new.start + x.new.length < startChar)
+
(acc, span) => acc + (span.new.length - span.original.length),
+
startChar = startChar - addedCharacters;
+
endChar = endChar - addedCharacters;
+
length: endChar - startChar,
+
length: endChar - startChar,
+
.filter(x => x.start + x.length <= endPosition);
+
const parsed = parse(text, { noLocation: true });
+
parsed.definitions.some(x => x.kind === Kind.OPERATION_DEFINITION)
+
const op = parsed.definitions.find(
+
x => x.kind === Kind.OPERATION_DEFINITION
+
) as OperationDefinitionNode;
+
graphQLDiagnostics.push({
+
message: 'Operation needs a name for types to be generated.',
+
code: MISSING_OPERATION_NAME_CODE,
+
length: originalNode.getText().length,
+
return graphQLDiagnostics;
+
.filter(Boolean) as Array<Diagnostic & { length: number; start: number }>;
+
const tsDiagnostics = diagnostics.map(diag => ({
+
? ts.DiagnosticCategory.Warning
+
: ts.DiagnosticCategory.Error,
+
typeof diag.code === 'number'
+
? USING_DEPRECATED_FIELD_CODE
+
: SEMANTIC_DIAGNOSTIC_CODE,
+
messageText: diag.message.split('\n')[0],
+
const importDiagnostics = checkImportsForFragments(source, info);
+
return [...tsDiagnostics, ...importDiagnostics];
+
const checkImportsForFragments = (
+
info: ts.server.PluginCreateInfo
+
const imports = findAllImports(source);
+
const shouldCheckForColocatedFragments =
+
info.config.shouldCheckForColocatedFragments ?? false;
+
const tsDiagnostics: ts.Diagnostic[] = [];
+
if (imports.length && shouldCheckForColocatedFragments) {
+
const typeChecker = info.languageService.getProgram()?.getTypeChecker();
+
imports.forEach(imp => {
+
if (!imp.importClause) return;
+
const importedNames: string[] = [];
+
if (imp.importClause.name) {
+
importedNames.push(imp.importClause?.name.text);
+
imp.importClause.namedBindings &&
+
ts.isNamespaceImport(imp.importClause.namedBindings)
+
// TODO: we might need to warn here when the fragment is unused as a namespace import
+
imp.importClause.namedBindings &&
+
ts.isNamedImportBindings(imp.importClause.namedBindings)
+
imp.importClause.namedBindings.elements.forEach(el => {
+
importedNames.push(el.name.text);
+
const symbol = typeChecker?.getSymbolAtLocation(imp.moduleSpecifier);
+
const moduleExports = typeChecker?.getExportsOfModule(symbol);
+
if (!moduleExports) return;
+
const missingImports = moduleExports
+
if (importedNames.includes(exp.name)) {
+
const declarations = exp.getDeclarations();
+
const declaration = declarations?.find(x => {
+
// TODO: check whether the sourceFile.fileName resembles the module
+
if (!declaration) return;
+
const [template] = findAllTaggedTemplateNodes(declaration);
+
ts.isNoSubstitutionTemplateLiteral(node) ||
+
ts.isTemplateExpression(node)
+
if (ts.isTaggedTemplateExpression(node.parent)) {
+
const text = resolveTemplate(
+
node.getSourceFile().fileName,
+
const parsed = parse(text, { noLocation: true });
+
parsed.definitions.every(
+
x => x.kind === Kind.FRAGMENT_DEFINITION
+
return `'${exp.name}'`;
+
if (missingImports.length) {
+
length: imp.getText().length,
+
category: ts.DiagnosticCategory.Message,
+
code: MISSING_FRAGMENT_CODE,
+
messageText: `Missing Fragment import(s) ${missingImports.join(
+
)} from ${imp.moduleSpecifier.getText()}.`,
+
const runTypedDocumentNodes = (
+
nodes: (ts.TaggedTemplateExpression | ts.NoSubstitutionTemplateLiteral)[],
+
texts: (string | undefined)[],
+
schema: { current: GraphQLSchema | null },
+
diagnostics: ts.Diagnostic[],
+
sourceFile: ts.SourceFile,
+
info: ts.server.PluginCreateInfo
+
const filename = sourceFile.fileName;
+
const scalars = info.config.scalars || {};
+
const disableTypegen = info.config.disableTypegen ?? false;
+
let source: ts.SourceFile | undefined = sourceFile;
x.category === ts.DiagnosticCategory.Error ||
x.category === ts.DiagnosticCategory.Warning
···
if (isFileDirty(filename, source) && !isGeneratingTypes) {
isGeneratingTypes = true;
const parts = source.fileName.split('/');
···
isGeneratingTypes = false;