Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/test/simulation/diagnosticProviders/utils.ts
13394 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as cp from 'child_process';
7
import { spawnSync } from 'child_process';
8
import * as fs from 'fs';
9
import * as path from 'path';
10
import type * as vscode from 'vscode';
11
import { ITestingServicesAccessor } from '../../../src/platform/test/node/services';
12
import { ResourceMap } from '../../../src/util/vs/base/common/map';
13
import { Diagnostic, DiagnosticRelatedInformation, Location, Range } from '../../../src/vscodeTypes';
14
import { computeSHA256 } from '../../base/hash';
15
import { CacheScope, ICachingResourceFetcher } from '../../base/simulationContext';
16
import { CACHING_DIAGNOSTICS_PROVIDER_CACHE_SALT } from '../../cacheSalt';
17
import { cleanTempDirWithRetry, createTempDir } from '../stestUtil';
18
import { DiagnosticsProvider, IFile, ITestDiagnostic } from './diagnosticsProvider';
19
20
/**
21
* Abstract class which finds diagnostics for a set of files and stores them in a cache path
22
*/
23
export abstract class CachingDiagnosticsProvider extends DiagnosticsProvider {
24
protected readonly id = this.constructor.name;
25
26
abstract readonly cacheSalt: string;
27
abstract readonly cacheScope: CacheScope;
28
29
protected get cacheVersion(): number { return CACHING_DIAGNOSTICS_PROVIDER_CACHE_SALT; }
30
31
override async getDiagnostics(accessor: ITestingServicesAccessor, files: IFile[]): Promise<ITestDiagnostic[]> {
32
// Always use / as separators in file names to avoid cache misses on Windows
33
files = files.map(f => ({ ...f, fileName: f.fileName.replace(/\\/g, '/') }));
34
35
// Keep files stable and maximize cache hits by sorting them by file name
36
files.sort((a, b) => a.fileName.localeCompare(b.fileName));
37
38
const cacheKey = computeSHA256(`${this.id}-v${this.cacheVersion}-${JSON.stringify(files)}`);
39
40
return await accessor.get(ICachingResourceFetcher).invokeWithCache(
41
this.cacheScope,
42
files,
43
this.cacheSalt,
44
cacheKey,
45
this.computeDiagnostics.bind(this)
46
);
47
}
48
49
protected abstract computeDiagnostics(files: IFile[]): Promise<ITestDiagnostic[]>;
50
}
51
52
/**
53
* Abstract class for defining diagnostics provider which provide linting errors
54
*/
55
export abstract class LintingDiagnosticsProvider extends CachingDiagnosticsProvider {
56
57
protected override async computeDiagnostics(_files: IFile[]): Promise<ITestDiagnostic[]> {
58
const temporaryDirectory = await createTempDir();
59
const diagnostics: ITestDiagnostic[] = [];
60
const files = await setupTemporaryWorkspace(temporaryDirectory, _files);
61
for (const file of files) {
62
const command = await this.fetchCommand(temporaryDirectory, file.filePath);
63
const spawnResult = spawnSync(command.command, command.arguments, { shell: true, encoding: 'utf-8', env: command.env });
64
const processedDiagnostics = this.processDiagnostics(file.fileName, JSON.parse(spawnResult.stdout || '{}'));
65
diagnostics.push(...processedDiagnostics);
66
}
67
await cleanTempDirWithRetry(temporaryDirectory);
68
return diagnostics;
69
}
70
71
protected abstract fetchCommand(temporaryDirectory: string, filePath: string): Promise<{ command: string; arguments: string[]; env?: NodeJS.ProcessEnv }>;
72
73
protected abstract processDiagnostics(fileName: string, stdoutResult: any): ITestDiagnostic[];
74
}
75
76
export async function setupTemporaryWorkspace(workspacePath: string, _files: IFile[]): Promise<{ filePath: string; fileName: string; fileContents: string }[]> {
77
const files = _files.map((file) => {
78
return {
79
filePath: path.join(workspacePath, file.fileName),
80
fileName: file.fileName,
81
fileContents: file.fileContents
82
};
83
});
84
await fs.promises.rm(workspacePath, { recursive: true, force: true });
85
await fs.promises.mkdir(workspacePath, { recursive: true });
86
for (const file of files) {
87
await fs.promises.mkdir(path.dirname(file.filePath), { recursive: true });
88
await fs.promises.writeFile(file.filePath, file.fileContents);
89
}
90
return files;
91
}
92
93
export function convertTestToVSCodeDiagnostics(diagnostics: ITestDiagnostic[], pathToUri: (path: string) => vscode.Uri): ResourceMap<vscode.Diagnostic[]> {
94
const result = new ResourceMap<vscode.Diagnostic[]>();
95
for (const d of diagnostics) {
96
const diagnostic = new Diagnostic(new Range(d.startLine, d.startCharacter, d.endLine, d.endCharacter), d.message);
97
diagnostic.code = d.code;
98
diagnostic.source = d.source;
99
diagnostic.relatedInformation = d.relatedInformation?.map(r => {
100
const range = new Range(r.location.startLine, r.location.startCharacter, r.location.endLine, r.location.endCharacter);
101
const relatedLocation = new Location(pathToUri(r.location.file), range);
102
return new DiagnosticRelatedInformation(relatedLocation, r.message);
103
});
104
const uri = pathToUri(d.file);
105
if (!result.has(uri)) {
106
result.set(uri, []);
107
}
108
result.get(uri)!.push(diagnostic);
109
}
110
return result;
111
}
112
113
export function findIfInstalled(verificationCommand: { command: string; arguments: string[] }, verificationRegex: RegExp): boolean {
114
const spawnResult = cp.spawnSync(verificationCommand.command, verificationCommand.arguments, { shell: true, encoding: 'utf-8' });
115
const regexMatch = spawnResult.stdout.match(verificationRegex);
116
if (!regexMatch || regexMatch.length === 0) {
117
return false;
118
}
119
return true;
120
}
121
122