Path: blob/main/extensions/copilot/test/simulation/diagnosticProviders/python.ts
13394 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import assert from 'assert';6import * as cp from 'child_process';7import * as fs from 'fs';8import { tmpdir } from 'os';9import path from 'path';10import { promisify } from 'util';11import { ITestingServicesAccessor } from '../../../src/platform/test/node/services';12import { generateUuid } from '../../../src/util/vs/base/common/uuid';13import { Range } from '../../../src/util/vs/workbench/api/common/extHostTypes/range';14import { computeSHA256 } from '../../base/hash';15import { TestingCacheSalts } from '../../base/salts';16import { CacheScope, ICachingResourceFetcher } from '../../base/simulationContext';17import { PYTHON_EXECUTES_WITHOUT_ERRORS, PYTHON_VALID_SYNTAX_CACHE_SALT } from '../../cacheSalt';18import { ITestDiagnostic } from './diagnosticsProvider';19import { LintingDiagnosticsProvider } from './utils';2021/**22* Class which finds Pyright diagnostics23*/24export class PyrightDiagnosticsProvider extends LintingDiagnosticsProvider {2526override readonly id = 'pyright';27override readonly cacheSalt = TestingCacheSalts.pyrightCacheSalt;28override readonly cacheScope = CacheScope.Pyright;2930protected override async fetchCommand(temporaryDirectory: string, filePath: string) {31const configPyrightFile = path.join(temporaryDirectory, 'pyrightconfig.json');32await fs.promises.writeFile(configPyrightFile, JSON.stringify({}));33const virtualEnvironment = ensurePythonVEnv();34if (!virtualEnvironment) {35// throw36throw new Error('No virtual environment found');37}3839return {40command: virtualEnvironment.pythonInterpreter,41arguments: ['-m', 'pyright', '--project', configPyrightFile, '--outputjson', filePath],42env: virtualEnvironment.env43};44}4546protected override processDiagnostics(fileName: string, stdoutResult: any): ITestDiagnostic[] {47const generalDiagnostics = stdoutResult.generalDiagnostics;48assert(Array.isArray(generalDiagnostics));49const diagnostics = [];50for (const diagnostic of generalDiagnostics) {51const range = diagnostic.range;52const message = diagnostic.message;53assert(Range.isRange(range) && typeof message === 'string');54diagnostics.push({55file: fileName,56startLine: range.start.line,57startCharacter: range.start.character,58endLine: range.end.line,59endCharacter: range.end.character,60message: message,61code: undefined,62relatedInformation: undefined,63source: 'pyright'64});65}66return diagnostics;67}68}6970/**71* Class which finds Pylint diagnostics72*/73export class PylintDiagnosticsProvider extends LintingDiagnosticsProvider {7475override readonly id = 'pylint';76override readonly cacheSalt = TestingCacheSalts.pylintCacheSalt;77override readonly cacheScope = CacheScope.Pylint;7879private get pylintConfigFile(): string {80const pylintConfigFile = [81`[MESSAGES CONTROL]`,82`disable=W0311, C0115, C0305, C0116, C0114, C0304, C0103, W0108`83].join(`\n`);84return pylintConfigFile;85}8687protected override async fetchCommand(temporaryDirectory: string, filePath: string) {88const configPylintFile = path.join(temporaryDirectory, '.pylintrc');89await fs.promises.writeFile(configPylintFile, this.pylintConfigFile);90const virtualEnvironment = ensurePythonVEnv();91if (!virtualEnvironment) {92throw new Error('No virtual environment found');93}9495return {96command: virtualEnvironment.pythonInterpreter,97arguments: ['-m', 'pylint', '--rcfile', configPylintFile, '--output-format', 'json', filePath]98};99}100101protected override processDiagnostics(fileName: string, stdoutResult: any): ITestDiagnostic[] {102const diagnostics = [];103assert(Array.isArray(stdoutResult));104if (stdoutResult.length === 0) {105return [];106}107for (const stdout of stdoutResult) {108const message = stdout.message;109const line = stdout.line;110const column = stdout.column;111const endLine = stdout.endLine ?? null;112const endColumn = stdout.endColumn ?? null;113const code = stdout['message-id'] ?? null;114assert(115typeof message === 'string'116&& typeof line === 'number'117&& typeof column === 'number'118&& (typeof endLine === 'number' || endColumn === null)119&& (typeof endColumn === 'number' || endColumn === null)120);121diagnostics.push({122file: fileName,123startLine: line - 1,124startCharacter: column,125endLine: (endLine ?? line) - 1,126endCharacter: (endColumn ?? column),127message: message,128code: code,129relatedInformation: undefined,130source: 'pylint'131});132}133return diagnostics;134}135}136137138export async function isValidPythonFile(accessor: ITestingServicesAccessor, text: string): Promise<boolean> {139// Remove lines that start with `%xyz` as they can be cell magics in Jupyter Notebooks140// & that doesn't work in a standalone Python file141text = text.split(/\r?\n/g).filter(line => !line.startsWith('%')).join('\n');142const cacheKey = computeSHA256(`python-v2${PYTHON_VALID_SYNTAX_CACHE_SALT}-${text}`);143return accessor.get(ICachingResourceFetcher).invokeWithCache(144CacheScope.Python,145text,146TestingCacheSalts.pythonCacheSalt,147cacheKey,148doIsValidPythonFile149);150}151152async function doIsValidPythonFile(text: string): Promise<boolean> {153const fileName = `python-verify_${computeSHA256(`python-v${PYTHON_VALID_SYNTAX_CACHE_SALT}-${text}`)}.py`;154const dir = path.join(tmpdir(), generateUuid());155const tmpFile = path.join(dir, fileName);156await promisify(fs.mkdir)(dir, { recursive: true });157await promisify(fs.writeFile)(tmpFile, text);158return new Promise<boolean>((resolve) => {159cp.exec(`python3 -m py_compile "${tmpFile}"`, (error, stdout, stderr) => {160if (error) {161return resolve(false);162} else if (stderr && stderr.length > 0) {163return resolve(false);164}165166resolve(true);167});168}).finally(() => {169fs.rm(dir, { recursive: true, force: true }, () => { });170});171}172173export async function canExecutePythonCodeWithoutErrors(accessor: ITestingServicesAccessor, text: string): Promise<boolean> {174// Remove lines that start with `%xyz` as they can be cell magics in Jupyter Notebooks175// & that doesn't work in a standalone Python file176text = text.split(/\r?\n/g).filter(line => !line.startsWith('%')).join('\n');177const cacheKey = computeSHA256(`python-verify-execution_${PYTHON_EXECUTES_WITHOUT_ERRORS}-${text}`);178return accessor.get(ICachingResourceFetcher).invokeWithCache(179CacheScope.Python,180text,181TestingCacheSalts.pythonCacheSalt,182cacheKey,183canExecutePythonCodeWithoutErrorsImpl184);185}186187async function canExecutePythonCodeWithoutErrorsImpl(text: string): Promise<boolean> {188const fileName = `python-verify-execution_${computeSHA256(`python-v${PYTHON_EXECUTES_WITHOUT_ERRORS}-${text}`)}.py`;189const dir = path.join(tmpdir(), generateUuid());190const tmpFile = path.join(dir, fileName);191await promisify(fs.mkdir)(dir, { recursive: true });192await promisify(fs.writeFile)(tmpFile, text);193return new Promise<boolean>((resolve) => {194cp.exec(`python3 "${tmpFile}"`, (error, stdout, stderr) => {195if (error) {196return resolve(false);197} else if (stderr && stderr.length > 0) {198return resolve(false);199}200201resolve(true);202});203}).finally(() => {204fs.rm(dir, { recursive: true, force: true }, () => { });205});206}207208export function ensurePythonVEnv(): { pythonInterpreter: string; env: NodeJS.ProcessEnv } | undefined {209const repoRoot = path.join(__dirname, '../');210const isWindows = process.platform === 'win32';211const envBinFolder = path.join(repoRoot, '.venv', isWindows ? 'Scripts' : 'bin');212const p = path.join(envBinFolder, isWindows ? 'python.exe' : 'python');213214for (let i = 0; i < 2; i++) {215try {216assert(fs.existsSync(p));217218const envs = Object.assign({}, process.env);219envs.PATH = `${envBinFolder}${path.delimiter}${envs.PATH}`;220envs.Path = `${envBinFolder}${path.delimiter}${envs.Path}`;221222return {223pythonInterpreter: p,224env: envs225};226} catch (err) {227if (!err.stack.includes('AssertionError')) {228throw err;229}230231cp.execSync(`npm run create_venv`, { stdio: 'inherit' });232}233}234235throw new Error('Python virtual environment not found, create it manually with `npm run create_venv`');236}237238239