forked from golang/vscode-go
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgoCover.ts
564 lines (513 loc) · 17.6 KB
/
goCover.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
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
/* eslint-disable no-useless-escape */
/* eslint-disable no-prototype-builtins */
/* eslint-disable @typescript-eslint/no-explicit-any */
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See LICENSE in the project root for license information.
*--------------------------------------------------------*/
'use strict';
import fs = require('fs');
import path = require('path');
import vscode = require('vscode');
import { CommandFactory } from './commands';
import { getGoConfig } from './config';
import { isModSupported } from './goModules';
import { getImportPathToFolder } from './goPackages';
import { getTestFlags, goTest, showTestOutput, TestConfig } from './testUtils';
import { fixDriveCasingInWindows } from './utils/pathUtils';
let gutterSvgs: { [key: string]: string };
interface Highlight {
top: vscode.TextEditorDecorationType;
mid: vscode.TextEditorDecorationType;
bot: vscode.TextEditorDecorationType;
all: vscode.TextEditorDecorationType;
}
let decorators: {
type: 'highlight' | 'gutter';
coveredGutter: vscode.TextEditorDecorationType;
uncoveredGutter: vscode.TextEditorDecorationType;
coveredHighlight: Highlight;
uncoveredHighlight: Highlight;
};
let decoratorConfig: {
[key: string]: any;
type: 'highlight' | 'gutter';
coveredHighlightColor: string;
uncoveredHighlightColor: string;
coveredBorderColor: string;
uncoveredBorderColor: string;
coveredGutterStyle: string;
uncoveredGutterStyle: string;
};
// a list of modified, unsaved go files with actual code edits (rather than comment edits)
let modifiedFiles: {
[key: string]: boolean;
} = {};
/**
* Initializes the decorators used for Code coverage.
* @param ctx The extension context
*/
export function initCoverageDecorators(ctx: vscode.ExtensionContext) {
// Initialize gutter svgs
gutterSvgs = {
blockred: ctx.asAbsolutePath('media/gutter-blockred.svg'),
blockgreen: ctx.asAbsolutePath('media/gutter-blockgreen.svg'),
blockblue: ctx.asAbsolutePath('media/gutter-blockblue.svg'),
blockyellow: ctx.asAbsolutePath('media/gutter-blockyellow.svg'),
slashred: ctx.asAbsolutePath('media/gutter-slashred.svg'),
slashgreen: ctx.asAbsolutePath('media/gutter-slashgreen.svg'),
slashblue: ctx.asAbsolutePath('media/gutter-slashblue.svg'),
slashyellow: ctx.asAbsolutePath('media/gutter-slashyellow.svg'),
verticalred: ctx.asAbsolutePath('media/gutter-vertred.svg'),
verticalgreen: ctx.asAbsolutePath('media/gutter-vertgreen.svg'),
verticalblue: ctx.asAbsolutePath('media/gutter-vertblue.svg'),
verticalyellow: ctx.asAbsolutePath('media/gutter-vertyellow.svg')
};
const goConfig = getGoConfig();
updateCodeCoverageDecorators(goConfig.get('coverageDecorator'));
}
/**
* Updates the decorators used for Code coverage.
* @param coverageDecoratorConfig The coverage decorated as configured by the user
*/
export function updateCodeCoverageDecorators(coverageDecoratorConfig: any) {
// These defaults are chosen to be distinguishable in nearly any color scheme (even Red)
// as well as by people who have difficulties with color perception.
// It appears that the contributions in package.json are only used to check what users
// put in settings.json, while the defaults come from the defaults section of
// go.coverageDecorator in package.json.
decoratorConfig = {
type: 'highlight',
coveredHighlightColor: 'rgba(64,128,128,0.5)',
coveredBorderColor: 'rgba(64,128,128,1.0)',
uncoveredHighlightColor: 'rgba(128,64,64,0.25)',
uncoveredBorderColor: 'rgba(128,64,64,1.0)',
coveredGutterStyle: 'blockblue',
uncoveredGutterStyle: 'slashyellow'
};
// Update from configuration.
if (typeof coverageDecoratorConfig !== 'object') {
vscode.window.showWarningMessage("invalid go.coverageDecorator type, expected an 'object'");
} else {
for (const k in coverageDecoratorConfig) {
if (coverageDecoratorConfig.hasOwnProperty(k)) {
decoratorConfig[k] = coverageDecoratorConfig[k];
} else {
vscode.window.showWarningMessage(`invalid coverage parameter ${k}`);
}
}
}
setDecorators();
vscode.window.visibleTextEditors.forEach(applyCodeCoverage);
}
function setDecorators() {
disposeDecorators();
if (!decorators) {
initForTest();
} // only happens in tests
const f = (x: { overviewRulerColor: string; backgroundColor: string }, arg: string) => {
const y = {
overviewRulerLane: 2,
borderStyle: arg,
borderWidth: '2px'
};
return Object.assign(y, x);
};
const cov = {
overviewRulerColor: 'green',
backgroundColor: decoratorConfig.coveredHighlightColor,
borderColor: decoratorConfig.coveredBorderColor
};
const uncov = {
overviewRulerColor: 'red',
backgroundColor: decoratorConfig.uncoveredHighlightColor,
borderColor: decoratorConfig.uncoveredBorderColor
};
const ctop = f(cov, 'solid solid none solid');
const cmid = f(cov, 'none solid none solid');
const cbot = f(cov, 'none solid solid solid');
const cone = f(cov, 'solid solid solid solid');
const utop = f(uncov, 'solid solid none solid');
const umid = f(uncov, 'none solid none solid');
const ubot = f(uncov, 'none solid solid solid');
const uone = f(uncov, 'solid solid solid solid');
decorators = {
type: decoratorConfig.type,
coveredGutter: vscode.window.createTextEditorDecorationType({
gutterIconPath: gutterSvgs[decoratorConfig.coveredGutterStyle]
}),
uncoveredGutter: vscode.window.createTextEditorDecorationType({
gutterIconPath: gutterSvgs[decoratorConfig.uncoveredGutterStyle]
}),
coveredHighlight: {
all: vscode.window.createTextEditorDecorationType(cone),
top: vscode.window.createTextEditorDecorationType(ctop),
mid: vscode.window.createTextEditorDecorationType(cmid),
bot: vscode.window.createTextEditorDecorationType(cbot)
},
uncoveredHighlight: {
all: vscode.window.createTextEditorDecorationType(uone),
top: vscode.window.createTextEditorDecorationType(utop),
mid: vscode.window.createTextEditorDecorationType(umid),
bot: vscode.window.createTextEditorDecorationType(ubot)
}
};
}
/**
* Disposes decorators so that the current coverage is removed from the editor.
*/
function disposeDecorators() {
if (decorators) {
decorators.coveredGutter.dispose();
decorators.uncoveredGutter.dispose();
decorators.coveredHighlight.all.dispose();
decorators.coveredHighlight.top.dispose();
decorators.coveredHighlight.mid.dispose();
decorators.coveredHighlight.bot.dispose();
decorators.uncoveredHighlight.all.dispose();
decorators.uncoveredHighlight.top.dispose();
decorators.uncoveredHighlight.mid.dispose();
decorators.uncoveredHighlight.bot.dispose();
}
}
interface CoverageData {
uncoveredOptions: vscode.DecorationOptions[];
coveredOptions: vscode.DecorationOptions[];
}
let coverageData: { [key: string]: CoverageData } = {}; // actual file path to the coverage data.
let isCoverageApplied = false;
function emptyCoverageData(): CoverageData {
return {
uncoveredOptions: [],
coveredOptions: []
};
}
/**
* Clear the coverage on all files
*/
function clearCoverage() {
coverageData = {};
disposeDecorators();
isCoverageApplied = false;
}
/**
* Extract the coverage data from the given cover profile & apply them on the files in the open editors.
* @param coverProfilePath Path to the file that has the cover profile data
* @param packageDirPath Absolute path of the package for which the coverage was calculated
* @param dir Directory to execute go list in
*/
export function applyCodeCoverageToAllEditors(coverProfilePath: string, dir?: string): Promise<void> {
const v = new Promise<void>((resolve, reject) => {
try {
const showCounts = getGoConfig().get('coverShowCounts') as boolean;
const coveragePath = new Map<string, CoverageData>(); // <filename> from the cover profile to the coverage data.
// Clear existing coverage files
clearCoverage();
// collect the packages named in the coverage file
const seenPaths = new Set<string>();
// for now read synchronously and hope for no errors
const contents = fs.readFileSync(coverProfilePath).toString();
contents.split('\n').forEach((line) => {
// go test coverageprofile generates output:
// filename:StartLine.StartColumn,EndLine.EndColumn Hits CoverCount
// where the filename is either the import path + '/' + base file name, or
// the actual file path (either absolute or starting with .)
// See https://golang.org/issues/40251.
//
// The first line will be like "mode: set" which we will ignore.
// TODO: port https://golang.org/cl/179377 for faster parsing.
const parse = line.match(/^(\S+)\:(\d+)\.(\d+)\,(\d+)\.(\d+)\s(\d+)\s(\d+)/);
if (!parse) {
return;
}
let filename = parse[1];
if (filename.startsWith('.' + path.sep)) {
// If it's a relative file path, convert it to an absolute path.
// From now on, we can assume that it's a real file name if it is
// an absolute path.
filename = path.resolve(filename);
}
// If this is not a real file name, that's package_path + file name,
// Record it in seenPaths for `go list` call to resolve package path ->
// directory mapping.
if (!path.isAbsolute(filename)) {
const lastSlash = filename.lastIndexOf('/');
if (lastSlash !== -1) {
seenPaths.add(filename.slice(0, lastSlash));
}
}
// and fill in coveragePath
const coverage = coveragePath.get(parse[1]) || emptyCoverageData();
const range = new vscode.Range(
// Convert lines and columns to 0-based
parseInt(parse[2], 10) - 1,
parseInt(parse[3], 10) - 1,
parseInt(parse[4], 10) - 1,
parseInt(parse[5], 10) - 1
);
const counts = parseInt(parse[7], 10);
// If is Covered (CoverCount > 0)
if (counts > 0) {
coverage.coveredOptions.push(...elaborate(range, counts, showCounts));
} else {
coverage.uncoveredOptions.push(...elaborate(range, counts, showCounts));
}
coveragePath.set(filename, coverage);
});
getImportPathToFolder([...seenPaths], dir).then((pathsToDirs) => {
createCoverageData(pathsToDirs, coveragePath);
setDecorators();
vscode.window.visibleTextEditors.forEach(applyCodeCoverage);
resolve();
});
} catch (e) {
vscode.window.showInformationMessage((e as any).msg);
reject(e);
}
});
return v;
}
// add decorations to the range
function elaborate(r: vscode.Range, count: number, showCounts: boolean): vscode.DecorationOptions[] {
// irrelevant for "gutter"
if (!decorators || decorators.type === 'gutter') {
return [{ range: r }];
}
const ans: vscode.DecorationOptions[] = [];
const dc = decoratorConfig;
const backgroundColor = [dc.uncoveredHighlightColor, dc.coveredHighlightColor];
const txt: vscode.ThemableDecorationAttachmentRenderOptions = {
contentText: count > 0 && showCounts ? `--${count}--` : '',
backgroundColor: backgroundColor[count === 0 ? 0 : 1]
};
const v: vscode.DecorationOptions = {
range: r,
hoverMessage: `${count} executions`,
renderOptions: {
before: txt
}
};
ans.push(v);
return ans;
}
function createCoverageData(pathsToDirs: Map<string, string>, coveragePath: Map<string, CoverageData>) {
coveragePath.forEach((cd, ip) => {
if (path.isAbsolute(ip)) {
setCoverageDataByFilePath(ip, cd);
return;
}
const lastSlash = ip.lastIndexOf('/');
if (lastSlash === -1) {
setCoverageDataByFilePath(ip, cd);
return;
}
const maybePkgPath = ip.slice(0, lastSlash);
const fileDir = pathsToDirs.get(maybePkgPath) || path.resolve(maybePkgPath);
const file = fileDir + path.sep + ip.slice(lastSlash + 1);
setCoverageDataByFilePath(file, cd);
});
}
/**
* Set the object that holds the coverage data for given file path.
* @param filePath
* @param data
*/
function setCoverageDataByFilePath(filePath: string, data: CoverageData) {
if (filePath.startsWith('_')) {
filePath = filePath.substr(1);
}
if (process.platform === 'win32') {
const parts = filePath.split('/');
if (parts.length) {
filePath = parts.join(path.sep);
}
}
coverageData[filePath] = data;
}
/**
* Apply the code coverage highlighting in given editor
* @param editor
*/
export function applyCodeCoverage(editor: vscode.TextEditor | undefined) {
if (!editor || editor.document.languageId !== 'go' || editor.document.fileName.endsWith('_test.go')) {
return;
}
let doc = editor.document.fileName;
if (path.isAbsolute(doc)) {
doc = fixDriveCasingInWindows(doc);
}
const cfg = getGoConfig(editor.document.uri);
const coverageOptions = cfg['coverageOptions'];
for (const filename in coverageData) {
if (doc !== fixDriveCasingInWindows(filename)) {
continue;
}
isCoverageApplied = true;
const cd = coverageData[filename];
if (coverageOptions === 'showCoveredCodeOnly' || coverageOptions === 'showBothCoveredAndUncoveredCode') {
if (decorators.type === 'gutter') {
editor.setDecorations(decorators.coveredGutter, cd.coveredOptions);
} else {
detailed(editor, decorators.coveredHighlight, cd.coveredOptions);
}
}
if (coverageOptions === 'showUncoveredCodeOnly' || coverageOptions === 'showBothCoveredAndUncoveredCode') {
if (decorators.type === 'gutter') {
editor.setDecorations(decorators.uncoveredGutter, cd.uncoveredOptions);
} else {
detailed(editor, decorators.uncoveredHighlight, cd.uncoveredOptions);
}
}
}
}
function detailed(editor: vscode.TextEditor, h: Highlight, opts: vscode.DecorationOptions[]) {
const tops: vscode.DecorationOptions[] = [];
const mids: vscode.DecorationOptions[] = [];
const bots: vscode.DecorationOptions[] = [];
const alls: vscode.DecorationOptions[] = [];
opts.forEach((opt) => {
const r = opt.range;
if (r.start.line === r.end.line) {
alls.push(opt);
return;
}
for (let line = r.start.line; line <= r.end.line; line++) {
if (line === r.start.line) {
const use: vscode.DecorationOptions = {
range: editor.document.validateRange(
new vscode.Range(line, r.start.character, line, Number.MAX_SAFE_INTEGER)
),
hoverMessage: opt.hoverMessage,
renderOptions: opt.renderOptions
};
tops.push(use);
} else if (line < r.end.line) {
const use = {
range: editor.document.validateRange(new vscode.Range(line, 0, line, Number.MAX_SAFE_INTEGER)),
hoverMessage: opt.hoverMessage
};
mids.push(use);
} else {
const use = {
range: new vscode.Range(line, 0, line, r.end.character),
hoverMessage: opt.hoverMessage
};
bots.push(use);
}
}
});
if (tops.length > 0) {
editor.setDecorations(h.top, tops);
}
if (mids.length > 0) {
editor.setDecorations(h.mid, mids);
}
if (bots.length > 0) {
editor.setDecorations(h.bot, bots);
}
if (alls.length > 0) {
editor.setDecorations(h.all, alls);
}
}
/**
* Listener for file save that clears potential stale coverage data.
* Local cache tracks files with changes outside of comments to determine
* files for which the save event can cause stale coverage data.
* @param e TextDocument
*/
export function removeCodeCoverageOnFileSave(e: vscode.TextDocument) {
if (e.languageId !== 'go' || !isCoverageApplied) {
return;
}
if (vscode.window.visibleTextEditors.every((editor) => editor.document !== e)) {
return;
}
if (modifiedFiles[e.fileName]) {
clearCoverage();
modifiedFiles = {}; // reset the list of modified files
}
}
/**
* Listener for file change that tracks files with changes outside of comments
* to determine files for which an eventual save can cause stale coverage data.
* @param e TextDocumentChangeEvent
*/
export function trackCodeCoverageRemovalOnFileChange(e: vscode.TextDocumentChangeEvent) {
if (e.document.languageId !== 'go' || !e.contentChanges.length || !isCoverageApplied) {
return;
}
if (vscode.window.visibleTextEditors.every((editor) => editor.document !== e.document)) {
return;
}
if (isPartOfComment(e)) {
return;
}
modifiedFiles[e.document.fileName] = true;
}
/**
* If current editor has Code coverage applied, then remove it.
* Else run tests to get the coverage and apply.
*/
export const toggleCoverageCurrentPackage: CommandFactory = () => async () => {
const editor = vscode.window.activeTextEditor;
if (!editor) {
vscode.window.showInformationMessage('No editor is active.');
return;
}
if (isCoverageApplied) {
clearCoverage();
return;
}
const goConfig = getGoConfig();
const cwd = path.dirname(editor.document.uri.fsPath);
const testFlags = getTestFlags(goConfig);
const isMod = await isModSupported(editor.document.uri);
const testConfig: TestConfig = {
goConfig,
dir: cwd,
flags: testFlags,
background: true,
isMod,
applyCodeCoverage: true
};
return goTest(testConfig).then((success) => {
if (!success) {
showTestOutput();
}
});
};
export function isPartOfComment(e: vscode.TextDocumentChangeEvent): boolean {
return e.contentChanges.every((change) => {
// We cannot be sure with using just regex on individual lines whether a multi line change is part of a comment or not
// So play it safe and treat it as not a comment
if (!change.range.isSingleLine || change.text.includes('\n')) {
return false;
}
const text = e.document.lineAt(change.range.start).text;
const idx = text.search('//');
return idx > -1 && idx <= change.range.start.character;
});
}
// These routines enable testing without starting an editing session.
export function coverageFilesForTest(): { [key: string]: CoverageData } {
return coverageData;
}
export function initForTest() {
if (!decoratorConfig) {
// this code is unnecessary except for testing, where there may be no workspace
// nor the normal flow of initializations
const x = 'rgba(0,0,0,0)';
if (!gutterSvgs) {
gutterSvgs = { x };
}
decoratorConfig = {
type: 'highlight',
coveredHighlightColor: x,
uncoveredHighlightColor: x,
coveredBorderColor: x,
uncoveredBorderColor: x,
coveredGutterStyle: x,
uncoveredGutterStyle: x
};
}
}