···
21
+
import { LRUCache } from 'lru-cache';
22
+
import fnv1a from '@sindresorhus/fnv1a';
···
let isGeneratingTypes = false;
40
+
const cache = new LRUCache<number, ts.Diagnostic[]>({
41
+
// how long to live in ms
42
+
ttl: 1000 * 60 * 15,
export function getGraphQLDiagnostics(
// This is so that we don't change offsets when there are
44
-
schema: { current: GraphQLSchema | null },
52
+
schema: { current: GraphQLSchema | null; version: number },
info: ts.server.PluginCreateInfo
): ts.Diagnostic[] | undefined {
const logger = (msg: string) =>
···
return resolveTemplate(node, filename, info).combinedText;
72
-
const diagnostics = nodes
73
-
.map(originalNode => {
74
-
let node = originalNode;
75
-
if (isNoSubstitutionTemplateLiteral(node) || isTemplateExpression(node)) {
76
-
if (isTaggedTemplateExpression(node.parent)) {
80
+
let tsDiagnostics: ts.Diagnostic[] = [];
81
+
const cacheKey = fnv1a(texts.join('-') + schema.version);
82
+
if (cache.has(cacheKey)) {
83
+
tsDiagnostics = cache.get(cacheKey)!;
85
+
const diagnostics = nodes
86
+
.map(originalNode => {
87
+
let node = originalNode;
89
+
isNoSubstitutionTemplateLiteral(node) ||
90
+
isTemplateExpression(node)
92
+
if (isTaggedTemplateExpression(node.parent)) {
83
-
const { combinedText: text, resolvedSpans } = resolveTemplate(
88
-
const lines = text.split('\n');
99
+
const { combinedText: text, resolvedSpans } = resolveTemplate(
104
+
const lines = text.split('\n');
90
-
let isExpression = false;
91
-
if (isAsExpression(node.parent)) {
92
-
if (isExpressionStatement(node.parent.parent)) {
93
-
isExpression = true;
106
+
let isExpression = false;
107
+
if (isAsExpression(node.parent)) {
108
+
if (isExpressionStatement(node.parent.parent)) {
109
+
isExpression = true;
112
+
if (isExpressionStatement(node.parent)) {
113
+
isExpression = true;
96
-
if (isExpressionStatement(node.parent)) {
97
-
isExpression = true;
100
-
// When we are dealing with a plain gql statement we have to add two these can be recognised
101
-
// by the fact that the parent is an expressionStatement
102
-
let startingPosition =
103
-
node.pos + (tagTemplate.length + (isExpression ? 2 : 1));
104
-
const endPosition = startingPosition + node.getText().length;
105
-
const graphQLDiagnostics = getDiagnostics(text, schema.current)
107
-
const { start, end } = x.range;
116
+
// When we are dealing with a plain gql statement we have to add two these can be recognised
117
+
// by the fact that the parent is an expressionStatement
118
+
let startingPosition =
119
+
node.pos + (tagTemplate.length + (isExpression ? 2 : 1));
120
+
const endPosition = startingPosition + node.getText().length;
121
+
const graphQLDiagnostics = getDiagnostics(text, schema.current)
123
+
const { start, end } = x.range;
109
-
// We add the start.line to account for newline characters which are
111
-
let startChar = startingPosition + start.line;
112
-
for (let i = 0; i <= start.line; i++) {
113
-
if (i === start.line) startChar += start.character;
114
-
else startChar += lines[i].length;
125
+
// We add the start.line to account for newline characters which are
127
+
let startChar = startingPosition + start.line;
128
+
for (let i = 0; i <= start.line; i++) {
129
+
if (i === start.line) startChar += start.character;
130
+
else startChar += lines[i].length;
117
-
let endChar = startingPosition + end.line;
118
-
for (let i = 0; i <= end.line; i++) {
119
-
if (i === end.line) endChar += end.character;
120
-
else endChar += lines[i].length;
133
+
let endChar = startingPosition + end.line;
134
+
for (let i = 0; i <= end.line; i++) {
135
+
if (i === end.line) endChar += end.character;
136
+
else endChar += lines[i].length;
123
-
const locatedInFragment = resolvedSpans.find(x => {
124
-
const newEnd = x.new.start + x.new.length;
125
-
return startChar >= x.new.start && endChar <= newEnd;
139
+
const locatedInFragment = resolvedSpans.find(x => {
140
+
const newEnd = x.new.start + x.new.length;
141
+
return startChar >= x.new.start && endChar <= newEnd;
128
-
if (!!locatedInFragment) {
131
-
start: locatedInFragment.original.start,
132
-
length: locatedInFragment.original.length,
135
-
if (startChar > endPosition) {
136
-
// we have to calculate the added length and fix this
137
-
const addedCharacters = resolvedSpans
138
-
.filter(x => x.new.start + x.new.length < startChar)
140
-
(acc, span) => acc + (span.new.length - span.original.length),
143
-
startChar = startChar - addedCharacters;
144
-
endChar = endChar - addedCharacters;
144
+
if (!!locatedInFragment) {
147
-
start: startChar + 1,
148
-
length: endChar - startChar,
147
+
start: locatedInFragment.original.start,
148
+
length: locatedInFragment.original.length,
153
-
start: startChar + 1,
154
-
length: endChar - startChar,
151
+
if (startChar > endPosition) {
152
+
// we have to calculate the added length and fix this
153
+
const addedCharacters = resolvedSpans
154
+
.filter(x => x.new.start + x.new.length < startChar)
157
+
acc + (span.new.length - span.original.length),
160
+
startChar = startChar - addedCharacters;
161
+
endChar = endChar - addedCharacters;
164
+
start: startChar + 1,
165
+
length: endChar - startChar,
170
+
start: startChar + 1,
171
+
length: endChar - startChar,
159
-
.filter(x => x.start + x.length <= endPosition);
176
+
.filter(x => x.start + x.length <= endPosition);
162
-
const parsed = parse(text, { noLocation: true });
179
+
const parsed = parse(text, { noLocation: true });
165
-
parsed.definitions.some(x => x.kind === Kind.OPERATION_DEFINITION)
167
-
const op = parsed.definitions.find(
168
-
x => x.kind === Kind.OPERATION_DEFINITION
169
-
) as OperationDefinitionNode;
171
-
graphQLDiagnostics.push({
172
-
message: 'Operation needs a name for types to be generated.',
174
-
code: MISSING_OPERATION_NAME_CODE,
175
-
length: originalNode.getText().length,
182
+
parsed.definitions.some(x => x.kind === Kind.OPERATION_DEFINITION)
184
+
const op = parsed.definitions.find(
185
+
x => x.kind === Kind.OPERATION_DEFINITION
186
+
) as OperationDefinitionNode;
188
+
graphQLDiagnostics.push({
189
+
message: 'Operation needs a name for types to be generated.',
191
+
code: MISSING_OPERATION_NAME_CODE,
192
+
length: originalNode.getText().length,
183
-
return graphQLDiagnostics;
186
-
.filter(Boolean) as Array<Diagnostic & { length: number; start: number }>;
188
-
const tsDiagnostics: ts.Diagnostic[] = diagnostics.map(diag => ({
190
-
length: diag.length,
193
-
diag.severity === 2
194
-
? ts.DiagnosticCategory.Warning
195
-
: ts.DiagnosticCategory.Error,
197
-
typeof diag.code === 'number'
199
-
: diag.severity === 2
200
-
? USING_DEPRECATED_FIELD_CODE
201
-
: SEMANTIC_DIAGNOSTIC_CODE,
202
-
messageText: diag.message.split('\n')[0],
200
+
return graphQLDiagnostics;
203
+
.filter(Boolean) as Array<Diagnostic & { length: number; start: number }>;
205
-
const imports = findAllImports(source);
206
-
if (imports.length && shouldCheckForColocatedFragments) {
207
-
const typeChecker = info.languageService.getProgram()?.getTypeChecker();
208
-
imports.forEach(imp => {
209
-
if (!imp.importClause) return;
205
+
tsDiagnostics = diagnostics.map(diag => ({
207
+
length: diag.length,
210
+
diag.severity === 2
211
+
? ts.DiagnosticCategory.Warning
212
+
: ts.DiagnosticCategory.Error,
214
+
typeof diag.code === 'number'
216
+
: diag.severity === 2
217
+
? USING_DEPRECATED_FIELD_CODE
218
+
: SEMANTIC_DIAGNOSTIC_CODE,
219
+
messageText: diag.message.split('\n')[0],
211
-
const importedNames: string[] = [];
212
-
if (imp.importClause.name) {
213
-
importedNames.push(imp.importClause?.name.text);
222
+
const imports = findAllImports(source);
223
+
if (imports.length && shouldCheckForColocatedFragments) {
224
+
const typeChecker = info.languageService.getProgram()?.getTypeChecker();
225
+
imports.forEach(imp => {
226
+
if (!imp.importClause) return;
217
-
imp.importClause.namedBindings &&
218
-
isNamespaceImport(imp.importClause.namedBindings)
220
-
// TODO: we might need to warn here when the fragment is unused as a namespace import
223
-
imp.importClause.namedBindings &&
224
-
isNamedImportBindings(imp.importClause.namedBindings)
226
-
imp.importClause.namedBindings.elements.forEach(el => {
227
-
importedNames.push(el.name.text);
228
+
const importedNames: string[] = [];
229
+
if (imp.importClause.name) {
230
+
importedNames.push(imp.importClause?.name.text);
231
-
const symbol = typeChecker?.getSymbolAtLocation(imp.moduleSpecifier);
232
-
if (!symbol) return;
234
+
imp.importClause.namedBindings &&
235
+
isNamespaceImport(imp.importClause.namedBindings)
237
+
// TODO: we might need to warn here when the fragment is unused as a namespace import
240
+
imp.importClause.namedBindings &&
241
+
isNamedImportBindings(imp.importClause.namedBindings)
243
+
imp.importClause.namedBindings.elements.forEach(el => {
244
+
importedNames.push(el.name.text);
234
-
const moduleExports = typeChecker?.getExportsOfModule(symbol);
235
-
if (!moduleExports) return;
248
+
const symbol = typeChecker?.getSymbolAtLocation(imp.moduleSpecifier);
249
+
if (!symbol) return;
237
-
const missingImports = moduleExports
239
-
if (importedNames.includes(exp.name)) {
251
+
const moduleExports = typeChecker?.getExportsOfModule(symbol);
252
+
if (!moduleExports) return;
243
-
const declarations = exp.getDeclarations();
244
-
const declaration = declarations?.find(x => {
245
-
// TODO: check whether the sourceFile.fileName resembles the module
254
+
const missingImports = moduleExports
256
+
if (importedNames.includes(exp.name)) {
250
-
if (!declaration) return;
260
+
const declarations = exp.getDeclarations();
261
+
const declaration = declarations?.find(x => {
262
+
// TODO: check whether the sourceFile.fileName resembles the module
252
-
const [template] = findAllTaggedTemplateNodes(declaration);
254
-
let node = template;
256
-
isNoSubstitutionTemplateLiteral(node) ||
257
-
isTemplateExpression(node)
259
-
if (isTaggedTemplateExpression(node.parent)) {
260
-
node = node.parent;
267
+
if (!declaration) return;
266
-
const text = resolveTemplate(
268
-
node.getSourceFile().fileName,
272
-
const parsed = parse(text, { noLocation: true });
269
+
const [template] = findAllTaggedTemplateNodes(declaration);
271
+
let node = template;
274
-
parsed.definitions.every(
275
-
x => x.kind === Kind.FRAGMENT_DEFINITION
273
+
isNoSubstitutionTemplateLiteral(node) ||
274
+
isTemplateExpression(node)
278
-
return `'${exp.name}'`;
276
+
if (isTaggedTemplateExpression(node.parent)) {
277
+
node = node.parent;
283
+
const text = resolveTemplate(
285
+
node.getSourceFile().fileName,
289
+
const parsed = parse(text, { noLocation: true });
291
+
parsed.definitions.every(
292
+
x => x.kind === Kind.FRAGMENT_DEFINITION
295
+
return `'${exp.name}'`;
287
-
if (missingImports.length) {
288
-
// TODO: we could use getCodeFixesAtPosition
289
-
// to build on this
290
-
tsDiagnostics.push({
292
-
length: imp.getText().length,
293
-
start: imp.getStart(),
294
-
category: ts.DiagnosticCategory.Message,
295
-
code: MISSING_FRAGMENT_CODE,
296
-
messageText: `Missing Fragment import(s) ${missingImports.join(
298
-
)} from ${imp.moduleSpecifier.getText()}.`,
304
+
if (missingImports.length) {
305
+
// TODO: we could use getCodeFixesAtPosition
306
+
// to build on this
307
+
tsDiagnostics.push({
309
+
length: imp.getText().length,
310
+
start: imp.getStart(),
311
+
category: ts.DiagnosticCategory.Message,
312
+
code: MISSING_FRAGMENT_CODE,
313
+
messageText: `Missing Fragment import(s) ${missingImports.join(
315
+
)} from ${imp.moduleSpecifier.getText()}.`,
321
+
cache.set(cacheKey, tsDiagnostics);