Path: blob/main/extensions/copilot/test/simulation/diagnosticProviders/roslyn.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 * as cp from 'child_process';6import * as fs from 'fs';7import * as path from 'path';8import { TestingCacheSalts } from '../../base/salts';9import { CacheScope } from '../../base/simulationContext';10import { cleanTempDirWithRetry, createTempDir } from '../stestUtil';11import { IFile, ITestDiagnostic } from './diagnosticsProvider';12import { CachingDiagnosticsProvider, findIfInstalled, setupTemporaryWorkspace } from './utils';1314/**15* Class which finds roslyn diagnostics after compilation of C# files16*/17export class RoslynDiagnosticsProvider extends CachingDiagnosticsProvider {1819override readonly id = 'roslyn';20override readonly cacheSalt = TestingCacheSalts.roslynCacheSalt;21override readonly cacheScope = CacheScope.Roslyn;2223private _isInstalled: 'local' | 'docker' | false | undefined;2425private get csprojFile(): string {26return [27'<Project Sdk="Microsoft.NET.Sdk">',28' <PropertyGroup>',29' <OutputType>Library</OutputType>',30' <TargetFramework>net7.0</TargetFramework>',31' <ImplicitUsings>enable</ImplicitUsings>',32' <Nullable>enable</Nullable>',33' <AllowUnsafeBlocks>true</AllowUnsafeBlocks>',34' <ErrorLog>error_list.sarif,version=2.1</ErrorLog>',35' <CodeAnalysisRuleSet>CSharp.ruleset</CodeAnalysisRuleSet>',36' </PropertyGroup>',37' <ItemGroup>',38' <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.2.1"/>',39' <PackageReference Include="System.Runtime.Loader" Version="4.0.0-*"/>',40' </ItemGroup>',41'</Project>',42].join('\n');43}4445private get rulesetFile(): string {46return [47'<?xml version="1.0" encoding="utf-8" ?>',48'<RuleSet Name="CSharp Ruleset" Description="Code analysis rules for CSharp project" ToolsVersion="14.0">',49' <Rules AnalyzerId="Microsoft.CodeAnalysis.CSharp" RuleNamespace="Microsoft.CodeAnalysis.CSharp">',50' <Rule Id="CS8981" Action="None"/>',51' </Rules>',52'</RuleSet>',53].join('\n');54}5556override isInstalled(): boolean {57if (this._isInstalled === undefined) {58if (findIfInstalled({ command: 'dotnet', arguments: ['--version'] }, /^(\d+[\.\d+]*)/g)) {59this._isInstalled = 'local';60} else if (findIfInstalled({ command: 'docker', arguments: ['--version'] }, /\d+\.\d+\.\d+/)) {61this._isInstalled = 'docker';62} else {63this._isInstalled = false;64}65}66return this._isInstalled !== false;67}6869protected override async computeDiagnostics(_files: IFile[]): Promise<ITestDiagnostic[]> {70if (!this.isInstalled()) {71throw new Error('clang or dotnet must be available in this environment for csharp diagnostics.');72}73const temporaryDirectory = await createTempDir();74try {75return await this.runDotnetCompiler(temporaryDirectory, _files);76} finally {77await cleanTempDirWithRetry(temporaryDirectory);78}79}8081private runInDocker(temporaryDirectory: string, basename: string, command: string[]): string {82const args = ['run', '--rm', '-v', `${temporaryDirectory}:/${basename}`, 'mcr.microsoft.com/dotnet/sdk:8.0', ...command];83//console.log('docker ' + args.map(arg => `'${arg}'`).join(' '));84const spawnResult = cp.spawnSync('docker', args, { shell: true, encoding: 'utf-8' });85if (spawnResult.status !== 0) {86throw new Error(`Error while running '${command.join(' ')}' in docker : ${spawnResult.stdout} , ${spawnResult.stderr}`);87}88return spawnResult.stdout;89}9091private runInLocal(command: string[]): string {92const spawnResult = cp.spawnSync(command[0], command.slice(1), { shell: true, encoding: 'utf-8' });93if (spawnResult.status !== 0) {94throw new Error(`Error while running '${command.join(' ')}' in local OS : ${spawnResult.stderr}`);95}96return spawnResult.stdout;97}9899private async runDotnetCompiler(temporaryDirectory: string, files: IFile[]): Promise<ITestDiagnostic[]> {100101const projectName = 'project123';102const projectLocation = path.join(temporaryDirectory, projectName);103await setupTemporaryWorkspace(projectLocation, files);104let outputLocation;105if (this._isInstalled === 'docker') {106const script = [107`set -x`,108`cd "$(dirname "$0")"`, // cd to the directory of the script109`dotnet new classlib --force -o ${projectName}`,110`cp CSharp.ruleset ${projectName}/CSharp.ruleset`,111`cp proj.csproj ${projectName}/${projectName}.csproj`, // replace the csproj file112`dotnet build ${projectName} --no-incremental`,113`cp ${projectName}/error_list.sarif error_list.sarif`, // the error_list.sarif file is generated by dotnet build and has the output114`rm -rfd ${projectName}`, // Docker might run as root, so clean up all generated files right away, we won't be able to do it from the outside115].join('\n');116await fs.promises.writeFile(path.join(temporaryDirectory, `validate.sh`), script);117await fs.promises.writeFile(path.join(temporaryDirectory, `error_list.sarif`), ''); // pre-create the file so it gets the permissions of the current user and we can delete it after118await fs.promises.writeFile(path.join(temporaryDirectory, `CSharp.ruleset`), this.rulesetFile);119await fs.promises.writeFile(path.join(temporaryDirectory, `proj.csproj`), this.csprojFile);120121const basename = path.basename(temporaryDirectory);122this.runInDocker(temporaryDirectory, basename, ['/bin/sh', `/${basename}/validate.sh`]);123outputLocation = temporaryDirectory;124} else {125this.runInLocal(['dotnet', 'new', 'classlib', '--force', '-o', projectLocation]);126await fs.promises.writeFile(path.join(projectLocation, `${projectName}.csproj`), this.csprojFile);127await fs.promises.writeFile(path.join(projectLocation, `CSharp.ruleset`), this.rulesetFile);128this.runInLocal(['dotnet', 'build', projectLocation, '--no-incremental']);129outputLocation = projectLocation;130}131const fileContents = await fs.promises.readFile(path.join(outputLocation, `error_list.sarif`), 'utf8');132const parsedErrors = JSON.parse(fileContents).runs[0].results;133const diagnostics: ITestDiagnostic[] = [];134for (const error of parsedErrors) {135const uri = error.locations[0].physicalLocation.artifactLocation.uri;136const diagnostic = {137file: path.basename(uri),138message: error.message.text,139code: error.ruleId,140startLine: error.locations[0].physicalLocation.region.startLine - 1,141startCharacter: error.locations[0].physicalLocation.region.startColumn - 1,142endLine: error.locations[0].physicalLocation.region.endLine - 1,143endCharacter: error.locations[0].physicalLocation.region.endColumn - 1,144relatedInformation: undefined,145source: 'roslyn'146};147diagnostics.push(diagnostic);148}149return diagnostics;150}151}152153154