This repository was archived by the owner on May 4, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathdiagnostics.ts
307 lines (279 loc) · 10.2 KB
/
diagnostics.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
import {getLanguageForDocument} from '../utils/fileUtils';
import * as rosieClient from "../rosie/rosieClient";
import {
DIAGNOSTIC_SOURCE,
TIME_BEFORE_STARTING_ANALYSIS_MILLISECONDS,
} from "../constants";
import { Language } from "../graphql-api/types";
import { RosieFix } from "../rosie/rosieTypes";
import {
GRAPHQL_LANGUAGE_TO_ROSIE_LANGUAGE,
ROSIE_SEVERITY_CRITICAL,
ROSIE_SEVERITY_ERROR,
ROSIE_SEVERITY_WARNING,
} from "../rosie/rosieConstants";
import { getRulesFromCache } from "../rosie/rosieCache";
import {URI} from "vscode-uri";
import { Range, Position, Diagnostic, DiagnosticSeverity } from 'vscode-languageserver-types';
import { DocumentUri, TextDocument } from 'vscode-languageserver-textdocument';
//import * as console from '../utils/connectionLogger';
const DIAGNOSTICS_TIMESTAMP: Map<string, number> = new Map();
const FIXES_BY_DOCUMENT: Map<
DocumentUri,
//Uses a string key (the JSON stringified Range) because Map.has() works based on === equality.
// Having Range as key sometimes resulted in the same range added multiple times with the same
// fixes in 'registerFixForDocument'.
Map<string, [Range, RosieFix[]]>
> = new Map();
/**
* This function is a helper for the quick fixes. It retrieves the quickfix for a
* violation. We register the list of fixes when we analyze. Then, when the user
* hovers a quick fix, we get the list of quick fixes using this function.
*
* @param documentUri - the URI of the VS Code document
* @param range - the range we are at in the document
* @returns - the list of fixes for the given range
*/
export const getFixesForDocument = (
documentUri: DocumentUri,
range: Range
): RosieFix[] => {
const fixesForDocument = FIXES_BY_DOCUMENT.get(documentUri);
const result: RosieFix[] = [];
if (fixesForDocument) {
for (const rangeAndFixes of fixesForDocument.values()) {
if (contains(rangeAndFixes[0], range)) {
rangeAndFixes[1]?.forEach((f) => result.push(f));
}
}
}
return result;
};
/**
* Validates whether on Range or Position contains another one.
* This is a replacement for vscode.Range.contains() as vscode-languageserver doesn't have
* a corresponding logic or method.
*
* The implementation is adopted from https://github.com/microsoft/vscode/blob/main/src/vs/workbench/api/common/extHostTypes.ts.
*
* Exported for testing purposes.
*
* @param container the Range/Position that should contain 'containee'
* @param containee the Range/Position that should be contained by 'container'
*/
export const contains = (container: Range | Position, containee: Range | Position): boolean => {
if (Range.is(container) && Range.is(containee)) {
return contains(container, containee.start) && contains(container, containee.end);
}
if (Range.is(container) && Position.is(containee)) {
return !(isBefore(Position.create(containee.line, containee.character), container.start) || isBefore(container.end, containee));
}
return false;
};
/**
* Returns whether the 'first' position is located before the 'second' one.
*
* @param first a position
* @param second another position
*/
const isBefore = (first: Position, second: Position): boolean => {
if (first.line < second.line) {
return true;
}
if (second.line < first.line) {
return false;
}
return first.character < second.character;
};
/**
* Register a fix for a document and a range. When we analyze the file,
* we store all the quick fixes in a Map so that we can retrieve them
* later when the user hover the fixes.
*
* It makes sure that no duplicate ranges, and no duplicate fixes are added.
*
* Exported for testing purposes.
*
* @param documentUri - the URI of the analyzed VS Code document
* @param range - the range we are at in the document
* @param fix - the quick fix to register for this document and range
*/
export const registerFixForDocument = (
documentUri: DocumentUri,
range: Range,
fix: RosieFix
): void => {
// If there is no range or fix saved for this document, save the document
if (!FIXES_BY_DOCUMENT.has(documentUri)) {
FIXES_BY_DOCUMENT.set(documentUri, new Map());
}
// Query the ranges saved for this document, and if the currently inspected range is not saved,
// associate an empty list of fixes to it. Otherwise, add the fix for this range.
const rangeAndFixesForDocument = FIXES_BY_DOCUMENT.get(documentUri);
const rangeString = JSON.stringify(range);
if (!rangeAndFixesForDocument?.has(rangeString)) {
rangeAndFixesForDocument?.set(rangeString, [range, []]);
}
if (rangeAndFixesForDocument?.get(rangeString)) {
// @ts-ignore
const fixesForRange = rangeAndFixesForDocument?.get(rangeString)[1];
// If the fix hasn't been added to this range, add it.
if (fixesForRange?.filter(f => JSON.stringify(f) === JSON.stringify(fix)).length === 0) {
fixesForRange?.push(fix);
}
}
};
/**
* Reset the quick fixes for a document. When we start another analysis, we reset
* the list of fixes to only have a short list of quick fixes.
*
* @param documentUri - the URI of the VS Code document
*/
const resetFixesForDocument = (documentUri: DocumentUri): void => {
FIXES_BY_DOCUMENT.set(documentUri, new Map());
};
/**
* Clears all documents and fixes. Only for testing purposes.
*/
export const resetFixes = (): void => {
FIXES_BY_DOCUMENT.clear();
};
/**
* This function is here to check when we should (or not)
* inspect a document. It checks that there was not another
* request for inspection within TIME_BEFORE_STARTING_ANALYSIS_MILLISECONDS
* and if not, trigger an analysis.
*
* @param doc - the document we are trying to update
* @returns - if we should run the analysis or not
*/
const shouldProceed = async (doc: TextDocument): Promise<boolean> => {
const filename = URI.parse(doc.uri).toString();
const currentTimestampMs = Date.now();
/**
* Set the timestamp in a hashmap so that other thread
* and analysis request can see it.
*/
DIAGNOSTICS_TIMESTAMP.set(filename, currentTimestampMs);
/**
* Wait for some time. During that time, the user
* might type another key that trigger other analysis
* (and will update the hashmap).
*/
await new Promise((r) =>
setTimeout(r, TIME_BEFORE_STARTING_ANALYSIS_MILLISECONDS)
);
/**
* Get the actual timeout in the hashmap. It might have
* changed since we sleep and therefore, take tha latest
* value.
*/
const actualTimeoutMs = DIAGNOSTICS_TIMESTAMP.get(filename);
/**
* check that the actual latest value is the one we called
* the function with. If yes, let's go!
*/
return actualTimeoutMs === currentTimestampMs;
};
/**
* Maps the argument Rosie severity to the LSP specific DiagnosticSeverity,
* to have squiggles with proper severities displayed in the editor.
*
* @param rosieSeverity the severity to map
*/
const mapRosieSeverityToLSPSeverity = (
rosieSeverity: string
): DiagnosticSeverity => {
if (rosieSeverity.toLocaleUpperCase() === ROSIE_SEVERITY_CRITICAL) {
return DiagnosticSeverity.Error;
}
if (rosieSeverity.toLocaleUpperCase() === ROSIE_SEVERITY_ERROR
|| rosieSeverity.toLocaleUpperCase() === ROSIE_SEVERITY_WARNING) {
return DiagnosticSeverity.Warning;
}
return DiagnosticSeverity.Information;
};
/**
* Analyses the argument document and updates/overwrites the diagnostics for that document.
* This in turn updates the displayed squiggles in the editor.
*
* No update happens when
* <ul>
* <li>The language of the document is unknown.</li>
* <li>The language of the document is not supported by Rosie.</li>
* <li>The user hasn't finished typing for at least 500ms.</li>
* <li>The document is empty.</li>
* <li>The document has less than 2 lines.</li>
* <li>There is no rule cached for the current document's language.</li>
* </ul>
*
* @param doc the currently analysed document
* @param sendDiagnostics the callback to send the diagnostics to the client
*/
export async function refreshDiagnostics(doc: TextDocument, sendDiagnostics: (diagnostics: Diagnostic[]) => Promise<void>): Promise<void> {
const language: Language = getLanguageForDocument(doc);
if (language === Language.Unknown) {
return;
}
const supportedLanguages = Array.from(
GRAPHQL_LANGUAGE_TO_ROSIE_LANGUAGE.keys()
);
if (supportedLanguages.indexOf(language) === -1) {
return;
}
/**
* We do not proceed yet, we make sure the user is done typing some text
*/
const shouldDoAnalysis = await shouldProceed(doc);
if (!shouldDoAnalysis) {
return;
}
if (doc.getText().length === 0) {
// console.log("empty code");
return;
}
if (doc.lineCount < 2) {
// console.log("not enough lines");
return;
}
const rules = getRulesFromCache(doc);
// Empty the mapping between the analysis and the list of fixes
resetFixesForDocument(doc.uri);
if (rules && rules.length > 0) {
const ruleResponses = await rosieClient.getRuleResponses(doc, rules);
const diags: Diagnostic[] = [];
ruleResponses.forEach((ruleResponse) => {
// console.log(`Response took ${ruleResponse.executionTimeMs} ms`);
ruleResponse.violations.forEach((violation) => {
const range = Range.create(
Position.create(violation.start.line - 1, violation.start.col - 1),
Position.create(violation.end.line - 1, violation.end.col - 1)
);
const diag: Diagnostic = {
range: range,
message: violation.message,
severity: mapRosieSeverityToLSPSeverity(violation.severity),
//For example, the 'source', 'code' and 'codeDescription' are displayed like
// this in VS Code: <source>(<code>), e.g. Codiga(rulesetname/ruleset), where
// the value in parentheses can be clicked to open the 'codeDescription.href' URL in a browser.
source: DIAGNOSTIC_SOURCE,
code: ruleResponse.identifier,
codeDescription: {
//The URL to open the rule in the browser
href: `https://app.codiga.io/hub/ruleset/${ruleResponse.identifier}`
}
};
if (violation.fixes) {
violation.fixes.forEach((fix) => {
registerFixForDocument(doc.uri, range, fix);
});
}
diags.push(diag);
});
});
sendDiagnostics(diags);
} else {
// console.log("no ruleset to use");
sendDiagnostics([]);
}
}