Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/test/simulation/diagnosticProviders/python.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 assert from 'assert';
7
import * as cp from 'child_process';
8
import * as fs from 'fs';
9
import { tmpdir } from 'os';
10
import path from 'path';
11
import { promisify } from 'util';
12
import { ITestingServicesAccessor } from '../../../src/platform/test/node/services';
13
import { generateUuid } from '../../../src/util/vs/base/common/uuid';
14
import { Range } from '../../../src/util/vs/workbench/api/common/extHostTypes/range';
15
import { computeSHA256 } from '../../base/hash';
16
import { TestingCacheSalts } from '../../base/salts';
17
import { CacheScope, ICachingResourceFetcher } from '../../base/simulationContext';
18
import { PYTHON_EXECUTES_WITHOUT_ERRORS, PYTHON_VALID_SYNTAX_CACHE_SALT } from '../../cacheSalt';
19
import { ITestDiagnostic } from './diagnosticsProvider';
20
import { LintingDiagnosticsProvider } from './utils';
21
22
/**
23
* Class which finds Pyright diagnostics
24
*/
25
export class PyrightDiagnosticsProvider extends LintingDiagnosticsProvider {
26
27
override readonly id = 'pyright';
28
override readonly cacheSalt = TestingCacheSalts.pyrightCacheSalt;
29
override readonly cacheScope = CacheScope.Pyright;
30
31
protected override async fetchCommand(temporaryDirectory: string, filePath: string) {
32
const configPyrightFile = path.join(temporaryDirectory, 'pyrightconfig.json');
33
await fs.promises.writeFile(configPyrightFile, JSON.stringify({}));
34
const virtualEnvironment = ensurePythonVEnv();
35
if (!virtualEnvironment) {
36
// throw
37
throw new Error('No virtual environment found');
38
}
39
40
return {
41
command: virtualEnvironment.pythonInterpreter,
42
arguments: ['-m', 'pyright', '--project', configPyrightFile, '--outputjson', filePath],
43
env: virtualEnvironment.env
44
};
45
}
46
47
protected override processDiagnostics(fileName: string, stdoutResult: any): ITestDiagnostic[] {
48
const generalDiagnostics = stdoutResult.generalDiagnostics;
49
assert(Array.isArray(generalDiagnostics));
50
const diagnostics = [];
51
for (const diagnostic of generalDiagnostics) {
52
const range = diagnostic.range;
53
const message = diagnostic.message;
54
assert(Range.isRange(range) && typeof message === 'string');
55
diagnostics.push({
56
file: fileName,
57
startLine: range.start.line,
58
startCharacter: range.start.character,
59
endLine: range.end.line,
60
endCharacter: range.end.character,
61
message: message,
62
code: undefined,
63
relatedInformation: undefined,
64
source: 'pyright'
65
});
66
}
67
return diagnostics;
68
}
69
}
70
71
/**
72
* Class which finds Pylint diagnostics
73
*/
74
export class PylintDiagnosticsProvider extends LintingDiagnosticsProvider {
75
76
override readonly id = 'pylint';
77
override readonly cacheSalt = TestingCacheSalts.pylintCacheSalt;
78
override readonly cacheScope = CacheScope.Pylint;
79
80
private get pylintConfigFile(): string {
81
const pylintConfigFile = [
82
`[MESSAGES CONTROL]`,
83
`disable=W0311, C0115, C0305, C0116, C0114, C0304, C0103, W0108`
84
].join(`\n`);
85
return pylintConfigFile;
86
}
87
88
protected override async fetchCommand(temporaryDirectory: string, filePath: string) {
89
const configPylintFile = path.join(temporaryDirectory, '.pylintrc');
90
await fs.promises.writeFile(configPylintFile, this.pylintConfigFile);
91
const virtualEnvironment = ensurePythonVEnv();
92
if (!virtualEnvironment) {
93
throw new Error('No virtual environment found');
94
}
95
96
return {
97
command: virtualEnvironment.pythonInterpreter,
98
arguments: ['-m', 'pylint', '--rcfile', configPylintFile, '--output-format', 'json', filePath]
99
};
100
}
101
102
protected override processDiagnostics(fileName: string, stdoutResult: any): ITestDiagnostic[] {
103
const diagnostics = [];
104
assert(Array.isArray(stdoutResult));
105
if (stdoutResult.length === 0) {
106
return [];
107
}
108
for (const stdout of stdoutResult) {
109
const message = stdout.message;
110
const line = stdout.line;
111
const column = stdout.column;
112
const endLine = stdout.endLine ?? null;
113
const endColumn = stdout.endColumn ?? null;
114
const code = stdout['message-id'] ?? null;
115
assert(
116
typeof message === 'string'
117
&& typeof line === 'number'
118
&& typeof column === 'number'
119
&& (typeof endLine === 'number' || endColumn === null)
120
&& (typeof endColumn === 'number' || endColumn === null)
121
);
122
diagnostics.push({
123
file: fileName,
124
startLine: line - 1,
125
startCharacter: column,
126
endLine: (endLine ?? line) - 1,
127
endCharacter: (endColumn ?? column),
128
message: message,
129
code: code,
130
relatedInformation: undefined,
131
source: 'pylint'
132
});
133
}
134
return diagnostics;
135
}
136
}
137
138
139
export async function isValidPythonFile(accessor: ITestingServicesAccessor, text: string): Promise<boolean> {
140
// Remove lines that start with `%xyz` as they can be cell magics in Jupyter Notebooks
141
// & that doesn't work in a standalone Python file
142
text = text.split(/\r?\n/g).filter(line => !line.startsWith('%')).join('\n');
143
const cacheKey = computeSHA256(`python-v2${PYTHON_VALID_SYNTAX_CACHE_SALT}-${text}`);
144
return accessor.get(ICachingResourceFetcher).invokeWithCache(
145
CacheScope.Python,
146
text,
147
TestingCacheSalts.pythonCacheSalt,
148
cacheKey,
149
doIsValidPythonFile
150
);
151
}
152
153
async function doIsValidPythonFile(text: string): Promise<boolean> {
154
const fileName = `python-verify_${computeSHA256(`python-v${PYTHON_VALID_SYNTAX_CACHE_SALT}-${text}`)}.py`;
155
const dir = path.join(tmpdir(), generateUuid());
156
const tmpFile = path.join(dir, fileName);
157
await promisify(fs.mkdir)(dir, { recursive: true });
158
await promisify(fs.writeFile)(tmpFile, text);
159
return new Promise<boolean>((resolve) => {
160
cp.exec(`python3 -m py_compile "${tmpFile}"`, (error, stdout, stderr) => {
161
if (error) {
162
return resolve(false);
163
} else if (stderr && stderr.length > 0) {
164
return resolve(false);
165
}
166
167
resolve(true);
168
});
169
}).finally(() => {
170
fs.rm(dir, { recursive: true, force: true }, () => { });
171
});
172
}
173
174
export async function canExecutePythonCodeWithoutErrors(accessor: ITestingServicesAccessor, text: string): Promise<boolean> {
175
// Remove lines that start with `%xyz` as they can be cell magics in Jupyter Notebooks
176
// & that doesn't work in a standalone Python file
177
text = text.split(/\r?\n/g).filter(line => !line.startsWith('%')).join('\n');
178
const cacheKey = computeSHA256(`python-verify-execution_${PYTHON_EXECUTES_WITHOUT_ERRORS}-${text}`);
179
return accessor.get(ICachingResourceFetcher).invokeWithCache(
180
CacheScope.Python,
181
text,
182
TestingCacheSalts.pythonCacheSalt,
183
cacheKey,
184
canExecutePythonCodeWithoutErrorsImpl
185
);
186
}
187
188
async function canExecutePythonCodeWithoutErrorsImpl(text: string): Promise<boolean> {
189
const fileName = `python-verify-execution_${computeSHA256(`python-v${PYTHON_EXECUTES_WITHOUT_ERRORS}-${text}`)}.py`;
190
const dir = path.join(tmpdir(), generateUuid());
191
const tmpFile = path.join(dir, fileName);
192
await promisify(fs.mkdir)(dir, { recursive: true });
193
await promisify(fs.writeFile)(tmpFile, text);
194
return new Promise<boolean>((resolve) => {
195
cp.exec(`python3 "${tmpFile}"`, (error, stdout, stderr) => {
196
if (error) {
197
return resolve(false);
198
} else if (stderr && stderr.length > 0) {
199
return resolve(false);
200
}
201
202
resolve(true);
203
});
204
}).finally(() => {
205
fs.rm(dir, { recursive: true, force: true }, () => { });
206
});
207
}
208
209
export function ensurePythonVEnv(): { pythonInterpreter: string; env: NodeJS.ProcessEnv } | undefined {
210
const repoRoot = path.join(__dirname, '../');
211
const isWindows = process.platform === 'win32';
212
const envBinFolder = path.join(repoRoot, '.venv', isWindows ? 'Scripts' : 'bin');
213
const p = path.join(envBinFolder, isWindows ? 'python.exe' : 'python');
214
215
for (let i = 0; i < 2; i++) {
216
try {
217
assert(fs.existsSync(p));
218
219
const envs = Object.assign({}, process.env);
220
envs.PATH = `${envBinFolder}${path.delimiter}${envs.PATH}`;
221
envs.Path = `${envBinFolder}${path.delimiter}${envs.Path}`;
222
223
return {
224
pythonInterpreter: p,
225
env: envs
226
};
227
} catch (err) {
228
if (!err.stack.includes('AssertionError')) {
229
throw err;
230
}
231
232
cp.execSync(`npm run create_venv`, { stdio: 'inherit' });
233
}
234
}
235
236
throw new Error('Python virtual environment not found, create it manually with `npm run create_venv`');
237
}
238
239