···
import { resolveTemplate } from './ast/resolve';
import { generateTypedDocumentNodes } from './graphql/generateTypes';
22
-
import { Logger } from '.';
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
40
-
hasTSErrors: Boolean,
39
+
hasTSErrors: boolean,
schema: { current: GraphQLSchema | null; version: number },
info: ts.server.PluginCreateInfo
): ts.Diagnostic[] | undefined {
46
-
const logger: Logger = (msg: string) =>
47
-
info.project.projectService.logger.info(`[GraphQLSP] ${msg}`);
48
-
const disableTypegen = info.config.disableTypegen ?? false;
const tagTemplate = info.config.template || 'gql';
50
-
const scalars = info.config.scalars || {};
51
-
const shouldCheckForColocatedFragments =
52
-
info.config.shouldCheckForColocatedFragments ?? false;
const isCallExpression = info.config.templateIsCallExpression ?? false;
let source = getSource(info, filename);
···
if (cache.has(cacheKey)) {
tsDiagnostics = cache.get(cacheKey)!;
89
-
const diagnostics = nodes
90
-
.map(originalNode => {
91
-
let node = originalNode;
93
-
!isCallExpression &&
94
-
(ts.isNoSubstitutionTemplateLiteral(node) ||
95
-
ts.isTemplateExpression(node))
97
-
if (ts.isTaggedTemplateExpression(node.parent)) {
82
+
tsDiagnostics = runDiagnostics(source, { nodes, fragments }, schema, info);
83
+
cache.set(cacheKey, tsDiagnostics);
86
+
runTypedDocumentNodes(
97
+
return tsDiagnostics;
100
+
const runDiagnostics = (
101
+
source: ts.SourceFile,
106
+
nodes: (ts.TaggedTemplateExpression | ts.NoSubstitutionTemplateLiteral)[];
107
+
fragments: FragmentDefinitionNode[];
109
+
schema: { current: GraphQLSchema | null; version: number },
110
+
info: ts.server.PluginCreateInfo
112
+
const tagTemplate = info.config.template || 'gql';
113
+
const filename = source.fileName;
114
+
const isCallExpression = info.config.templateIsCallExpression ?? false;
116
+
const diagnostics = nodes
117
+
.map(originalNode => {
118
+
let node = originalNode;
120
+
!isCallExpression &&
121
+
(ts.isNoSubstitutionTemplateLiteral(node) ||
122
+
ts.isTemplateExpression(node))
124
+
if (ts.isTaggedTemplateExpression(node.parent)) {
125
+
node = node.parent;
104
-
const { combinedText: text, resolvedSpans } = resolveTemplate(
109
-
const lines = text.split('\n');
131
+
const { combinedText: text, resolvedSpans } = resolveTemplate(
136
+
const lines = text.split('\n');
111
-
let isExpression = false;
112
-
if (ts.isAsExpression(node.parent)) {
113
-
if (ts.isExpressionStatement(node.parent.parent)) {
114
-
isExpression = true;
116
-
} else if (ts.isExpressionStatement(node.parent)) {
138
+
let isExpression = false;
139
+
if (ts.isAsExpression(node.parent)) {
140
+
if (ts.isExpressionStatement(node.parent.parent)) {
119
-
// When we are dealing with a plain gql statement we have to add two these can be recognised
120
-
// by the fact that the parent is an expressionStatement
121
-
let startingPosition =
123
-
(isCallExpression ? 0 : tagTemplate.length + (isExpression ? 2 : 1));
124
-
const endPosition = startingPosition + node.getText().length;
143
+
} else if (ts.isExpressionStatement(node.parent)) {
144
+
isExpression = true;
146
+
// When we are dealing with a plain gql statement we have to add two these can be recognised
147
+
// by the fact that the parent is an expressionStatement
148
+
let startingPosition =
150
+
(isCallExpression ? 0 : tagTemplate.length + (isExpression ? 2 : 1));
151
+
const endPosition = startingPosition + node.getText().length;
126
-
let docFragments = [...fragments];
127
-
if (isCallExpression) {
128
-
const documentFragments = parse(text, {
130
-
}).definitions.filter(x => x.kind === Kind.FRAGMENT_DEFINITION);
131
-
docFragments = docFragments.filter(
133
-
!documentFragments.some(
135
-
y.kind === Kind.FRAGMENT_DEFINITION &&
136
-
y.name.value === x.name.value
153
+
let docFragments = [...fragments];
154
+
if (isCallExpression) {
155
+
const documentFragments = parse(text, {
157
+
}).definitions.filter(x => x.kind === Kind.FRAGMENT_DEFINITION);
158
+
docFragments = docFragments.filter(
160
+
!documentFragments.some(
162
+
y.kind === Kind.FRAGMENT_DEFINITION &&
163
+
y.name.value === x.name.value
141
-
const graphQLDiagnostics = getDiagnostics(
149
-
const { start, end } = x.range;
168
+
const graphQLDiagnostics = getDiagnostics(
176
+
const { start, end } = x.range;
151
-
// We add the start.line to account for newline characters which are
153
-
let startChar = startingPosition + start.line;
154
-
for (let i = 0; i <= start.line; i++) {
155
-
if (i === start.line) startChar += start.character;
156
-
else startChar += lines[i].length;
178
+
// We add the start.line to account for newline characters which are
180
+
let startChar = startingPosition + start.line;
181
+
for (let i = 0; i <= start.line; i++) {
182
+
if (i === start.line) startChar += start.character;
183
+
else startChar += lines[i].length;
159
-
let endChar = startingPosition + end.line;
160
-
for (let i = 0; i <= end.line; i++) {
161
-
if (i === end.line) endChar += end.character;
162
-
else endChar += lines[i].length;
186
+
let endChar = startingPosition + end.line;
187
+
for (let i = 0; i <= end.line; i++) {
188
+
if (i === end.line) endChar += end.character;
189
+
else endChar += lines[i].length;
165
-
const locatedInFragment = resolvedSpans.find(x => {
166
-
const newEnd = x.new.start + x.new.length;
167
-
return startChar >= x.new.start && endChar <= newEnd;
192
+
const locatedInFragment = resolvedSpans.find(x => {
193
+
const newEnd = x.new.start + x.new.length;
194
+
return startChar >= x.new.start && endChar <= newEnd;
170
-
if (!!locatedInFragment) {
197
+
if (!!locatedInFragment) {
200
+
start: locatedInFragment.original.start,
201
+
length: locatedInFragment.original.length,
204
+
if (startChar > endPosition) {
205
+
// we have to calculate the added length and fix this
206
+
const addedCharacters = resolvedSpans
207
+
.filter(x => x.new.start + x.new.length < startChar)
209
+
(acc, span) => acc + (span.new.length - span.original.length),
212
+
startChar = startChar - addedCharacters;
213
+
endChar = endChar - addedCharacters;
173
-
start: locatedInFragment.original.start,
174
-
length: locatedInFragment.original.length,
216
+
start: startChar + 1,
217
+
length: endChar - startChar,
177
-
if (startChar > endPosition) {
178
-
// we have to calculate the added length and fix this
179
-
const addedCharacters = resolvedSpans
180
-
.filter(x => x.new.start + x.new.length < startChar)
183
-
acc + (span.new.length - span.original.length),
186
-
startChar = startChar - addedCharacters;
187
-
endChar = endChar - addedCharacters;
190
-
start: startChar + 1,
191
-
length: endChar - startChar,
196
-
start: startChar + 1,
197
-
length: endChar - startChar,
222
+
start: startChar + 1,
223
+
length: endChar - startChar,
202
-
.filter(x => x.start + x.length <= endPosition);
228
+
.filter(x => x.start + x.length <= endPosition);
205
-
const parsed = parse(text, { noLocation: true });
231
+
const parsed = parse(text, { noLocation: true });
208
-
parsed.definitions.some(x => x.kind === Kind.OPERATION_DEFINITION)
210
-
const op = parsed.definitions.find(
211
-
x => x.kind === Kind.OPERATION_DEFINITION
212
-
) as OperationDefinitionNode;
214
-
graphQLDiagnostics.push({
215
-
message: 'Operation needs a name for types to be generated.',
217
-
code: MISSING_OPERATION_NAME_CODE,
218
-
length: originalNode.getText().length,
234
+
parsed.definitions.some(x => x.kind === Kind.OPERATION_DEFINITION)
236
+
const op = parsed.definitions.find(
237
+
x => x.kind === Kind.OPERATION_DEFINITION
238
+
) as OperationDefinitionNode;
240
+
graphQLDiagnostics.push({
241
+
message: 'Operation needs a name for types to be generated.',
243
+
code: MISSING_OPERATION_NAME_CODE,
244
+
length: originalNode.getText().length,
226
-
return graphQLDiagnostics;
229
-
.filter(Boolean) as Array<Diagnostic & { length: number; start: number }>;
252
+
return graphQLDiagnostics;
255
+
.filter(Boolean) as Array<Diagnostic & { length: number; start: number }>;
231
-
tsDiagnostics = diagnostics.map(diag => ({
233
-
length: diag.length,
236
-
diag.severity === 2
237
-
? ts.DiagnosticCategory.Warning
238
-
: ts.DiagnosticCategory.Error,
240
-
typeof diag.code === 'number'
242
-
: diag.severity === 2
243
-
? USING_DEPRECATED_FIELD_CODE
244
-
: SEMANTIC_DIAGNOSTIC_CODE,
245
-
messageText: diag.message.split('\n')[0],
257
+
const tsDiagnostics = diagnostics.map(diag => ({
259
+
length: diag.length,
262
+
diag.severity === 2
263
+
? ts.DiagnosticCategory.Warning
264
+
: ts.DiagnosticCategory.Error,
266
+
typeof diag.code === 'number'
268
+
: diag.severity === 2
269
+
? USING_DEPRECATED_FIELD_CODE
270
+
: SEMANTIC_DIAGNOSTIC_CODE,
271
+
messageText: diag.message.split('\n')[0],
248
-
const imports = findAllImports(source);
249
-
if (imports.length && shouldCheckForColocatedFragments) {
250
-
const typeChecker = info.languageService.getProgram()?.getTypeChecker();
251
-
imports.forEach(imp => {
252
-
if (!imp.importClause) return;
274
+
const importDiagnostics = checkImportsForFragments(source, info);
254
-
const importedNames: string[] = [];
255
-
if (imp.importClause.name) {
256
-
importedNames.push(imp.importClause?.name.text);
276
+
return [...tsDiagnostics, ...importDiagnostics];
260
-
imp.importClause.namedBindings &&
261
-
ts.isNamespaceImport(imp.importClause.namedBindings)
263
-
// TODO: we might need to warn here when the fragment is unused as a namespace import
266
-
imp.importClause.namedBindings &&
267
-
ts.isNamedImportBindings(imp.importClause.namedBindings)
269
-
imp.importClause.namedBindings.elements.forEach(el => {
270
-
importedNames.push(el.name.text);
279
+
const checkImportsForFragments = (
280
+
source: ts.SourceFile,
281
+
info: ts.server.PluginCreateInfo
283
+
const imports = findAllImports(source);
274
-
const symbol = typeChecker?.getSymbolAtLocation(imp.moduleSpecifier);
275
-
if (!symbol) return;
285
+
const shouldCheckForColocatedFragments =
286
+
info.config.shouldCheckForColocatedFragments ?? false;
287
+
const tsDiagnostics: ts.Diagnostic[] = [];
288
+
if (imports.length && shouldCheckForColocatedFragments) {
289
+
const typeChecker = info.languageService.getProgram()?.getTypeChecker();
290
+
imports.forEach(imp => {
291
+
if (!imp.importClause) return;
277
-
const moduleExports = typeChecker?.getExportsOfModule(symbol);
278
-
if (!moduleExports) return;
293
+
const importedNames: string[] = [];
294
+
if (imp.importClause.name) {
295
+
importedNames.push(imp.importClause?.name.text);
280
-
const missingImports = moduleExports
282
-
if (importedNames.includes(exp.name)) {
299
+
imp.importClause.namedBindings &&
300
+
ts.isNamespaceImport(imp.importClause.namedBindings)
302
+
// TODO: we might need to warn here when the fragment is unused as a namespace import
305
+
imp.importClause.namedBindings &&
306
+
ts.isNamedImportBindings(imp.importClause.namedBindings)
308
+
imp.importClause.namedBindings.elements.forEach(el => {
309
+
importedNames.push(el.name.text);
286
-
const declarations = exp.getDeclarations();
287
-
const declaration = declarations?.find(x => {
288
-
// TODO: check whether the sourceFile.fileName resembles the module
313
+
const symbol = typeChecker?.getSymbolAtLocation(imp.moduleSpecifier);
314
+
if (!symbol) return;
293
-
if (!declaration) return;
316
+
const moduleExports = typeChecker?.getExportsOfModule(symbol);
317
+
if (!moduleExports) return;
295
-
const [template] = findAllTaggedTemplateNodes(declaration);
297
-
let node = template;
299
-
ts.isNoSubstitutionTemplateLiteral(node) ||
300
-
ts.isTemplateExpression(node)
302
-
if (ts.isTaggedTemplateExpression(node.parent)) {
303
-
node = node.parent;
319
+
const missingImports = moduleExports
321
+
if (importedNames.includes(exp.name)) {
309
-
const text = resolveTemplate(
311
-
node.getSourceFile().fileName,
315
-
const parsed = parse(text, { noLocation: true });
317
-
parsed.definitions.every(
318
-
x => x.kind === Kind.FRAGMENT_DEFINITION
321
-
return `'${exp.name}'`;
325
+
const declarations = exp.getDeclarations();
326
+
const declaration = declarations?.find(x => {
327
+
// TODO: check whether the sourceFile.fileName resembles the module
332
+
if (!declaration) return;
334
+
const [template] = findAllTaggedTemplateNodes(declaration);
336
+
let node = template;
338
+
ts.isNoSubstitutionTemplateLiteral(node) ||
339
+
ts.isTemplateExpression(node)
341
+
if (ts.isTaggedTemplateExpression(node.parent)) {
342
+
node = node.parent;
330
-
if (missingImports.length) {
331
-
tsDiagnostics.push({
333
-
length: imp.getText().length,
334
-
start: imp.getStart(),
335
-
category: ts.DiagnosticCategory.Message,
336
-
code: MISSING_FRAGMENT_CODE,
337
-
messageText: `Missing Fragment import(s) ${missingImports.join(
339
-
)} from ${imp.moduleSpecifier.getText()}.`,
348
+
const text = resolveTemplate(
350
+
node.getSourceFile().fileName,
354
+
const parsed = parse(text, { noLocation: true });
356
+
parsed.definitions.every(
357
+
x => x.kind === Kind.FRAGMENT_DEFINITION
360
+
return `'${exp.name}'`;
345
-
cache.set(cacheKey, tsDiagnostics);
369
+
if (missingImports.length) {
370
+
tsDiagnostics.push({
372
+
length: imp.getText().length,
373
+
start: imp.getStart(),
374
+
category: ts.DiagnosticCategory.Message,
375
+
code: MISSING_FRAGMENT_CODE,
376
+
messageText: `Missing Fragment import(s) ${missingImports.join(
378
+
)} from ${imp.moduleSpecifier.getText()}.`,
384
+
return tsDiagnostics;
387
+
const runTypedDocumentNodes = (
388
+
nodes: (ts.TaggedTemplateExpression | ts.NoSubstitutionTemplateLiteral)[],
389
+
texts: (string | undefined)[],
390
+
schema: { current: GraphQLSchema | null },
391
+
diagnostics: ts.Diagnostic[],
392
+
hasTSErrors: boolean,
393
+
baseTypesPath: string,
394
+
sourceFile: ts.SourceFile,
395
+
info: ts.server.PluginCreateInfo
397
+
const filename = sourceFile.fileName;
398
+
const scalars = info.config.scalars || {};
399
+
const disableTypegen = info.config.disableTypegen ?? false;
400
+
let source: ts.SourceFile | undefined = sourceFile;
349
-
!tsDiagnostics.filter(
403
+
!diagnostics.filter(
x.category === ts.DiagnosticCategory.Error ||
x.category === ts.DiagnosticCategory.Warning
···
if (isFileDirty(filename, source) && !isGeneratingTypes) {
358
-
return tsDiagnostics;
isGeneratingTypes = true;
const parts = source.fileName.split('/');
···
isGeneratingTypes = false;
478
-
return tsDiagnostics;