diff --git a/src/OmniSharp.Abstractions/Models/v1/AutoComplete/AutoCompleteResponse.cs b/src/OmniSharp.Abstractions/Models/v1/AutoComplete/AutoCompleteResponse.cs index 52effc260e..802f52b723 100644 --- a/src/OmniSharp.Abstractions/Models/v1/AutoComplete/AutoCompleteResponse.cs +++ b/src/OmniSharp.Abstractions/Models/v1/AutoComplete/AutoCompleteResponse.cs @@ -8,6 +8,7 @@ public class AutoCompleteResponse /// public string CompletionText { get; set; } public string Description { get; set; } + /// /// The text that should be displayed in the auto-complete UI. /// diff --git a/src/OmniSharp.Abstractions/Models/v1/Diagnostics/DiagnosticLocation.cs b/src/OmniSharp.Abstractions/Models/v1/Diagnostics/DiagnosticLocation.cs index ebe5574921..8ed274166b 100644 --- a/src/OmniSharp.Abstractions/Models/v1/Diagnostics/DiagnosticLocation.cs +++ b/src/OmniSharp.Abstractions/Models/v1/Diagnostics/DiagnosticLocation.cs @@ -1,8 +1,28 @@ -namespace OmniSharp.Models.Diagnostics +using System.Collections.Generic; + +namespace OmniSharp.Models.Diagnostics { public class DiagnosticLocation : QuickFix { public string LogLevel { get; set; } public string Id { get; set; } + + public override bool Equals(object obj) + { + var location = obj as DiagnosticLocation; + return location != null && + base.Equals(obj) && + LogLevel == location.LogLevel && + Id == location.Id; + } + + public override int GetHashCode() + { + var hashCode = -1670479257; + hashCode = hashCode * -1521134295 + base.GetHashCode(); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(LogLevel); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Id); + return hashCode; + } } } diff --git a/src/OmniSharp.Abstractions/Models/v1/Diagnostics/DiagnosticResult.cs b/src/OmniSharp.Abstractions/Models/v1/Diagnostics/DiagnosticResult.cs index 2429f4801b..07d2f4ce12 100644 --- a/src/OmniSharp.Abstractions/Models/v1/Diagnostics/DiagnosticResult.cs +++ b/src/OmniSharp.Abstractions/Models/v1/Diagnostics/DiagnosticResult.cs @@ -6,5 +6,10 @@ public class DiagnosticResult { public string FileName { get; set; } public IEnumerable QuickFixes { get; set; } + + public override string ToString() + { + return $"{FileName} -> {string.Join(", ", QuickFixes)}"; + } } -} \ No newline at end of file +} diff --git a/src/OmniSharp.Abstractions/Models/v1/QuickFix.cs b/src/OmniSharp.Abstractions/Models/v1/QuickFix.cs index 70ec20d4b2..39bd7541ca 100644 --- a/src/OmniSharp.Abstractions/Models/v1/QuickFix.cs +++ b/src/OmniSharp.Abstractions/Models/v1/QuickFix.cs @@ -49,7 +49,7 @@ public override int GetHashCode() } public override string ToString() - => $"({Line}:{Column}) - ({EndLine}:{EndColumn})"; + => $"{Text} ({Line}:{Column}) - ({EndLine}:{EndColumn})"; public bool Contains(int line, int column) { diff --git a/src/OmniSharp.Host/CompositionHostBuilder.cs b/src/OmniSharp.Host/CompositionHostBuilder.cs index a80af3f10a..2fccf0e9d6 100644 --- a/src/OmniSharp.Host/CompositionHostBuilder.cs +++ b/src/OmniSharp.Host/CompositionHostBuilder.cs @@ -4,6 +4,7 @@ using System.Composition.Hosting.Core; using System.Linq; using System.Reflection; +using Microsoft.CodeAnalysis; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -43,6 +44,7 @@ public CompositionHost Build() var memoryCache = _serviceProvider.GetRequiredService(); var loggerFactory = _serviceProvider.GetRequiredService(); var assemblyLoader = _serviceProvider.GetRequiredService(); + var analyzerAssemblyLoader = _serviceProvider.GetRequiredService(); var environment = _serviceProvider.GetRequiredService(); var eventEmitter = _serviceProvider.GetRequiredService(); var dotNetCliService = _serviceProvider.GetRequiredService(); @@ -74,6 +76,7 @@ public CompositionHost Build() .WithProvider(MefValueProvider.From(options.CurrentValue)) .WithProvider(MefValueProvider.From(options.CurrentValue.FormattingOptions)) .WithProvider(MefValueProvider.From(assemblyLoader)) + .WithProvider(MefValueProvider.From(analyzerAssemblyLoader)) .WithProvider(MefValueProvider.From(dotNetCliService)) .WithProvider(MefValueProvider.From(metadataHelper)) .WithProvider(MefValueProvider.From(msbuildLocator)) @@ -122,6 +125,7 @@ public static IServiceProvider CreateDefaultServiceProvider( // Caching services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddOptions(); services.AddSingleton(); diff --git a/src/OmniSharp.Host/Services/AssemblyLoader.cs b/src/OmniSharp.Host/Services/AssemblyLoader.cs index 2766d1b4bd..c843f9ac02 100644 --- a/src/OmniSharp.Host/Services/AssemblyLoader.cs +++ b/src/OmniSharp.Host/Services/AssemblyLoader.cs @@ -5,10 +5,11 @@ using System.IO; using System.Reflection; using Microsoft.Extensions.Logging; +using Microsoft.CodeAnalysis; namespace OmniSharp.Services { - internal class AssemblyLoader : IAssemblyLoader + internal class AssemblyLoader : IAssemblyLoader, IAnalyzerAssemblyLoader { private static readonly ConcurrentDictionary AssemblyCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); private readonly ILogger _logger; @@ -18,6 +19,11 @@ public AssemblyLoader(ILoggerFactory loggerFactory) _logger = loggerFactory.CreateLogger(); } + public void AddDependencyLocation(string fullPath) + { + LoadFrom(fullPath); + } + public Assembly Load(AssemblyName name) { Assembly result = null; @@ -90,5 +96,10 @@ public Assembly LoadFrom(string assemblyPath, bool dontLockAssemblyOnDisk = fals _logger.LogTrace($"Assembly loaded from path: {assemblyPath}"); return assembly; } + + public Assembly LoadFromPath(string fullPath) + { + return LoadFrom(fullPath); + } } } diff --git a/src/OmniSharp.Http/HttpCommandLineApplication.cs b/src/OmniSharp.Http/HttpCommandLineApplication.cs index fea8c8bfdc..b76bb7e327 100644 --- a/src/OmniSharp.Http/HttpCommandLineApplication.cs +++ b/src/OmniSharp.Http/HttpCommandLineApplication.cs @@ -14,7 +14,6 @@ public HttpCommandLineApplication() : base() _serverInterface = Application.Option("-i | --interface", "Server interface address (defaults to 'localhost').", CommandOptionType.SingleValue); } - public int Port => _port.GetValueOrDefault(2000); public string Interface => _serverInterface.GetValueOrDefault("localhost"); } diff --git a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.ProjectData.cs b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.ProjectData.cs index 52e834ac23..da513e023b 100644 --- a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.ProjectData.cs +++ b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.ProjectData.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.IO; +using System.Linq; using System.Runtime.Versioning; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -46,6 +47,7 @@ private class ProjectData public ImmutableArray References { get; } public ImmutableArray PackageReferences { get; } public ImmutableArray Analyzers { get; } + public RuleSet RuleSet { get; } public ImmutableDictionary ReferenceAliases { get; } private ProjectData() @@ -78,7 +80,8 @@ private ProjectData( ImmutableArray preprocessorSymbolNames, ImmutableArray suppressedDiagnosticIds, bool signAssembly, - string assemblyOriginatorKeyFile) + string assemblyOriginatorKeyFile, + RuleSet ruleset) : this() { Guid = guid; @@ -105,6 +108,7 @@ private ProjectData( SignAssembly = signAssembly; AssemblyOriginatorKeyFile = assemblyOriginatorKeyFile; + RuleSet = ruleset; } private ProjectData( @@ -128,10 +132,11 @@ private ProjectData( ImmutableArray references, ImmutableArray packageReferences, ImmutableArray analyzers, + RuleSet ruleset, ImmutableDictionary referenceAliases) : this(guid, name, assemblyName, targetPath, outputPath, intermediateOutputPath, projectAssetsFile, configuration, platform, targetFramework, targetFrameworks, outputKind, languageVersion, nullableContextOptions, allowUnsafeCode, - documentationFile, preprocessorSymbolNames, suppressedDiagnosticIds, signAssembly, assemblyOriginatorKeyFile) + documentationFile, preprocessorSymbolNames, suppressedDiagnosticIds, signAssembly, assemblyOriginatorKeyFile, ruleset) { SourceFiles = sourceFiles.EmptyIfDefault(); ProjectReferences = projectReferences.EmptyIfDefault(); @@ -176,7 +181,7 @@ public static ProjectData Create(MSB.Evaluation.Project project) return new ProjectData( guid, name, assemblyName, targetPath, outputPath, intermediateOutputPath, projectAssetsFile, configuration, platform, targetFramework, targetFrameworks, outputKind, languageVersion, nullableContextOptions, allowUnsafeCode, - documentationFile, preprocessorSymbolNames, suppressedDiagnosticIds, signAssembly, assemblyOriginatorKeyFile); + documentationFile, preprocessorSymbolNames, suppressedDiagnosticIds, signAssembly, assemblyOriginatorKeyFile, ruleset: null); } public static ProjectData Create(MSB.Execution.ProjectInstance projectInstance) @@ -211,6 +216,8 @@ public static ProjectData Create(MSB.Execution.ProjectInstance projectInstance) var signAssembly = PropertyConverter.ToBoolean(projectInstance.GetPropertyValue(PropertyNames.SignAssembly), defaultValue: false); var assemblyOriginatorKeyFile = projectInstance.GetPropertyValue(PropertyNames.AssemblyOriginatorKeyFile); + var ruleset = ResolveRulesetIfAny(projectInstance); + var sourceFiles = GetFullPaths( projectInstance.GetItems(ItemNames.Compile), filter: FileNameIsNotGenerated); @@ -259,7 +266,17 @@ public static ProjectData Create(MSB.Execution.ProjectInstance projectInstance) configuration, platform, targetFramework, targetFrameworks, outputKind, languageVersion, nullableContextOptions, allowUnsafeCode, documentationFile, preprocessorSymbolNames, suppressedDiagnosticIds, signAssembly, assemblyOriginatorKeyFile, - sourceFiles, projectReferences, references.ToImmutable(), packageReferences, analyzers, referenceAliases.ToImmutableDictionary()); + sourceFiles, projectReferences, references.ToImmutable(), packageReferences, analyzers, ruleset, referenceAliases.ToImmutableDictionary()); + } + + private static RuleSet ResolveRulesetIfAny(MSB.Execution.ProjectInstance projectInstance) + { + var rulesetIfAny = projectInstance.Properties.FirstOrDefault(x => x.Name == "ResolvedCodeAnalysisRuleSet"); + + if (rulesetIfAny != null) + return RuleSet.LoadEffectiveRuleSetFromFile(Path.Combine(projectInstance.Directory, rulesetIfAny.EvaluatedValue)); + + return null; } private static bool IsCSharpProject(string filePath) diff --git a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.cs b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.cs index 17f6a2f6a5..f0bc94a665 100644 --- a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.cs +++ b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.cs @@ -43,6 +43,7 @@ internal partial class ProjectFileInfo public bool SignAssembly => _data.SignAssembly; public string AssemblyOriginatorKeyFile => _data.AssemblyOriginatorKeyFile; + public RuleSet RuleSet => _data.RuleSet; public ImmutableArray SourceFiles => _data.SourceFiles; public ImmutableArray References => _data.References; diff --git a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfoExtensions.cs b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfoExtensions.cs index c6fec99f89..d0571c0be8 100644 --- a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfoExtensions.cs +++ b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfoExtensions.cs @@ -1,6 +1,10 @@ -using System.IO; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; using OmniSharp.Helpers; namespace OmniSharp.MSBuild.ProjectFile @@ -40,8 +44,10 @@ public static CSharpCompilationOptions CreateCompilationOptions(this ProjectFile return result; } - public static ProjectInfo CreateProjectInfo(this ProjectFileInfo projectFileInfo) + public static ProjectInfo CreateProjectInfo(this ProjectFileInfo projectFileInfo, IAnalyzerAssemblyLoader analyzerAssemblyLoader) { + var analyzerReferences = ResolveAnalyzerReferencesForProject(projectFileInfo, analyzerAssemblyLoader); + return ProjectInfo.Create( id: projectFileInfo.Id, version: VersionStamp.Create(), @@ -50,7 +56,18 @@ public static ProjectInfo CreateProjectInfo(this ProjectFileInfo projectFileInfo language: LanguageNames.CSharp, filePath: projectFileInfo.FilePath, outputFilePath: projectFileInfo.TargetPath, - compilationOptions: projectFileInfo.CreateCompilationOptions()); + compilationOptions: projectFileInfo.CreateCompilationOptions(), + analyzerReferences: analyzerReferences); + } + + private static IEnumerable ResolveAnalyzerReferencesForProject(ProjectFileInfo projectFileInfo, IAnalyzerAssemblyLoader analyzerAssemblyLoader) + { + foreach(var analyzerAssemblyPath in projectFileInfo.Analyzers.Distinct()) + { + analyzerAssemblyLoader.AddDependencyLocation(analyzerAssemblyPath); + } + + return projectFileInfo.Analyzers.Select(analyzerCandicatePath => new AnalyzerFileReference(analyzerCandicatePath, analyzerAssemblyLoader)); } } } diff --git a/src/OmniSharp.MSBuild/ProjectManager.cs b/src/OmniSharp.MSBuild/ProjectManager.cs index 9d74565a2c..c6fbdcb480 100644 --- a/src/OmniSharp.MSBuild/ProjectManager.cs +++ b/src/OmniSharp.MSBuild/ProjectManager.cs @@ -18,6 +18,8 @@ using OmniSharp.MSBuild.Models.Events; using OmniSharp.MSBuild.Notification; using OmniSharp.MSBuild.ProjectFile; +using OmniSharp.Roslyn.CSharp.Services.Diagnostics; +using OmniSharp.Roslyn.CSharp.Services.Refactoring.V2; using OmniSharp.Options; using OmniSharp.Roslyn.Utilities; using OmniSharp.Services; @@ -53,17 +55,20 @@ public ProjectToUpdate(string filePath, bool allowAutoRestore, ProjectIdInfo pro private readonly ConcurrentDictionary _projectsRequestedOnDemand; private readonly ProjectLoader _projectLoader; private readonly OmniSharpWorkspace _workspace; + private readonly CachingCodeFixProviderForProjects _codeFixesForProject; private readonly ImmutableArray _eventSinks; - private const int LoopDelay = 100; // milliseconds private readonly BufferBlock _queue; private readonly CancellationTokenSource _processLoopCancellation; private readonly Task _processLoopTask; + private readonly IAnalyzerAssemblyLoader _assemblyLoader; private bool _processingQueue; private readonly FileSystemNotificationCallback _onDirectoryFileChanged; + private readonly RulesetsForProjects _rulesetsForProjects; - public ProjectManager(ILoggerFactory loggerFactory, + public ProjectManager( + ILoggerFactory loggerFactory, MSBuildOptions options, IEventEmitter eventEmitter, IFileSystemWatcher fileSystemWatcher, @@ -71,6 +76,9 @@ public ProjectManager(ILoggerFactory loggerFactory, PackageDependencyChecker packageDependencyChecker, ProjectLoader projectLoader, OmniSharpWorkspace workspace, + CachingCodeFixProviderForProjects codeFixesForProject, + RulesetsForProjects rulesetsForProjects, + IAnalyzerAssemblyLoader assemblyLoader, ImmutableArray eventSinks) { _logger = loggerFactory.CreateLogger(); @@ -84,13 +92,14 @@ public ProjectManager(ILoggerFactory loggerFactory, _projectsRequestedOnDemand = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); _projectLoader = projectLoader; _workspace = workspace; + _codeFixesForProject = codeFixesForProject; _eventSinks = eventSinks; - _queue = new BufferBlock(); _processLoopCancellation = new CancellationTokenSource(); _processLoopTask = Task.Run(() => ProcessLoopAsync(_processLoopCancellation.Token)); - + _assemblyLoader = assemblyLoader; _onDirectoryFileChanged = OnDirectoryFileChanged; + _rulesetsForProjects = rulesetsForProjects; if (_options.LoadProjectsOnDemand) { @@ -347,7 +356,13 @@ private void AddProject(ProjectFileInfo projectFileInfo) _projectFiles.Add(projectFileInfo); - var projectInfo = projectFileInfo.CreateProjectInfo(); + var projectInfo = projectFileInfo.CreateProjectInfo(_assemblyLoader); + + _codeFixesForProject.LoadFrom(projectInfo); + + if(projectFileInfo.RuleSet != null) + _rulesetsForProjects.AddOrUpdateRuleset(projectFileInfo.Id, projectFileInfo.RuleSet); + var newSolution = _workspace.CurrentSolution.AddProject(projectInfo); if (!_workspace.TryApplyChanges(newSolution)) diff --git a/src/OmniSharp.MSBuild/ProjectSystem.cs b/src/OmniSharp.MSBuild/ProjectSystem.cs index a5fede9d27..b03857a8d1 100644 --- a/src/OmniSharp.MSBuild/ProjectSystem.cs +++ b/src/OmniSharp.MSBuild/ProjectSystem.cs @@ -18,6 +18,8 @@ using OmniSharp.MSBuild.ProjectFile; using OmniSharp.MSBuild.SolutionParsing; using OmniSharp.Options; +using OmniSharp.Roslyn.CSharp.Services.Diagnostics; +using OmniSharp.Roslyn.CSharp.Services.Refactoring.V2; using OmniSharp.Services; using System.Linq; @@ -36,9 +38,11 @@ internal class ProjectSystem : IProjectSystem private readonly IFileSystemWatcher _fileSystemWatcher; private readonly FileSystemHelper _fileSystemHelper; private readonly ILoggerFactory _loggerFactory; + private readonly CachingCodeFixProviderForProjects _codeFixesForProjects; + private readonly RulesetsForProjects _rulesetsForProjects; private readonly ILogger _logger; + private readonly IAnalyzerAssemblyLoader _assemblyLoader; private readonly ImmutableArray _eventSinks; - private readonly object _gate = new object(); private readonly Queue _projectsToProcess; @@ -65,6 +69,9 @@ public ProjectSystem( IFileSystemWatcher fileSystemWatcher, FileSystemHelper fileSystemHelper, ILoggerFactory loggerFactory, + CachingCodeFixProviderForProjects codeFixesForProjects, + RulesetsForProjects rulesetsForProjects, + IAnalyzerAssemblyLoader assemblyLoader, [ImportMany] IEnumerable eventSinks) { _environment = environment; @@ -77,10 +84,13 @@ public ProjectSystem( _fileSystemWatcher = fileSystemWatcher; _fileSystemHelper = fileSystemHelper; _loggerFactory = loggerFactory; + _codeFixesForProjects = codeFixesForProjects; + _rulesetsForProjects = rulesetsForProjects; _eventSinks = eventSinks.ToImmutableArray(); _projectsToProcess = new Queue(); _logger = loggerFactory.CreateLogger(); + _assemblyLoader = assemblyLoader; } public void Initalize(IConfiguration configuration) @@ -99,8 +109,8 @@ public void Initalize(IConfiguration configuration) _packageDependencyChecker = new PackageDependencyChecker(_loggerFactory, _eventEmitter, _dotNetCli, _options); _loader = new ProjectLoader(_options, _environment.TargetDirectory, _propertyOverrides, _loggerFactory, _sdksPathResolver); - _manager = new ProjectManager(_loggerFactory, _options, _eventEmitter, _fileSystemWatcher, _metadataFileReferenceCache, _packageDependencyChecker, - _loader, _workspace, _eventSinks); + + _manager = new ProjectManager(_loggerFactory, _options, _eventEmitter, _fileSystemWatcher, _metadataFileReferenceCache, _packageDependencyChecker, _loader, _workspace, _codeFixesForProjects, _rulesetsForProjects, _assemblyLoader, _eventSinks); if (_options.LoadProjectsOnDemand) { diff --git a/src/OmniSharp.Roslyn.CSharp/Helpers/DiagnosticExtensions.cs b/src/OmniSharp.Roslyn.CSharp/Helpers/DiagnosticExtensions.cs index 94380327b6..f78467d871 100644 --- a/src/OmniSharp.Roslyn.CSharp/Helpers/DiagnosticExtensions.cs +++ b/src/OmniSharp.Roslyn.CSharp/Helpers/DiagnosticExtensions.cs @@ -18,47 +18,27 @@ internal static DiagnosticLocation ToDiagnosticLocation(this Diagnostic diagnost Column = span.StartLinePosition.Character, EndLine = span.EndLinePosition.Line, EndColumn = span.EndLinePosition.Character, - Text = diagnostic.GetMessage(), + Text = $"{diagnostic.GetMessage()} ({diagnostic.Id})", LogLevel = diagnostic.Severity.ToString(), Id = diagnostic.Id }; } - internal static async Task> FindDiagnosticLocationsAsync(this IEnumerable documents, OmniSharpWorkspace workspace) + internal static IEnumerable DistinctDiagnosticLocationsByProject(this IEnumerable<(string projectName, Diagnostic diagnostic)> diagnostics) { - if (documents == null || !documents.Any()) return Enumerable.Empty(); - - var items = new List(); - foreach (var document in documents) - { - IEnumerable diagnostics; - if (workspace.IsCapableOfSemanticDiagnostics(document)) + return diagnostics + .Select(x => new { - var semanticModel = await document.GetSemanticModelAsync(); - diagnostics = semanticModel.GetDiagnostics(); - } - else + location = x.diagnostic.ToDiagnosticLocation(), + project = x.projectName + }) + .GroupBy(x => x.location) + .Select(x => { - var syntaxModel = await document.GetSyntaxTreeAsync(); - diagnostics = syntaxModel.GetDiagnostics(); - } - - foreach (var quickFix in diagnostics.Select(d => d.ToDiagnosticLocation())) - { - var existingQuickFix = items.FirstOrDefault(q => q.Equals(quickFix)); - if (existingQuickFix == null) - { - quickFix.Projects.Add(document.Project.Name); - items.Add(quickFix); - } - else - { - existingQuickFix.Projects.Add(document.Project.Name); - } - } - } - - return items; + var location = x.First().location; + location.Projects = x.Select(a => a.project).ToList(); + return location; + }); } } } diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Diagnostics/CodeCheckService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Diagnostics/CodeCheckService.cs index c61a07b2ab..e0e526921d 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Diagnostics/CodeCheckService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Diagnostics/CodeCheckService.cs @@ -1,35 +1,59 @@ using System.Collections.Generic; +using System.Collections.Immutable; using System.Composition; using System.Linq; using System.Threading.Tasks; using Microsoft.CodeAnalysis; +using Microsoft.Extensions.Logging; using OmniSharp.Helpers; using OmniSharp.Mef; using OmniSharp.Models; using OmniSharp.Models.CodeCheck; +using OmniSharp.Models.Diagnostics; +using OmniSharp.Options; +using OmniSharp.Roslyn.CSharp.Workers.Diagnostics; namespace OmniSharp.Roslyn.CSharp.Services.Diagnostics { [OmniSharpHandler(OmniSharpEndpoints.CodeCheck, LanguageNames.CSharp)] public class CodeCheckService : IRequestHandler { - private OmniSharpWorkspace _workspace; + private readonly ICsDiagnosticWorker _diagWorker; + private readonly ILogger _logger; [ImportingConstructor] - public CodeCheckService(OmniSharpWorkspace workspace) + public CodeCheckService( + OmniSharpWorkspace workspace, + ILoggerFactory loggerFactory, + OmniSharpOptions options, + ICsDiagnosticWorker diagWorker) { - _workspace = workspace; + _diagWorker = diagWorker; + _logger = loggerFactory.CreateLogger(); } public async Task Handle(CodeCheckRequest request) { - var documents = request.FileName != null - // To properly handle the request wait until all projects are loaded. - ? await _workspace.GetDocumentsFromFullProjectModelAsync(request.FileName) - : _workspace.CurrentSolution.Projects.SelectMany(project => project.Documents); + if (string.IsNullOrEmpty(request.FileName)) + { + var allDiagnostics = await _diagWorker.GetAllDiagnosticsAsync(); + return GetResponseFromDiagnostics(allDiagnostics, fileName: null); + } - var quickFixes = await documents.FindDiagnosticLocationsAsync(_workspace); - return new QuickFixResponse(quickFixes); + var diagnostics = await _diagWorker.GetDiagnostics(new [] { request.FileName }.ToImmutableArray()); + + return GetResponseFromDiagnostics(diagnostics, request.FileName); + } + + private static QuickFixResponse GetResponseFromDiagnostics(ImmutableArray<(string projectName, Diagnostic diagnostic)> diagnostics, string fileName) + { + var diagnosticLocations = diagnostics + .Where(x => (string.IsNullOrEmpty(fileName) + || x.diagnostic.Location.GetLineSpan().Path == fileName)) + .DistinctDiagnosticLocationsByProject() + .Where(x => x.FileName != null); + + return new QuickFixResponse(diagnosticLocations); } } } diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Diagnostics/DiagnosticsService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Diagnostics/DiagnosticsService.cs index 9ed6cda2f6..65b0f9666c 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Diagnostics/DiagnosticsService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Diagnostics/DiagnosticsService.cs @@ -1,26 +1,25 @@ +using System.Collections.Immutable; using System.Composition; using System.Linq; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using OmniSharp.Mef; using OmniSharp.Models.Diagnostics; -using OmniSharp.Workers.Diagnostics; +using OmniSharp.Roslyn.CSharp.Workers.Diagnostics; namespace OmniSharp.Roslyn.CSharp.Services.Diagnostics { [OmniSharpHandler(OmniSharpEndpoints.Diagnostics, LanguageNames.CSharp)] public class DiagnosticsService : IRequestHandler { - private readonly CSharpDiagnosticService _diagnostics; private readonly DiagnosticEventForwarder _forwarder; - private readonly OmniSharpWorkspace _workspace; + private readonly ICsDiagnosticWorker _diagWorker; [ImportingConstructor] - public DiagnosticsService(OmniSharpWorkspace workspace, DiagnosticEventForwarder forwarder, CSharpDiagnosticService diagnostics) + public DiagnosticsService(DiagnosticEventForwarder forwarder, ICsDiagnosticWorker diagWorker) { _forwarder = forwarder; - _workspace = workspace; - _diagnostics = diagnostics; + _diagWorker = diagWorker; } public Task Handle(DiagnosticsRequest request) @@ -30,11 +29,7 @@ public Task Handle(DiagnosticsRequest request) _forwarder.IsEnabled = true; } - var documents = request.FileName != null - ? new [] { request.FileName } - : _workspace.CurrentSolution.Projects.SelectMany(project => project.Documents.Select(x => x.FilePath)); - - _diagnostics.QueueDiagnostics(documents.ToArray()); + _diagWorker.QueueAllDocumentsForDiagnostics(); return Task.FromResult(new DiagnosticsResponse()); } diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Diagnostics/RulesetsForProjects.cs b/src/OmniSharp.Roslyn.CSharp/Services/Diagnostics/RulesetsForProjects.cs new file mode 100644 index 0000000000..98d186ea92 --- /dev/null +++ b/src/OmniSharp.Roslyn.CSharp/Services/Diagnostics/RulesetsForProjects.cs @@ -0,0 +1,41 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace OmniSharp.Roslyn.CSharp.Services.Diagnostics +{ + [Shared] + [Export(typeof(RulesetsForProjects))] + public class RulesetsForProjects + { + private readonly ConcurrentDictionary _rules = new ConcurrentDictionary(); + public ImmutableDictionary GetRules(ProjectId projectId) + { + if (!_rules.ContainsKey(projectId)) + return ImmutableDictionary.Empty; + + return _rules[projectId].SpecificDiagnosticOptions; + } + + public CompilationOptions BuildCompilationOptionsWithCurrentRules(Project project) + { + if (!_rules.ContainsKey(project.Id)) + return project.CompilationOptions; + + var existingRules = project.CompilationOptions.SpecificDiagnosticOptions; + var projectRules = GetRules(project.Id); + + var distinctRulesWithProjectSpecificRules = projectRules.Concat(existingRules.Where( x=> !projectRules.Keys.Contains(x.Key))); + + return project.CompilationOptions.WithSpecificDiagnosticOptions(distinctRulesWithProjectSpecificRules); + } + + public void AddOrUpdateRuleset(ProjectId projectId, RuleSet ruleset) + { + _rules.AddOrUpdate(projectId, ruleset, (_,__) => ruleset); + } + } +} diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/AvailableCodeAction.cs b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/AvailableCodeAction.cs index c29e975cf8..26a3d2c6ba 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/AvailableCodeAction.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/AvailableCodeAction.cs @@ -13,12 +13,7 @@ public class AvailableCodeAction public AvailableCodeAction(CodeAction codeAction, CodeAction parentCodeAction = null) { - if (codeAction == null) - { - throw new ArgumentNullException(nameof(codeAction)); - } - - this.CodeAction = codeAction; + this.CodeAction = codeAction ?? throw new ArgumentNullException(nameof(codeAction)); this.ParentCodeAction = parentCodeAction; } diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/BaseCodeActionService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/BaseCodeActionService.cs index c8598a6192..4aadcadfd0 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/BaseCodeActionService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/BaseCodeActionService.cs @@ -12,9 +12,12 @@ using Microsoft.CodeAnalysis.Text; using Microsoft.Extensions.Logging; using OmniSharp.Extensions; +using OmniSharp.Helpers; using OmniSharp.Mef; using OmniSharp.Models.V2.CodeActions; -using OmniSharp.Roslyn.CSharp.Services.CodeActions; +using OmniSharp.Options; +using OmniSharp.Roslyn.CSharp.Services.Diagnostics; +using OmniSharp.Roslyn.CSharp.Workers.Diagnostics; using OmniSharp.Services; using OmniSharp.Utilities; @@ -25,23 +28,30 @@ public abstract class BaseCodeActionService : IRequestHandl protected readonly OmniSharpWorkspace Workspace; protected readonly IEnumerable Providers; protected readonly ILogger Logger; - - private readonly CodeActionHelper _helper; + private readonly ICsDiagnosticWorker diagnostics; + private readonly CachingCodeFixProviderForProjects codeFixesForProject; private readonly MethodInfo _getNestedCodeActions; - private static readonly Func> s_createDiagnosticList = _ => new List(); - - protected Lazy> OrderedCodeFixProviders; protected Lazy> OrderedCodeRefactoringProviders; - protected BaseCodeActionService(OmniSharpWorkspace workspace, CodeActionHelper helper, IEnumerable providers, ILogger logger) + // CS8019 isn't directly used (via roslyn) but has an analyzer that report different diagnostic based on CS8019 to improve user experience. + private readonly Dictionary customDiagVsFixMap = new Dictionary + { + { "CS8019", "RemoveUnnecessaryImportsFixable" } + }; + + protected BaseCodeActionService( + OmniSharpWorkspace workspace, + IEnumerable providers, + ILogger logger, + ICsDiagnosticWorker diagnostics, + CachingCodeFixProviderForProjects codeFixesForProject) { this.Workspace = workspace; this.Providers = providers; this.Logger = logger; - this._helper = helper; - - OrderedCodeFixProviders = new Lazy>(() => GetSortedCodeFixProviders()); + this.diagnostics = diagnostics; + this.codeFixesForProject = codeFixesForProject; OrderedCodeRefactoringProviders = new Lazy>(() => GetSortedCodeRefactoringProviders()); // Sadly, the CodeAction.NestedCodeActions property is still internal. @@ -52,6 +62,7 @@ protected BaseCodeActionService(OmniSharpWorkspace workspace, CodeActionHelper h } this._getNestedCodeActions = nestedCodeActionsProperty.GetGetMethod(nonPublic: true); + if (this._getNestedCodeActions == null) { throw new InvalidOperationException("Could not retrieve 'get' method for CodeAction.NestedCodeActions property."); @@ -77,10 +88,12 @@ protected async Task> GetAvailableCodeActions(I await CollectCodeFixesActions(document, span, codeActions); await CollectRefactoringActions(document, span, codeActions); + var distinctActions = codeActions.GroupBy(x => x.Title).Select(x => x.First()); + // Be sure to filter out any code actions that inherit from CodeActionWithOptions. // This isn't a great solution and might need changing later, but every Roslyn code action // derived from this type tries to display a dialog. For now, this is a reasonable solution. - var availableActions = ConvertToAvailableCodeAction(codeActions) + var availableActions = ConvertToAvailableCodeAction(distinctActions) .Where(a => !a.CodeAction.GetType().GetTypeInfo().IsSubclassOf(typeof(CodeActionWithOptions))); return availableActions; @@ -99,31 +112,17 @@ private TextSpan GetTextSpan(ICodeActionRequest request, SourceText sourceText) private async Task CollectCodeFixesActions(Document document, TextSpan span, List codeActions) { - Dictionary> aggregatedDiagnostics = null; + var diagnosticsWithProjects = await this.diagnostics.GetDiagnostics(ImmutableArray.Create(document.FilePath)); - var semanticModel = await document.GetSemanticModelAsync(); + var groupedBySpan = diagnosticsWithProjects + .Select(x => x.diagnostic) + .Where(diagnostic => span.IntersectsWith(diagnostic.Location.SourceSpan)) + .GroupBy(diagnostic => diagnostic.Location.SourceSpan); - foreach (var diagnostic in semanticModel.GetDiagnostics()) + foreach (var diagnosticGroupedBySpan in groupedBySpan) { - if (!span.IntersectsWith(diagnostic.Location.SourceSpan)) - { - continue; - } - - aggregatedDiagnostics = aggregatedDiagnostics ?? new Dictionary>(); - var list = aggregatedDiagnostics.GetOrAdd(diagnostic.Location.SourceSpan, s_createDiagnosticList); - list.Add(diagnostic); - } - - if (aggregatedDiagnostics == null) - { - return; - } - - foreach (var kvp in aggregatedDiagnostics) - { - var diagnosticSpan = kvp.Key; - var diagnosticsWithSameSpan = kvp.Value.OrderByDescending(d => d.Severity); + var diagnosticSpan = diagnosticGroupedBySpan.Key; + var diagnosticsWithSameSpan = diagnosticGroupedBySpan.OrderByDescending(d => d.Severity); await AppendFixesAsync(document, diagnosticSpan, diagnosticsWithSameSpan, codeActions); } @@ -131,9 +130,10 @@ private async Task CollectCodeFixesActions(Document document, TextSpan span, Lis private async Task AppendFixesAsync(Document document, TextSpan span, IEnumerable diagnostics, List codeActions) { - foreach (var codeFixProvider in OrderedCodeFixProviders.Value) + foreach (var codeFixProvider in GetSortedCodeFixProviders(document)) { var fixableDiagnostics = diagnostics.Where(d => HasFix(codeFixProvider, d.Id)).ToImmutableArray(); + if (fixableDiagnostics.Length > 0) { var context = new CodeFixContext(document, span, fixableDiagnostics, (a, _) => codeActions.Add(a), CancellationToken.None); @@ -150,58 +150,36 @@ private async Task AppendFixesAsync(Document document, TextSpan span, IEnumerabl } } - private List GetSortedCodeFixProviders() + private List GetSortedCodeFixProviders(Document document) { - var providerList = this.Providers.SelectMany(provider => provider.CodeFixProviders); + var providerList = + this.Providers.SelectMany(provider => provider.CodeFixProviders) + .Concat(codeFixesForProject.GetAllCodeFixesForProject(document.Project.Id)); + return ExtensionOrderer.GetOrderedOrUnorderedList(providerList, attribute => attribute.Name).ToList(); } private List GetSortedCodeRefactoringProviders() { var providerList = this.Providers.SelectMany(provider => provider.CodeRefactoringProviders); - return ExtensionOrderer.GetOrderedOrUnorderedList(providerList, attribute => attribute.Name).ToList(); + return ExtensionOrderer.GetOrderedOrUnorderedList(providerList, attribute => attribute.Name).ToList(); } private bool HasFix(CodeFixProvider codeFixProvider, string diagnosticId) { - var typeName = codeFixProvider.GetType().FullName; - - if (_helper.IsDisallowed(typeName)) - { - return false; - } - - // TODO: This is a horrible hack! However, remove unnecessary usings only - // responds for diagnostics that are produced by its diagnostic analyzer. - // We need to provide a *real* diagnostic engine to address this. - if (typeName != CodeActionHelper.RemoveUnnecessaryUsingsProviderName) - { - if (!codeFixProvider.FixableDiagnosticIds.Contains(diagnosticId)) - { - return false; - } - } - else if (diagnosticId != "CS8019") // ErrorCode.HDN_UnusedUsingDirective - { - return false; - } - - return true; + return codeFixProvider.FixableDiagnosticIds.Any(id => id == diagnosticId) + || (customDiagVsFixMap.ContainsKey(diagnosticId) && codeFixProvider.FixableDiagnosticIds.Any(id => id == customDiagVsFixMap[diagnosticId])); } private async Task CollectRefactoringActions(Document document, TextSpan span, List codeActions) { - foreach (var codeRefactoringProvider in OrderedCodeRefactoringProviders.Value) - { - if (_helper.IsDisallowed(codeRefactoringProvider)) - { - continue; - } - - var context = new CodeRefactoringContext(document, span, a => codeActions.Add(a), CancellationToken.None); + var availableRefactorings = OrderedCodeRefactoringProviders.Value; + foreach (var codeRefactoringProvider in availableRefactorings) + { try { + var context = new CodeRefactoringContext(document, span, a => codeActions.Add(a), CancellationToken.None); await codeRefactoringProvider.ComputeRefactoringsAsync(context); } catch (Exception ex) @@ -213,32 +191,17 @@ private async Task CollectRefactoringActions(Document document, TextSpan span, L private IEnumerable ConvertToAvailableCodeAction(IEnumerable actions) { - var codeActions = new List(); - - foreach (var action in actions) + return actions.SelectMany(action => { - var handledNestedActions = false; - - // Roslyn supports "nested" code actions in order to allow submenus in the VS light bulb menu. - // For now, we'll just expand nested code actions in place. var nestedActions = this._getNestedCodeActions.Invoke>(action, null); - if (nestedActions.Length > 0) - { - foreach (var nestedAction in nestedActions) - { - codeActions.Add(new AvailableCodeAction(nestedAction, action)); - } - handledNestedActions = true; - } - - if (!handledNestedActions) + if (nestedActions.Any()) { - codeActions.Add(new AvailableCodeAction(action)); + return nestedActions.Select(nestedAction => new AvailableCodeAction(nestedAction, action)); } - } - return codeActions; + return new[] { new AvailableCodeAction(action) }; + }); } } } diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/CachingCodeFixProviderForProjects.cs b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/CachingCodeFixProviderForProjects.cs new file mode 100644 index 0000000000..9f7561a062 --- /dev/null +++ b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/CachingCodeFixProviderForProjects.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Reflection; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.Extensions.Logging; +using OmniSharp.Services; +using OmniSharp.Utilities; + +namespace OmniSharp.Roslyn.CSharp.Services.Refactoring.V2 +{ + [Shared] + [Export(typeof(CachingCodeFixProviderForProjects))] + public class CachingCodeFixProviderForProjects + { + private readonly ConcurrentDictionary> _cache = new ConcurrentDictionary>(); + private readonly ILogger _logger; + + [ImportingConstructor] + public CachingCodeFixProviderForProjects(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + public IEnumerable GetAllCodeFixesForProject(ProjectId projectId) + { + if (_cache.ContainsKey(projectId)) + return _cache[projectId]; + + return Enumerable.Empty(); + } + + public void LoadFrom(ProjectInfo project) + { + var codeFixes = project.AnalyzerReferences + .OfType() + .SelectMany(analyzerFileReference => analyzerFileReference.GetAssembly().DefinedTypes) + .Where(x => x.IsSubclassOf(typeof(CodeFixProvider))) + .Select(x => + { + try + { + var attribute = x.GetCustomAttribute(); + + if (attribute?.Languages != null && attribute.Languages.Contains(project.Language)) + { + return x.AsType().CreateInstance(); + } + + _logger.LogInformation($"Skipping code fix provider '{x.AsType()}' because it's language doesn't match '{project.Language}'."); + + return null; + } + catch (Exception ex) + { + _logger.LogError($"Creating instance of code fix provider '{x.AsType()}' failed, error: {ex}"); + + return null; + } + }) + .Where(x => x != null) + .ToImmutableArray(); + + _cache.AddOrUpdate(project.Id, codeFixes, (_, __) => codeFixes); + } + } +} diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/GetCodeActionsService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/GetCodeActionsService.cs index efc47cdb3a..5ade3b1ca8 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/GetCodeActionsService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/GetCodeActionsService.cs @@ -6,7 +6,10 @@ using Microsoft.Extensions.Logging; using OmniSharp.Mef; using OmniSharp.Models.V2.CodeActions; +using OmniSharp.Options; using OmniSharp.Roslyn.CSharp.Services.CodeActions; +using OmniSharp.Roslyn.CSharp.Services.Diagnostics; +using OmniSharp.Roslyn.CSharp.Workers.Diagnostics; using OmniSharp.Services; namespace OmniSharp.Roslyn.CSharp.Services.Refactoring.V2 @@ -19,8 +22,10 @@ public GetCodeActionsService( OmniSharpWorkspace workspace, CodeActionHelper helper, [ImportMany] IEnumerable providers, - ILoggerFactory loggerFactory) - : base(workspace, helper, providers, loggerFactory.CreateLogger()) + ILoggerFactory loggerFactory, + ICsDiagnosticWorker diagnostics, + CachingCodeFixProviderForProjects codeFixesForProjects) + : base(workspace, providers, loggerFactory.CreateLogger(), diagnostics, codeFixesForProjects) { } diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/RunCodeActionService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/RunCodeActionService.cs index d98c7bef78..5528a10420 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/RunCodeActionService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Refactoring/V2/RunCodeActionService.cs @@ -11,7 +11,10 @@ using Microsoft.Extensions.Logging; using OmniSharp.Mef; using OmniSharp.Models; +using OmniSharp.Options; using OmniSharp.Roslyn.CSharp.Services.CodeActions; +using OmniSharp.Roslyn.CSharp.Services.Diagnostics; +using OmniSharp.Roslyn.CSharp.Workers.Diagnostics; using OmniSharp.Roslyn.Utilities; using OmniSharp.Services; using OmniSharp.Utilities; @@ -38,8 +41,10 @@ public RunCodeActionService( OmniSharpWorkspace workspace, CodeActionHelper helper, [ImportMany] IEnumerable providers, - ILoggerFactory loggerFactory) - : base(workspace, helper, providers, loggerFactory.CreateLogger()) + ILoggerFactory loggerFactory, + ICsDiagnosticWorker diagnostics, + CachingCodeFixProviderForProjects codeFixesForProjects) + : base(workspace, providers, loggerFactory.CreateLogger(), diagnostics, codeFixesForProjects) { _loader = loader; _workspaceAssembly = _loader.LazyLoad(Configuration.RoslynWorkspaces); diff --git a/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/AnalyzerWorkQueue.cs b/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/AnalyzerWorkQueue.cs new file mode 100644 index 0000000000..a27af877fe --- /dev/null +++ b/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/AnalyzerWorkQueue.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.Extensions.Logging; + +namespace OmniSharp.Roslyn.CSharp.Workers.Diagnostics +{ + public class AnalyzerWorkQueue + { + private readonly int _throttlingMs = 300; + + private readonly ConcurrentDictionary _workQueue = + new ConcurrentDictionary(); + + private readonly ConcurrentDictionary _currentWork = + new ConcurrentDictionary(); + + private readonly Func _utcNow; + private readonly int _maximumDelayWhenWaitingForResults; + private readonly ILogger _logger; + + public AnalyzerWorkQueue(ILoggerFactory loggerFactory, Func utcNow = null, int timeoutForPendingWorkMs = 15*1000) + { + utcNow = utcNow ?? (() => DateTime.UtcNow); + _logger = loggerFactory.CreateLogger(); + _utcNow = utcNow; + _maximumDelayWhenWaitingForResults = timeoutForPendingWorkMs; + } + + public void PutWork(DocumentId documentId) + { + _workQueue.AddOrUpdate(documentId, + (modified: DateTime.UtcNow, new CancellationTokenSource()), + (_, oldValue) => (modified: DateTime.UtcNow, oldValue.workDoneSource)); + } + + public ImmutableArray TakeWork() + { + lock (_workQueue) + { + var now = _utcNow(); + var currentWork = _workQueue + .Where(x => ThrottlingPeriodNotActive(x.Value.modified, now)) + .OrderByDescending(x => x.Value.modified) + .Take(50) + .ToImmutableArray(); + + foreach (var work in currentWork) + { + _workQueue.TryRemove(work.Key, out _); + _currentWork.TryAdd(work.Key, work.Value); + } + + return currentWork.Select(x => x.Key).ToImmutableArray(); + } + } + + private bool ThrottlingPeriodNotActive(DateTime modified, DateTime now) + { + return (now - modified).TotalMilliseconds >= _throttlingMs; + } + + public void MarkWorkAsCompleteForDocumentId(DocumentId documentId) + { + if(_currentWork.TryGetValue(documentId, out var work)) + { + work.workDoneSource.Cancel(); + _currentWork.TryRemove(documentId, out _); + } + } + + // Omnisharp V2 api expects that it can request current information of diagnostics any time, + // however analysis is worker based and is eventually ready. This method is used to make api look + // like it's syncronous even that actual analysis may take a while. + public async Task WaitForResultsAsync(ImmutableArray documentIds) + { + var items = new List<(DateTime modified, CancellationTokenSource workDoneSource)>(); + + foreach (var documentId in documentIds) + { + if (_currentWork.ContainsKey(documentId)) + { + items.Add(_currentWork[documentId]); + } + else if (_workQueue.ContainsKey(documentId)) + { + items.Add(_workQueue[documentId]); + } + } + + await Task.WhenAll(items.Select(item => + Task.Delay(_maximumDelayWhenWaitingForResults, item.workDoneSource.Token) + .ContinueWith(task => LogTimeouts(task, documentIds)))); + } + + // This logs wait's for documentId diagnostics that continue without getting current version from analyzer. + // This happens on larger solutions during initial load or situations where analysis slows down remarkably. + private void LogTimeouts(Task task, IEnumerable documentIds) + { + if (!task.IsCanceled) _logger.LogDebug($"Timeout before work got ready for one of documents {string.Join(",", documentIds)}."); + } + } + +} diff --git a/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CSharpDiagnosticService.cs b/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CSharpDiagnosticWorker.cs similarity index 53% rename from src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CSharpDiagnosticService.cs rename to src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CSharpDiagnosticWorker.cs index f199b90af8..28f1e5db70 100644 --- a/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CSharpDiagnosticService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CSharpDiagnosticWorker.cs @@ -1,38 +1,42 @@ + using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.Immutable; using System.Composition; using System.Linq; using System.Reactive; using System.Reactive.Concurrency; using System.Reactive.Linq; using System.Reactive.Subjects; -using System.Reactive.Threading; +using System.Reflection; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Options; using Microsoft.Extensions.Logging; using OmniSharp.Helpers; using OmniSharp.Models.Diagnostics; +using OmniSharp.Options; using OmniSharp.Roslyn; +using OmniSharp.Roslyn.CSharp.Workers.Diagnostics; +using OmniSharp.Services; -namespace OmniSharp.Workers.Diagnostics +namespace OmniSharp.Roslyn.CSharp.Workers.Diagnostics { - [Export, Shared] - public class CSharpDiagnosticService + public class CSharpDiagnosticWorker: ICsDiagnosticWorker { private readonly ILogger _logger; private readonly OmniSharpWorkspace _workspace; - private readonly object _lock = new object(); private readonly DiagnosticEventForwarder _forwarder; private readonly IObserver _openDocuments; - [ImportingConstructor] - public CSharpDiagnosticService(OmniSharpWorkspace workspace, DiagnosticEventForwarder forwarder, ILoggerFactory loggerFactory) + public CSharpDiagnosticWorker(OmniSharpWorkspace workspace, DiagnosticEventForwarder forwarder, ILoggerFactory loggerFactory) { _workspace = workspace; _forwarder = forwarder; - _logger = loggerFactory.CreateLogger(); + _logger = loggerFactory.CreateLogger(); var openDocumentsSubject = new Subject(); _openDocuments = openDocumentsSubject; @@ -60,9 +64,6 @@ private void OnDocumentOpened(object sender, DocumentEventArgs args) { return; } - - EmitDiagnostics(args.Document.FilePath); - EmitDiagnostics(_workspace.GetOpenDocumentIds().Select(x => _workspace.CurrentSolution.GetDocument(x).FilePath).ToArray()); } private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs changeEvent) @@ -76,7 +77,6 @@ private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs changeEv { var newDocument = changeEvent.NewSolution.GetDocument(changeEvent.DocumentId); - EmitDiagnostics(newDocument.FilePath); EmitDiagnostics(_workspace.GetOpenDocumentIds().Select(x => _workspace.CurrentSolution.GetDocument(x).FilePath).ToArray()); } else if (changeEvent.Kind == WorkspaceChangeKind.ProjectAdded || changeEvent.Kind == WorkspaceChangeKind.ProjectReloaded) @@ -125,13 +125,76 @@ private IObservable ProcessQueue(IEnumerable filePaths) private async Task ProcessNextItem(string filePath) { var documents = _workspace.GetDocuments(filePath); - var items = await documents.FindDiagnosticLocationsAsync(_workspace); + var semanticModels = await Task.WhenAll(documents.Select(doc => doc.GetSemanticModelAsync())); + + var items = semanticModels + .SelectMany(sm => sm.GetDiagnostics()); return new DiagnosticResult() { FileName = filePath, - QuickFixes = items + QuickFixes = items.Select(x => x.ToDiagnosticLocation()).Distinct().ToArray() }; } + + public ImmutableArray QueueForDiagnosis(ImmutableArray documentPaths) + { + this.EmitDiagnostics(documentPaths.ToArray()); + return ImmutableArray.Empty; + } + + public async Task> GetDiagnostics(ImmutableArray documentPaths) + { + if (!documentPaths.Any()) return ImmutableArray<(string projectName, Diagnostic diagnostic)>.Empty; + + var results = new List<(string projectName, Diagnostic diagnostic)>(); + + var documents = + (await Task.WhenAll( + documentPaths + .Select(docPath => _workspace.GetDocumentsFromFullProjectModelAsync(docPath))) + ).SelectMany(s => s); + + foreach (var document in documents) + { + if(document?.Project?.Name == null) + continue; + + var projectName = document.Project.Name; + var diagnostics = await GetDiagnosticsForDocument(document, projectName); + results.AddRange(diagnostics.Select(x => (projectName: document.Project.Name, diagnostic: x))); + } + + return results.ToImmutableArray(); + } + + private static async Task> GetDiagnosticsForDocument(Document document, string projectName) + { + // Only basic syntax check is available if file is miscellanous like orphan .cs file. + // Those projects are on hard coded virtual project named 'MiscellaneousFiles.csproj'. + if (projectName == "MiscellaneousFiles.csproj") + { + var syntaxTree = await document.GetSyntaxTreeAsync(); + return syntaxTree.GetDiagnostics().ToImmutableArray(); + } + else + { + var semanticModel = await document.GetSemanticModelAsync(); + return semanticModel.GetDiagnostics(); + } + } + + public ImmutableArray QueueAllDocumentsForDiagnostics() + { + var documents = _workspace.CurrentSolution.Projects.SelectMany(x => x.Documents).ToImmutableArray(); + QueueForDiagnosis(documents.Select(x => x.FilePath).ToImmutableArray()); + return documents.Select(x => x.Id).ToImmutableArray(); + } + + public Task> GetAllDiagnosticsAsync() + { + var documents = _workspace.CurrentSolution.Projects.SelectMany(x => x.Documents).Select(x => x.FilePath).ToImmutableArray(); + return GetDiagnostics(documents); + } } } diff --git a/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CSharpDiagnosticWorkerWithAnalyzers.cs b/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CSharpDiagnosticWorkerWithAnalyzers.cs new file mode 100644 index 0000000000..d287cf4ac7 --- /dev/null +++ b/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CSharpDiagnosticWorkerWithAnalyzers.cs @@ -0,0 +1,287 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Options; +using Microsoft.Extensions.Logging; +using OmniSharp.Helpers; +using OmniSharp.Models.Diagnostics; +using OmniSharp.Options; +using OmniSharp.Roslyn.CSharp.Workers.Diagnostics; +using OmniSharp.Services; + +namespace OmniSharp.Roslyn.CSharp.Services.Diagnostics +{ + public class CSharpDiagnosticWorkerWithAnalyzers : ICsDiagnosticWorker + { + private readonly AnalyzerWorkQueue _workQueue; + private readonly ILogger _logger; + private readonly ConcurrentDictionary diagnostics)> _currentDiagnosticResults = + new ConcurrentDictionary diagnostics)>(); + private readonly ImmutableArray _providers; + private readonly DiagnosticEventForwarder _forwarder; + private readonly OmniSharpWorkspace _workspace; + private readonly RulesetsForProjects _rulesetsForProjects; + + // This is workaround. + // Currently roslyn doesn't expose official way to use IDE analyzers during analysis. + // This options gives certain IDE analysis access for services that are not yet publicly available. + private readonly ConstructorInfo _workspaceAnalyzerOptionsConstructor; + private bool _initialSolutionAnalysisInvoked = false; + + public CSharpDiagnosticWorkerWithAnalyzers( + OmniSharpWorkspace workspace, + [ImportMany] IEnumerable providers, + ILoggerFactory loggerFactory, + DiagnosticEventForwarder forwarder, + RulesetsForProjects rulesetsForProjects) + { + _logger = loggerFactory.CreateLogger(); + _providers = providers.ToImmutableArray(); + _workQueue = new AnalyzerWorkQueue(loggerFactory); + + _forwarder = forwarder; + _workspace = workspace; + _rulesetsForProjects = rulesetsForProjects; + + _workspaceAnalyzerOptionsConstructor = Assembly + .Load("Microsoft.CodeAnalysis.Features") + .GetType("Microsoft.CodeAnalysis.Diagnostics.WorkspaceAnalyzerOptions") + .GetConstructor(new Type[] { typeof(AnalyzerOptions), typeof(OptionSet), typeof(Solution) }) + ?? throw new InvalidOperationException("Could not resolve 'Microsoft.CodeAnalysis.Diagnostics.WorkspaceAnalyzerOptions' for IDE analyzers."); + + _workspace.WorkspaceChanged += OnWorkspaceChanged; + + Task.Factory.StartNew(Worker, TaskCreationOptions.LongRunning); + } + + private Task InitializeWithWorkspaceDocumentsIfNotYetDone() + { + if (_initialSolutionAnalysisInvoked) + return Task.CompletedTask; + + _initialSolutionAnalysisInvoked = true; + + return Task.Run(async () => + { + while (!_workspace.Initialized || _workspace.CurrentSolution.Projects.Count() == 0) await Task.Delay(50); + }) + .ContinueWith(_ => Task.Delay(50)) + .ContinueWith(_ => + { + var documentIds = QueueAllDocumentsForDiagnostics(); + _logger.LogInformation($"Solution initialized -> queue all documents for code analysis. Initial document count: {documentIds.Length}."); + }); + } + + public ImmutableArray QueueForDiagnosis(ImmutableArray documentPaths) + { + var documentIds = GetDocumentIdsFromPaths(documentPaths); + QueueForAnalysis(documentIds); + return documentIds; + } + + public async Task> GetDiagnostics(ImmutableArray documentPaths) + { + await InitializeWithWorkspaceDocumentsIfNotYetDone(); + + var documentIds = GetDocumentIdsFromPaths(documentPaths); + + return await GetDiagnosticsByDocumentIds(documentIds); + } + + private async Task> GetDiagnosticsByDocumentIds(ImmutableArray documentIds) + { + await _workQueue.WaitForResultsAsync(documentIds); + + return _currentDiagnosticResults + .Where(x => documentIds.Any(docId => docId == x.Key)) + .SelectMany(x => x.Value.diagnostics, (k, v) => ((k.Value.projectName, v))) + .ToImmutableArray(); + } + + private ImmutableArray GetDocumentIdsFromPaths(ImmutableArray documentPaths) + { + return documentPaths + .Select(docPath => _workspace.GetDocumentId(docPath)) + .ToImmutableArray(); + } + + private async Task Worker() + { + while (true) + { + try + { + var solution = _workspace.CurrentSolution; + + var currentWorkGroupedByProjects = _workQueue + .TakeWork() + .Select(documentId => (projectId: solution.GetDocument(documentId)?.Project?.Id, documentId)) + .Where(x => x.projectId != null) + .GroupBy(x => x.projectId, x => x.documentId) + .ToImmutableArray(); + + foreach (var projectGroup in currentWorkGroupedByProjects) + { + await AnalyzeProject(solution, projectGroup); + } + + await Task.Delay(50); + } + catch (Exception ex) + { + _logger.LogError($"Analyzer worker failed: {ex}"); + } + } + } + + private void QueueForAnalysis(ImmutableArray documentIds) + { + foreach (var document in documentIds) + { + _workQueue.PutWork(document); + } + } + + private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs changeEvent) + { + if (changeEvent.Kind == WorkspaceChangeKind.DocumentChanged + || changeEvent.Kind == WorkspaceChangeKind.DocumentAdded + || changeEvent.Kind == WorkspaceChangeKind.DocumentReloaded + || changeEvent.Kind == WorkspaceChangeKind.DocumentInfoChanged ) + { + QueueForAnalysis(ImmutableArray.Create(changeEvent.DocumentId)); + } + else if(changeEvent.Kind == WorkspaceChangeKind.DocumentRemoved) + { + _currentDiagnosticResults.TryRemove(changeEvent.DocumentId, out _); + } + } + + private async Task AnalyzeProject(Solution solution, IGrouping documentsGroupedByProject) + { + try + { + // TODO: This should be moved that project rulesets are updated + // to workspace projects itself when project is updated/loaded/manipulated and so on. + // It also causes these inderictions and multiple steps to collect work with projects / documents. + var projectOriginal = solution.GetProject(documentsGroupedByProject.Key); + + var projectWithOptions = projectOriginal.WithCompilationOptions( + _rulesetsForProjects.BuildCompilationOptionsWithCurrentRules(projectOriginal)); + + var allAnalyzers = _providers + .SelectMany(x => x.CodeDiagnosticAnalyzerProviders) + .Concat(projectWithOptions.AnalyzerReferences.SelectMany(x => x.GetAnalyzers(projectWithOptions.Language))) + .ToImmutableArray(); + + var compiled = await projectWithOptions + .GetCompilationAsync(); + + var workspaceAnalyzerOptions = + (AnalyzerOptions)_workspaceAnalyzerOptionsConstructor.Invoke(new object[] { projectWithOptions.AnalyzerOptions, projectWithOptions.Solution.Options, projectWithOptions.Solution }); + + foreach (var documentId in documentsGroupedByProject) + { + var document = projectWithOptions.GetDocument(documentId); + await AnalyzeDocument(projectWithOptions, allAnalyzers, compiled, workspaceAnalyzerOptions, document); + } + } + catch (Exception ex) + { + _logger.LogError($"Analysis of project {documentsGroupedByProject.Key} failed, underlaying error: {ex}"); + } + } + + private async Task AnalyzeDocument(Project project, ImmutableArray allAnalyzers, Compilation compiled, AnalyzerOptions workspaceAnalyzerOptions, Document document) + { + try + { + // There's real possibility that bug in analyzer causes analysis hang at document. + var perDocumentTimeout = new CancellationTokenSource(10 * 1000); + + var documentSemanticModel = await document.GetSemanticModelAsync(perDocumentTimeout.Token); + + var diagnostics = ImmutableArray.Empty; + + // Only basic syntax check is available if file is miscellanous like orphan .cs file. + // Those projects are on hard coded virtual project named 'MiscellaneousFiles.csproj'. + if (project.Name == "MiscellaneousFiles.csproj") + { + var syntaxTree = await document.GetSyntaxTreeAsync(); + diagnostics = syntaxTree.GetDiagnostics().ToImmutableArray(); + } + else if (allAnalyzers.Any()) // Analyzers cannot be called with empty analyzer list. + { + var semanticDiagnosticsWithAnalyzers = await compiled + .WithAnalyzers(allAnalyzers, workspaceAnalyzerOptions) + .GetAnalyzerSemanticDiagnosticsAsync(documentSemanticModel, filterSpan: null, perDocumentTimeout.Token); + + var syntaxDiagnosticsWithAnalyzers = await compiled + .WithAnalyzers(allAnalyzers, workspaceAnalyzerOptions) + .GetAnalyzerSyntaxDiagnosticsAsync(documentSemanticModel.SyntaxTree, perDocumentTimeout.Token); + + diagnostics = semanticDiagnosticsWithAnalyzers + .Concat(syntaxDiagnosticsWithAnalyzers) + .Concat(documentSemanticModel.GetDiagnostics()) + .ToImmutableArray(); + } + else + { + diagnostics = documentSemanticModel.GetDiagnostics().ToImmutableArray(); + } + + UpdateCurrentDiagnostics(project, document, diagnostics); + } + catch (Exception ex) + { + _logger.LogError($"Analysis of document {document.Name} failed or cancelled by timeout: {ex.Message}, analysers: {string.Join(", ", allAnalyzers)}"); + _workQueue.MarkWorkAsCompleteForDocumentId(document.Id); + } + } + + private void UpdateCurrentDiagnostics(Project project, Document document, ImmutableArray diagnosticsWithAnalyzers) + { + _currentDiagnosticResults[document.Id] = (project.Name, diagnosticsWithAnalyzers); + _workQueue.MarkWorkAsCompleteForDocumentId(document.Id); + EmitDiagnostics(_currentDiagnosticResults[document.Id].diagnostics); + } + + private void EmitDiagnostics(ImmutableArray results) + { + if (results.Any()) + { + _forwarder.Forward(new DiagnosticMessage + { + Results = results + .Select(x => x.ToDiagnosticLocation()) + .Where(x => x.FileName != null) + .GroupBy(x => x.FileName) + .Select(group => new DiagnosticResult { FileName = group.Key, QuickFixes = group.ToList() }) + }); + } + } + + public ImmutableArray QueueAllDocumentsForDiagnostics() + { + var documentIds = _workspace.CurrentSolution.Projects.SelectMany(x => x.DocumentIds).ToImmutableArray(); + QueueForAnalysis(documentIds); + return documentIds; + } + + public async Task> GetAllDiagnosticsAsync() + { + await InitializeWithWorkspaceDocumentsIfNotYetDone(); + var allDocumentsIds = _workspace.CurrentSolution.Projects.SelectMany(x => x.DocumentIds).ToImmutableArray(); + return await GetDiagnosticsByDocumentIds(allDocumentsIds); + } + } +} diff --git a/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CsharpDiagnosticWorkerComposer.cs b/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CsharpDiagnosticWorkerComposer.cs new file mode 100644 index 0000000000..5930a2c125 --- /dev/null +++ b/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/CsharpDiagnosticWorkerComposer.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Options; +using Microsoft.Extensions.Logging; +using OmniSharp.Helpers; +using OmniSharp.Models.Diagnostics; +using OmniSharp.Options; +using OmniSharp.Roslyn.CSharp.Services.Diagnostics; +using OmniSharp.Roslyn.CSharp.Workers.Diagnostics; +using OmniSharp.Services; + +namespace OmniSharp.Roslyn.CSharp.Workers.Diagnostics +{ + // Theres several implementation of worker currently based on configuration. + // This will handle switching between them. + [Export(typeof(ICsDiagnosticWorker)), Shared] + public class CsharpDiagnosticWorkerComposer: ICsDiagnosticWorker + { + private readonly ICsDiagnosticWorker _implementation; + private readonly OmniSharpWorkspace _workspace; + + [ImportingConstructor] + public CsharpDiagnosticWorkerComposer( + OmniSharpWorkspace workspace, + [ImportMany] IEnumerable providers, + ILoggerFactory loggerFactory, + DiagnosticEventForwarder forwarder, + RulesetsForProjects rulesetsForProjects, + OmniSharpOptions options) + { + if(options.RoslynExtensionsOptions.EnableAnalyzersSupport) + { + _implementation = new CSharpDiagnosticWorkerWithAnalyzers(workspace, providers, loggerFactory, forwarder, rulesetsForProjects); + } + else + { + _implementation = new CSharpDiagnosticWorker(workspace, forwarder, loggerFactory); + } + + _workspace = workspace; + } + + public Task> GetAllDiagnosticsAsync() + { + return _implementation.GetAllDiagnosticsAsync(); + } + + public Task> GetDiagnostics(ImmutableArray documentPaths) + { + return _implementation.GetDiagnostics(documentPaths); + } + + public ImmutableArray QueueAllDocumentsForDiagnostics() + { + return _implementation.QueueAllDocumentsForDiagnostics(); + } + + public ImmutableArray QueueForDiagnosis(ImmutableArray documentPaths) + { + return _implementation.QueueForDiagnosis(documentPaths); + } + } +} \ No newline at end of file diff --git a/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/ICsDiagnosticWorker.cs b/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/ICsDiagnosticWorker.cs new file mode 100644 index 0000000000..3295efa68b --- /dev/null +++ b/src/OmniSharp.Roslyn.CSharp/Workers/Diagnostics/ICsDiagnosticWorker.cs @@ -0,0 +1,14 @@ +using System.Collections.Immutable; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; + +namespace OmniSharp.Roslyn.CSharp.Workers.Diagnostics +{ + public interface ICsDiagnosticWorker + { + Task> GetDiagnostics(ImmutableArray documentPaths); + Task> GetAllDiagnosticsAsync(); + ImmutableArray QueueForDiagnosis(ImmutableArray documentsPaths); + ImmutableArray QueueAllDocumentsForDiagnostics(); + } +} \ No newline at end of file diff --git a/src/OmniSharp.Roslyn/Services/AbstractCodeActionProvider.cs b/src/OmniSharp.Roslyn/Services/AbstractCodeActionProvider.cs index 90627f1024..db49e43c41 100644 --- a/src/OmniSharp.Roslyn/Services/AbstractCodeActionProvider.cs +++ b/src/OmniSharp.Roslyn/Services/AbstractCodeActionProvider.cs @@ -4,6 +4,8 @@ using System.Reflection; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CodeRefactorings; +using Microsoft.CodeAnalysis.Diagnostics; +using OmniSharp.Utilities; namespace OmniSharp.Services { @@ -12,7 +14,7 @@ public abstract class AbstractCodeActionProvider : ICodeActionProvider public string ProviderName { get; } public ImmutableArray CodeRefactoringProviders { get; } public ImmutableArray CodeFixProviders { get; } - + public ImmutableArray CodeDiagnosticAnalyzerProviders { get; } public ImmutableArray Assemblies { get; } protected AbstractCodeActionProvider(string providerName, ImmutableArray assemblies) @@ -30,31 +32,21 @@ protected AbstractCodeActionProvider(string providerName, ImmutableArray typeof(CodeRefactoringProvider).IsAssignableFrom(t)) - .Select(type => CreateInstance(type)) + .Select(type => type.CreateInstance()) .Where(instance => instance != null) .ToImmutableArray(); this.CodeFixProviders = types .Where(t => typeof(CodeFixProvider).IsAssignableFrom(t)) - .Select(type => CreateInstance(type)) + .Select(type => type.CreateInstance()) .Where(instance => instance != null) .ToImmutableArray(); - } - private T CreateInstance(Type type) where T : class - { - try - { - var defaultCtor = type.GetConstructor(new Type[] { }); - - return defaultCtor != null - ? (T)Activator.CreateInstance(type) - : null; - } - catch (Exception ex) - { - throw new InvalidOperationException($"Failed to create instrance of {type.FullName} in {type.AssemblyQualifiedName}.", ex); - } + this.CodeDiagnosticAnalyzerProviders = types + .Where(t => typeof(DiagnosticAnalyzer).IsAssignableFrom(t)) + .Select(type => type.CreateInstance()) + .Where(instance => instance != null) + .ToImmutableArray(); } } } diff --git a/src/OmniSharp.Roslyn/Services/ICodeActionProvider.cs b/src/OmniSharp.Roslyn/Services/ICodeActionProvider.cs index b15f4a1b0c..6cc9d3bc21 100644 --- a/src/OmniSharp.Roslyn/Services/ICodeActionProvider.cs +++ b/src/OmniSharp.Roslyn/Services/ICodeActionProvider.cs @@ -2,6 +2,7 @@ using System.Reflection; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CodeRefactorings; +using Microsoft.CodeAnalysis.Diagnostics; namespace OmniSharp.Services { @@ -9,6 +10,7 @@ public interface ICodeActionProvider { ImmutableArray CodeRefactoringProviders { get; } ImmutableArray CodeFixProviders { get; } + ImmutableArray CodeDiagnosticAnalyzerProviders { get; } ImmutableArray Assemblies { get; } } } diff --git a/src/OmniSharp.Shared/Options/RoslynExtensionsOptions.cs b/src/OmniSharp.Shared/Options/RoslynExtensionsOptions.cs index 0072724225..879e22b4d8 100644 --- a/src/OmniSharp.Shared/Options/RoslynExtensionsOptions.cs +++ b/src/OmniSharp.Shared/Options/RoslynExtensionsOptions.cs @@ -7,6 +7,8 @@ namespace OmniSharp.Options { public class RoslynExtensionsOptions { + public bool EnableAnalyzersSupport { get; set; } + public string[] LocationPaths { get; set; } public IEnumerable GetNormalizedLocationPaths(IOmniSharpEnvironment env) diff --git a/src/OmniSharp.Shared/Utilities/ReflectionExtensions.cs b/src/OmniSharp.Shared/Utilities/ReflectionExtensions.cs index d79b0d6412..d997734995 100644 --- a/src/OmniSharp.Shared/Utilities/ReflectionExtensions.cs +++ b/src/OmniSharp.Shared/Utilities/ReflectionExtensions.cs @@ -113,6 +113,22 @@ public static object CreateInstance(this Lazy lazyType, params object[] ar return Activator.CreateInstance(lazyType.Value, args); } + public static T CreateInstance(this Type type) where T : class + { + try + { + var defaultCtor = type.GetConstructor(new Type[] { }); + + return defaultCtor != null + ? (T)Activator.CreateInstance(type) + : null; + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to create instrance of {type.FullName} in {type.AssemblyQualifiedName}.", ex); + } + } + public static T Invoke(this MethodInfo methodInfo, object obj, object[] args) { if (methodInfo == null) diff --git a/test-assets/test-projects/SolutionWithSignedProject/CallerLib/Caller.cs b/test-assets/test-projects/SolutionWithSignedProject/CallerLib/Caller.cs index 98d93786d6..fe77fc19df 100644 --- a/test-assets/test-projects/SolutionWithSignedProject/CallerLib/Caller.cs +++ b/test-assets/test-projects/SolutionWithSignedProject/CallerLib/Caller.cs @@ -4,7 +4,7 @@ public class Caller { public Caller() { - var callee = new Callee(); + Callee callee = new Callee(); } } } diff --git a/tests/OmniSharp.Cake.Tests/AssemblyInfo.cs b/tests/OmniSharp.Cake.Tests/AssemblyInfo.cs index 9933b8fd08..9468891954 100644 --- a/tests/OmniSharp.Cake.Tests/AssemblyInfo.cs +++ b/tests/OmniSharp.Cake.Tests/AssemblyInfo.cs @@ -1 +1 @@ -[assembly: Xunit.CollectionBehavior(DisableTestParallelization = true)] +[assembly: Xunit.CollectionBehavior(Xunit.CollectionBehavior.CollectionPerAssembly, DisableTestParallelization = true, MaxParallelThreads = -1)] diff --git a/tests/OmniSharp.Cake.Tests/CodeCheckFacts.cs b/tests/OmniSharp.Cake.Tests/CodeCheckFacts.cs index 02cfc810b5..f6289e6e71 100644 --- a/tests/OmniSharp.Cake.Tests/CodeCheckFacts.cs +++ b/tests/OmniSharp.Cake.Tests/CodeCheckFacts.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using OmniSharp.Cake.Services.RequestHandlers.Diagnostics; @@ -8,6 +9,7 @@ using TestUtility; using Xunit; using Xunit.Abstractions; +using Xunit.Sdk; namespace OmniSharp.Cake.Tests { @@ -48,7 +50,10 @@ public async Task ShouldNotIncludeDiagnosticsFromLoadedFilesIfFileNameIsSpecifie var target = Argument(""target"", ""Default"");"; var diagnostics = await FindDiagnostics(input, includeFileName: true); - Assert.Empty(diagnostics.QuickFixes); + + // error.cake file contains code that cause error like: + // The type or namespace name 'asdf' could not be found (are you missing a using directive or an assembly reference?) (CS0246) + Assert.DoesNotContain(diagnostics.QuickFixes.Select(x => x.ToString()), x => x.Contains("CS0246")); } private async Task FindDiagnostics(string contents, bool includeFileName) diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/AnalyzerWorkerQueueFacts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/AnalyzerWorkerQueueFacts.cs new file mode 100644 index 0000000000..8e6aaa85f6 --- /dev/null +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/AnalyzerWorkerQueueFacts.cs @@ -0,0 +1,238 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.Extensions.Logging; +using OmniSharp.Roslyn.CSharp.Workers.Diagnostics; +using Xunit; + +namespace OmniSharp.Roslyn.CSharp.Tests +{ + public class AnalyzerWorkerQueueFacts + { + private class Logger : ILogger + { + public IDisposable BeginScope(TState state) + { + return null; + } + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + RecordedMessages = RecordedMessages.Add(state.ToString()); + } + + public ImmutableArray RecordedMessages { get; set; } = ImmutableArray.Create(); + } + + private class LoggerFactory : ILoggerFactory + { + public Logger Logger { get; } = new Logger(); + + public void AddProvider(ILoggerProvider provider) + { + } + + public ILogger CreateLogger(string categoryName) + { + return Logger; + } + + public void Dispose() + { + } + } + + [Fact] + public void WhenItemsAreAddedButThrotlingIsntOverNoWorkShouldBeReturned() + { + var now = DateTime.UtcNow; + var queue = new AnalyzerWorkQueue(new LoggerFactory(), utcNow: () => now); + var document = CreateTestDocumentId(); + + queue.PutWork(document); + Assert.Empty(queue.TakeWork()); + } + + [Fact] + public void WhenWorksIsAddedToQueueThenTheyWillBeReturned() + { + var now = DateTime.UtcNow; + var queue = new AnalyzerWorkQueue(new LoggerFactory(), utcNow: () => now); + var document = CreateTestDocumentId(); + + queue.PutWork(document); + + now = PassOverThrotlingPeriod(now); + var work = queue.TakeWork(); + + Assert.Contains(document, work); + Assert.Empty(queue.TakeWork()); + } + + [Fact] + public void WhenSameItemIsAddedMultipleTimesInRowThenThrottleItemAsOne() + { + var now = DateTime.UtcNow; + var queue = new AnalyzerWorkQueue(new LoggerFactory(), utcNow: () => now); + var document = CreateTestDocumentId(); + + queue.PutWork(document); + queue.PutWork(document); + queue.PutWork(document); + + Assert.Empty(queue.TakeWork()); + + now = PassOverThrotlingPeriod(now); + + Assert.Contains(document, queue.TakeWork()); + Assert.Empty(queue.TakeWork()); + } + + private static DateTime PassOverThrotlingPeriod(DateTime now) => now.AddSeconds(30); + + [Fact] + public void WhenWorkIsAddedThenWaitNextIterationOfItReady() + { + var now = DateTime.UtcNow; + var queue = new AnalyzerWorkQueue(new LoggerFactory(), utcNow: () => now, timeoutForPendingWorkMs: 500); + var document = CreateTestDocumentId(); + + queue.PutWork(document); + + var pendingTask = queue.WaitForResultsAsync(new [] { document }.ToImmutableArray()); + pendingTask.Wait(TimeSpan.FromMilliseconds(50)); + + Assert.False(pendingTask.IsCompleted); + + now = PassOverThrotlingPeriod(now); + + var work = queue.TakeWork(); + queue.MarkWorkAsCompleteForDocumentId(document); + pendingTask.Wait(TimeSpan.FromMilliseconds(50)); + Assert.True(pendingTask.IsCompleted); + } + + [Fact] + public void WhenWorkIsUnderAnalysisOutFromQueueThenWaitUntilNextIterationOfItIsReady() + { + var now = DateTime.UtcNow; + var queue = new AnalyzerWorkQueue(new LoggerFactory(), utcNow: () => now, timeoutForPendingWorkMs: 500); + var document = CreateTestDocumentId(); + + queue.PutWork(document); + + now = PassOverThrotlingPeriod(now); + + var work = queue.TakeWork(); + + var pendingTask = queue.WaitForResultsAsync(work); + pendingTask.Wait(TimeSpan.FromMilliseconds(50)); + + Assert.False(pendingTask.IsCompleted); + queue.MarkWorkAsCompleteForDocumentId(document); + pendingTask.Wait(TimeSpan.FromMilliseconds(50)); + Assert.True(pendingTask.IsCompleted); + } + + [Fact] + public void WhenWorkIsWaitedButTimeoutForWaitIsExceededAllowContinue() + { + var now = DateTime.UtcNow; + var loggerFactory = new LoggerFactory(); + var queue = new AnalyzerWorkQueue(loggerFactory, utcNow: () => now, timeoutForPendingWorkMs: 20); + var document = CreateTestDocumentId(); + + queue.PutWork(document); + + now = PassOverThrotlingPeriod(now); + var work = queue.TakeWork(); + + var pendingTask = queue.WaitForResultsAsync(work); + pendingTask.Wait(TimeSpan.FromMilliseconds(100)); + + Assert.True(pendingTask.IsCompleted); + Assert.Contains("Timeout before work got ready", loggerFactory.Logger.RecordedMessages.Single()); + } + + [Fact] + public async Task WhenMultipleThreadsAreConsumingAnalyzerWorkerQueueItWorksAsExpected() + { + var now = DateTime.UtcNow; + + var queue = new AnalyzerWorkQueue(new LoggerFactory(), utcNow: () => now, timeoutForPendingWorkMs: 1000); + + var parallelQueues = + Enumerable.Range(0, 10) + .Select(_ => + Task.Run(() => { + var document = CreateTestDocumentId(); + + queue.PutWork(document); + + now = PassOverThrotlingPeriod(now); + var work = queue.TakeWork(); + + var pendingTask = queue.WaitForResultsAsync(work); + + foreach (var workDoc in work) + { + queue.MarkWorkAsCompleteForDocumentId(workDoc); + } + + pendingTask.Wait(TimeSpan.FromMilliseconds(300)); + })) + .ToArray(); + + await Task.WhenAll(parallelQueues); + + Assert.Empty(queue.TakeWork()); + } + + [Fact] + public async Task WhenWorkIsAddedAgainWhenPreviousIsAnalysing_ThenDontWaitAnotherOneToGetReady() + { + var now = DateTime.UtcNow; + var loggerFactory = new LoggerFactory(); + var queue = new AnalyzerWorkQueue(loggerFactory, utcNow: () => now); + var document = CreateTestDocumentId(); + + queue.PutWork(document); + + now = PassOverThrotlingPeriod(now); + + var work = queue.TakeWork(); + var waitingCall = Task.Run(async () => await queue.WaitForResultsAsync(work)); + await Task.Delay(50); + + // User updates code -> document is queued again during period when theres already api call waiting + // to continue. + queue.PutWork(document); + + // First iteration of work is done. + queue.MarkWorkAsCompleteForDocumentId(document); + + // Waiting call continues because it's iteration of work is done, even when theres next + // already waiting. + await waitingCall; + + Assert.True(waitingCall.IsCompleted); + Assert.Empty(loggerFactory.Logger.RecordedMessages); + } + + private DocumentId CreateTestDocumentId() + { + var projectInfo = ProjectInfo.Create( + id: ProjectId.CreateNewId(), + version: VersionStamp.Create(), + name: "testProject", + assemblyName: "AssemblyName", + language: LanguageNames.CSharp); + + return DocumentId.CreateNewId(projectInfo.Id); + } + } +} diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/BufferManagerMiscFilesFacts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/BufferManagerMiscFilesFacts.cs index c3ed03acec..a82f487b2c 100644 --- a/tests/OmniSharp.Roslyn.CSharp.Tests/BufferManagerMiscFilesFacts.cs +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/BufferManagerMiscFilesFacts.cs @@ -36,7 +36,7 @@ public async Task Adds_Misc_Document_Which_Supports_Only_syntactic_diagnostics() var request = new CodeCheckRequest() { FileName = filePath }; var actual = await host.GetResponse(OmniSharpEndpoints.CodeCheck, request); Assert.Single(actual.QuickFixes); - Assert.Equal("; expected", actual.QuickFixes.First().Text); + Assert.Equal("; expected (CS1002)", actual.QuickFixes.First().Text); } } } @@ -52,27 +52,26 @@ public static void Main(){ } }"; var testfile = new TestFile("a.cs", source); + using (var testProject = await TestAssets.Instance.GetTestProjectAsync("EmptyProject")) + using (var host = CreateOmniSharpHost(testProject.Directory)) { - using (var host = CreateOmniSharpHost(testProject.Directory)) + var filePath = await AddTestFile(host, testProject, testfile); + var point = testfile.Content.GetPointFromPosition(); + var request = new SignatureHelpRequest() { - var filePath = await AddTestFile(host, testProject, testfile); - var point = testfile.Content.GetPointFromPosition(); - var request = new SignatureHelpRequest() - { - FileName = filePath, - Line = point.Line, - Column = point.Offset, - Buffer = testfile.Content.Code - }; + FileName = filePath, + Line = point.Line, + Column = point.Offset, + Buffer = testfile.Content.Code + }; - var actual = await host.GetResponse(OmniSharpEndpoints.SignatureHelp, request); - Assert.Single(actual.Signatures); - Assert.Equal(0, actual.ActiveParameter); - Assert.Equal(0, actual.ActiveSignature); - Assert.Equal("NewGuid", actual.Signatures.ElementAt(0).Name); - Assert.Empty(actual.Signatures.ElementAt(0).Parameters); - } + var actual = await host.GetResponse(OmniSharpEndpoints.SignatureHelp, request); + Assert.Single(actual.Signatures); + Assert.Equal(0, actual.ActiveParameter); + Assert.Equal(0, actual.ActiveSignature); + Assert.Equal("NewGuid", actual.Signatures.ElementAt(0).Name); + Assert.Empty(actual.Signatures.ElementAt(0).Parameters); } } @@ -87,24 +86,23 @@ public void Foo() {} }"; var testfile = new TestFile("a.cs", source); + using (var testProject = await TestAssets.Instance.GetTestProjectAsync("EmptyProject")) + using (var host = CreateOmniSharpHost(testProject.Directory)) { - using (var host = CreateOmniSharpHost(testProject.Directory)) + var filePath = await AddTestFile(host, testProject, testfile); + var point = testfile.Content.GetPointFromPosition(); + var request = new FindImplementationsRequest() { - var filePath = await AddTestFile(host, testProject, testfile); - var point = testfile.Content.GetPointFromPosition(); - var request = new FindImplementationsRequest() - { - FileName = filePath, - Line = point.Line, - Column = point.Offset, - Buffer = testfile.Content.Code - }; + FileName = filePath, + Line = point.Line, + Column = point.Offset, + Buffer = testfile.Content.Code + }; - var actual = await host.GetResponse(OmniSharpEndpoints.FindImplementations, request); - Assert.Single(actual.QuickFixes); - Assert.Equal("public void Foo() {}", actual.QuickFixes.First().Text.Trim()); - } + var actual = await host.GetResponse(OmniSharpEndpoints.FindImplementations, request); + Assert.Single(actual.QuickFixes); + Assert.Equal("public void Foo() {}", actual.QuickFixes.First().Text.Trim()); } } @@ -250,24 +248,24 @@ public method1() public async Task Adds_Misc_Document_Which_Supports_TypeLookup() { const string code = @"class F$$oo {}"; + var testfile = new TestFile("a.cs", code); + using (var testProject = await TestAssets.Instance.GetTestProjectAsync("EmptyProject")) + using (var host = CreateOmniSharpHost(testProject.Directory)) { - using (var host = CreateOmniSharpHost(testProject.Directory)) + var filePath = await AddTestFile(host, testProject, testfile); + var service = host.GetRequestHandler(OmniSharpEndpoints.TypeLookup); + var point = testfile.Content.GetPointFromPosition(); + var request = new TypeLookupRequest { - var filePath = await AddTestFile(host, testProject, testfile); - var service = host.GetRequestHandler(OmniSharpEndpoints.TypeLookup); - var point = testfile.Content.GetPointFromPosition(); - var request = new TypeLookupRequest - { - FileName = filePath, - Line = point.Line, - Column = point.Offset, - }; + FileName = filePath, + Line = point.Line, + Column = point.Offset, + }; - var actual = await host.GetResponse(OmniSharpEndpoints.TypeLookup, request); - Assert.Equal("Foo", actual.Type); - } + var actual = await host.GetResponse(OmniSharpEndpoints.TypeLookup, request); + Assert.Equal("Foo", actual.Type); } } diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/CodeActionsV2Facts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/CodeActionsV2Facts.cs index 487d913851..ccaf723162 100644 --- a/tests/OmniSharp.Roslyn.CSharp.Tests/CodeActionsV2Facts.cs +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/CodeActionsV2Facts.cs @@ -21,8 +21,10 @@ public CodeActionsV2Facts(ITestOutputHelper output) { } - [Fact] - public async Task Can_get_code_actions_from_roslyn() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Can_get_code_actions_from_roslyn(bool roslynAnalyzersEnabled) { const string code = @"public class Class1 @@ -33,12 +35,14 @@ public void Whatever() } }"; - var refactorings = await FindRefactoringNamesAsync(code); + var refactorings = await FindRefactoringNamesAsync(code, roslynAnalyzersEnabled); Assert.Contains("using System;", refactorings); } - [Fact] - public async Task Can_get_code_actions_from_external_source() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Can_get_code_actions_from_external_source(bool roslynAnalyzersEnabled) { const string code = @" @@ -55,16 +59,20 @@ public async Task Whatever() var configuration = new Dictionary { - { "RoslynExtensionsOptions:LocationPaths:0", TestAssets.Instance.TestBinariesFolder } + { "RoslynExtensionsOptions:LocationPaths:0", TestAssets.Instance.TestBinariesFolder }, }; - var refactorings = await FindRefactoringsAsync(code, configuration); + + var refactorings = await FindRefactoringsAsync(code, + TestHelpers.GetConfigurationDataWithAnalyzerConfig(roslynAnalyzersEnabled, existingConfiguration: configuration)); Assert.NotEmpty(refactorings); Assert.Contains("Add ConfigureAwait(false)", refactorings.Select(x => x.Name)); } - [Fact] - public async Task Can_remove_unnecessary_usings() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Can_remove_unnecessary_usings(bool roslynAnalyzersEnabled) { const string code = @"using MyNamespace3; @@ -80,12 +88,14 @@ public class c {public c() {Guid.NewGuid();}}"; public class c {public c() {Guid.NewGuid();}}"; - var response = await RunRefactoringAsync(code, "Remove Unnecessary Usings"); + var response = await RunRefactoringAsync(code, "Remove Unnecessary Usings", roslynAnalyzersEnabled: roslynAnalyzersEnabled); AssertIgnoringIndent(expected, ((ModifiedFileResponse)response.Changes.First()).Buffer); } - [Fact] - public async Task Can_get_ranged_code_action() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Can_get_ranged_code_action(bool roslynAnalyzersEnabled) { const string code = @"public class Class1 @@ -96,12 +106,14 @@ public void Whatever() } }"; - var refactorings = await FindRefactoringNamesAsync(code); + var refactorings = await FindRefactoringNamesAsync(code, roslynAnalyzersEnabled); Assert.Contains("Extract Method", refactorings); } - [Fact] - public async Task Returns_ordered_code_actions() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Returns_ordered_code_actions(bool roslynAnalyzersEnabled) { const string code = @"public class Class1 @@ -112,8 +124,22 @@ public void Whatever() } }"; - var refactorings = await FindRefactoringNamesAsync(code); - List expected = new List + var refactorings = await FindRefactoringNamesAsync(code, roslynAnalyzersEnabled); + + List expected = roslynAnalyzersEnabled ? new List + { + "Fix formatting", + "using System;", + "System.Console", + "Generate variable 'Console' -> Generate property 'Class1.Console'", + "Generate variable 'Console' -> Generate field 'Class1.Console'", + "Generate variable 'Console' -> Generate read-only field 'Class1.Console'", + "Generate variable 'Console' -> Generate local 'Console'", + "Generate type 'Console' -> Generate class 'Console' in new file", + "Generate type 'Console' -> Generate class 'Console'", + "Generate type 'Console' -> Generate nested class 'Console'", + "Extract Method" + } : new List { "using System;", "System.Console", @@ -126,11 +152,14 @@ public void Whatever() "Generate type 'Console' -> Generate nested class 'Console'", "Extract Method" }; + Assert.Equal(expected, refactorings); } - [Fact] - public async Task Can_extract_method() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Can_extract_method(bool roslynAnalyzersEnabled) { const string code = @"public class Class1 @@ -153,15 +182,17 @@ private static void NewMethod() Console.Write(""should be using System;""); } }"; - var response = await RunRefactoringAsync(code, "Extract Method"); + var response = await RunRefactoringAsync(code, "Extract Method", roslynAnalyzersEnabled: roslynAnalyzersEnabled); AssertIgnoringIndent(expected, ((ModifiedFileResponse)response.Changes.First()).Buffer); } - [Fact] - public async Task Can_generate_type_and_return_name_of_new_file() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Can_generate_type_and_return_name_of_new_file(bool roslynAnalyzersEnabled) { using (var testProject = await TestAssets.Instance.GetTestProjectAsync("ProjectWithMissingType")) - using (var host = CreateOmniSharpHost(testProject.Directory)) + using (var host = CreateOmniSharpHost(testProject.Directory, configurationData: TestHelpers.GetConfigurationDataWithAnalyzerConfig(roslynAnalyzersEnabled))) { var requestHandler = host.GetRequestHandler(OmniSharpEndpoints.V2.RunCodeAction); var document = host.Workspace.CurrentSolution.Projects.First().Documents.First(); @@ -196,11 +227,13 @@ internal class Z } } - [Fact] - public async Task Can_send_rename_and_fileOpen_responses_when_codeAction_renames_file() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Can_send_rename_and_fileOpen_responses_when_codeAction_renames_file(bool roslynAnalyzersEnabled) { using (var testProject = await TestAssets.Instance.GetTestProjectAsync("ProjectWithMismatchedFileName")) - using (var host = CreateOmniSharpHost(testProject.Directory)) + using (var host = CreateOmniSharpHost(testProject.Directory, configurationData: TestHelpers.GetConfigurationDataWithAnalyzerConfig(roslynAnalyzersEnabled))) { var requestHandler = host.GetRequestHandler(OmniSharpEndpoints.V2.RunCodeAction); var document = host.Workspace.CurrentSolution.Projects.First().Documents.First(); @@ -237,18 +270,18 @@ private static string TrimLines(string source) return string.Join("\n", source.Split('\n').Select(s => s.Trim())); } - private async Task RunRefactoringAsync(string code, string refactoringName, bool wantsChanges = false) + private async Task RunRefactoringAsync(string code, string refactoringName, bool wantsChanges = false, bool roslynAnalyzersEnabled = false) { - var refactorings = await FindRefactoringsAsync(code); + var refactorings = await FindRefactoringsAsync(code, configurationData: TestHelpers.GetConfigurationDataWithAnalyzerConfig(roslynAnalyzersEnabled)); Assert.Contains(refactoringName, refactorings.Select(a => a.Name)); var identifier = refactorings.First(action => action.Name.Equals(refactoringName)).Identifier; return await RunRefactoringsAsync(code, identifier, wantsChanges); } - private async Task> FindRefactoringNamesAsync(string code) + private async Task> FindRefactoringNamesAsync(string code, bool roslynAnalyzersEnabled = false) { - var codeActions = await FindRefactoringsAsync(code); + var codeActions = await FindRefactoringsAsync(code, configurationData: TestHelpers.GetConfigurationDataWithAnalyzerConfig(roslynAnalyzersEnabled)); return codeActions.Select(a => a.Name); } diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/CustomRoslynAnalyzerFacts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/CustomRoslynAnalyzerFacts.cs new file mode 100644 index 0000000000..99649303e9 --- /dev/null +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/CustomRoslynAnalyzerFacts.cs @@ -0,0 +1,264 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using OmniSharp.Models.Diagnostics; +using OmniSharp.Roslyn.CSharp.Services.Diagnostics; +using TestUtility; +using Xunit; +using Xunit.Abstractions; + +namespace OmniSharp.Roslyn.CSharp.Tests +{ + public class CustomRoslynAnalyzerFacts + { + public class TestAnalyzerReference : AnalyzerReference + { + private readonly string _id; + private readonly bool _isEnabledByDefault; + + public TestAnalyzerReference(string testAnalyzerId, bool isEnabledByDefault = true) + { + _id = testAnalyzerId; + _isEnabledByDefault = isEnabledByDefault; + } + + public override string FullPath => null; + public override object Id => _id; + public override string Display => $"{nameof(TestAnalyzerReference)}_{Id}"; + + public override ImmutableArray GetAnalyzers(string language) + { + return new DiagnosticAnalyzer[] { new TestDiagnosticAnalyzer(Id.ToString(), _isEnabledByDefault) }.ToImmutableArray(); + } + + public override ImmutableArray GetAnalyzersForAllLanguages() + { + return new DiagnosticAnalyzer[] { new TestDiagnosticAnalyzer(Id.ToString(), _isEnabledByDefault) }.ToImmutableArray(); + } + } + + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class TestDiagnosticAnalyzer : DiagnosticAnalyzer + { + public TestDiagnosticAnalyzer(string id, bool isEnabledByDefault) + { + this.id = id; + _isEnabledByDefault = isEnabledByDefault; + } + + private DiagnosticDescriptor Rule => new DiagnosticDescriptor( + this.id, + "Testtitle", + "Type name '{0}' contains lowercase letters", + "Naming", + DiagnosticSeverity.Error, + isEnabledByDefault: _isEnabledByDefault + ); + + private readonly string id; + private readonly bool _isEnabledByDefault; + + public override ImmutableArray SupportedDiagnostics + { + get { return ImmutableArray.Create(Rule); } + } + + public override void Initialize(AnalysisContext context) + { + context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType); + } + + private void AnalyzeSymbol(SymbolAnalysisContext context) + { + var namedTypeSymbol = (INamedTypeSymbol)context.Symbol; + if (namedTypeSymbol.Name == "_this_is_invalid_test_class_name") + { + context.ReportDiagnostic(Diagnostic.Create( + Rule, + namedTypeSymbol.Locations[0], + namedTypeSymbol.Name + )); + } + } + } + + private readonly ITestOutputHelper _testOutput; + + public CustomRoslynAnalyzerFacts(ITestOutputHelper testOutput) + { + _testOutput = testOutput; + } + + [Fact] + public async Task When_custom_analyzers_are_executed_then_return_results() + { + using (var host = GetHost()) + { + var testFile = new TestFile("testFile.cs", "class _this_is_invalid_test_class_name { int n = true; }"); + + host.AddFilesToWorkspace(testFile); + + var testAnalyzerRef = new TestAnalyzerReference("TS1234", isEnabledByDefault: true); + + AddProjectWitFile(host, testFile, testAnalyzerRef); + + var result = await host.RequestCodeCheckAsync(); + Assert.Contains(result.QuickFixes, f => f.Text.Contains(testAnalyzerRef.Id.ToString())); + } + } + + private OmniSharpTestHost GetHost() + { + return OmniSharpTestHost.Create(testOutput: _testOutput, + configurationData: TestHelpers.GetConfigurationDataWithAnalyzerConfig(roslynAnalyzersEnabled: true)); + } + + [Fact] + public async Task Always_return_results_from_net_default_analyzers() + { + using (var host = GetHost()) + { + var testFile = new TestFile("testFile_1.cs", "class SomeClass { int n = true; }"); + + AddProjectWitFile(host, testFile); + + var result = await host.RequestCodeCheckAsync(); + + Assert.Contains(result.QuickFixes.Where(x => x.FileName == testFile.FileName), f => f.Text.Contains("CS")); + } + } + + [Fact] + public async Task Rulesets_should_work_with_syntax_analyzers() + { + using (var host = GetHost()) + { + var testFile = new TestFile("testFile_9.cs", @" + class Program + { + static void Main(string[] args) + { + return; + Console.WriteLine(null); // This is CS0162, unreachable code. + } + }"); + var ruleService = host.GetExport(); + + var projectIds = AddProjectWitFile(host, testFile); + + var testRules = CreateRules("CS0162", ReportDiagnostic.Hidden); + + ruleService.AddOrUpdateRuleset(projectIds.Single(), new RuleSet( + "", + new ReportDiagnostic(), + testRules.ToImmutableDictionary(), + new ImmutableArray())); + + var result = await host.RequestCodeCheckAsync(); + + Assert.Contains(result.QuickFixes.OfType(), f => f.Text.Contains("CS0162") && f.LogLevel == "Hidden"); + } + } + + [Fact] + public async Task When_rules_udpate_diagnostic_severity_then_show_them_with_new_severity() + { + using (var host = GetHost()) + { + var testFile = new TestFile("testFile_2.cs", "class _this_is_invalid_test_class_name { int n = true; }"); + var ruleService = host.GetExport(); + + var testAnalyzerRef = new TestAnalyzerReference("TS1100"); + + var projectIds = AddProjectWitFile(host, testFile, testAnalyzerRef); + var testRules = CreateRules(testAnalyzerRef.Id.ToString(), ReportDiagnostic.Hidden); + + ruleService.AddOrUpdateRuleset(projectIds.Single(), new RuleSet( + "", + new ReportDiagnostic(), + testRules.ToImmutableDictionary(), + new ImmutableArray())); + + var result = await host.RequestCodeCheckAsync("testFile_2.cs"); + Assert.Contains(result.QuickFixes.OfType(), f => f.Text.Contains(testAnalyzerRef.Id.ToString()) && f.LogLevel == "Hidden"); + } + } + + private static Dictionary CreateRules(string analyzerId, ReportDiagnostic diagnostic) + { + return new Dictionary + { + { analyzerId, diagnostic } + }; + } + + [Fact] + // This is important because hidden still allows code fixes to execute, not prevents it, for this reason suppressed analytics should not be returned at all. + public async Task When_custom_rule_is_set_to_none_dont_return_results_at_all() + { + using (var host = GetHost()) + { + var testFile = new TestFile("testFile_3.cs", "class _this_is_invalid_test_class_name { int n = true; }"); + + var ruleService = host.GetExport(); + + var testAnalyzerRef = new TestAnalyzerReference("TS1101"); + + var projectIds = AddProjectWitFile(host, testFile, testAnalyzerRef); + + var testRules = CreateRules(testAnalyzerRef.Id.ToString(), ReportDiagnostic.Suppress); + + ruleService.AddOrUpdateRuleset(projectIds.Single(), new RuleSet( + "", + new ReportDiagnostic(), + testRules.ToImmutableDictionary(), + new ImmutableArray())); + + var result = await host.RequestCodeCheckAsync("testFile_3.cs"); + Assert.DoesNotContain(result.QuickFixes, f => f.Text.Contains(testAnalyzerRef.Id.ToString())); + } + } + + [Fact] + public async Task When_diagnostic_is_disabled_by_default_updating_rule_will_enable_it() + { + using (var host = GetHost()) + { + var testFile = new TestFile("testFile_4.cs", "class _this_is_invalid_test_class_name { int n = true; }"); + var ruleService = host.GetExport(); + + var testAnalyzerRef = new TestAnalyzerReference("TS1101", isEnabledByDefault: false); + + var projectIds = AddProjectWitFile(host, testFile, testAnalyzerRef); + + var testRules = CreateRules(testAnalyzerRef.Id.ToString(), ReportDiagnostic.Error); + + ruleService.AddOrUpdateRuleset(projectIds.Single(), new RuleSet( + "", + new ReportDiagnostic(), + testRules.ToImmutableDictionary(), + new ImmutableArray())); + + var result = await host.RequestCodeCheckAsync("testFile_4.cs"); + Assert.Contains(result.QuickFixes, f => f.Text.Contains(testAnalyzerRef.Id.ToString())); + } + } + + private IEnumerable AddProjectWitFile(OmniSharpTestHost host, TestFile testFile, TestAnalyzerReference testAnalyzerRef = null) + { + var analyzerReferences = testAnalyzerRef == null ? default : + new AnalyzerReference[] { testAnalyzerRef }.ToImmutableArray(); + + return TestHelpers.AddProjectToWorkspace( + host.Workspace, + "project.csproj", + new[] { "netcoreapp2.1" }, + new[] { testFile }, + analyzerRefs: analyzerReferences); + } + } +} diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/DiagnosticsFacts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/DiagnosticsFacts.cs index 52213d86bf..e62f6a301b 100644 --- a/tests/OmniSharp.Roslyn.CSharp.Tests/DiagnosticsFacts.cs +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/DiagnosticsFacts.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.CodeAnalysis; @@ -9,37 +10,87 @@ namespace OmniSharp.Roslyn.CSharp.Tests { - public class DiagnosticsFacts : AbstractSingleRequestHandlerTestFixture + public class DiagnosticsFacts { - public DiagnosticsFacts(ITestOutputHelper output, SharedOmniSharpHostFixture sharedOmniSharpHostFixture) - : base(output, sharedOmniSharpHostFixture) + private readonly ITestOutputHelper _testOutput; + + public DiagnosticsFacts(ITestOutputHelper testOutput) { + _testOutput = testOutput; } - protected override string EndpointName => OmniSharpEndpoints.CodeCheck; - [Fact] - public async Task CodeCheckSpecifiedFileOnly() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task CodeCheckSpecifiedFileOnly(bool roslynAnalyzersEnabled) { - SharedOmniSharpTestHost.AddFilesToWorkspace(new TestFile("a.cs", "class C { int n = true; }")); - var requestHandler = GetRequestHandler(SharedOmniSharpTestHost); - var quickFixes = await requestHandler.Handle(new CodeCheckRequest() { FileName = "a.cs" }); + using (var host = GetHost(roslynAnalyzersEnabled)) + { + host.AddFilesToWorkspace(new TestFile("a.cs", "class C { int n = true; }")); + var quickFixes = await host.RequestCodeCheckAsync("a.cs"); - Assert.Single(quickFixes.QuickFixes); - Assert.Equal("a.cs", quickFixes.QuickFixes.First().FileName); + Assert.Contains(quickFixes.QuickFixes.Select(x => x.ToString()), x => x.Contains("CS0029")); + Assert.Equal("a.cs", quickFixes.QuickFixes.First().FileName); + } } - [Fact] - public async Task CheckAllFiles() + private OmniSharpTestHost GetHost(bool roslynAnalyzersEnabled) { - SharedOmniSharpTestHost.AddFilesToWorkspace( - new TestFile("a.cs", "class C1 { int n = true; }"), - new TestFile("b.cs", "class C2 { int n = true; }")); + return OmniSharpTestHost.Create(testOutput: _testOutput, configurationData: new Dictionary() { { "RoslynExtensionsOptions:EnableAnalyzersSupport", roslynAnalyzersEnabled.ToString() } }); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task CheckAllFiles(bool roslynAnalyzersEnabled) + { + using(var host = GetHost(roslynAnalyzersEnabled)) + { + host.AddFilesToWorkspace( + new TestFile("a.cs", "class C1 { int n = true; }"), + new TestFile("b.cs", "class C2 { int n = true; }")); + + var quickFixes = await host.RequestCodeCheckAsync(); + + Assert.Contains(quickFixes.QuickFixes, x => x.Text.Contains("CS0029") && x.FileName == "a.cs"); + Assert.Contains(quickFixes.QuickFixes, x => x.Text.Contains("CS0029") && x.FileName == "b.cs"); + } + } - var handler = GetRequestHandler(SharedOmniSharpTestHost); - var quickFixes = await handler.Handle(new CodeCheckRequest()); + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task WhenFileIsDeletedFromWorkSpaceThenResultsAreNotReturnedAnyMore(bool roslynAnalyzersEnabled) + { + using(var host = GetHost(roslynAnalyzersEnabled)) + { + host.AddFilesToWorkspace(new TestFile("a.cs", "class C1 { int n = true; }")); + await host.RequestCodeCheckAsync(); + + foreach (var doc in host.Workspace.CurrentSolution.Projects.SelectMany(x => x.Documents)) + { + // Theres document for each targeted framework, lets delete all. + host.Workspace.RemoveDocument(doc.Id); + } + + var quickFixes = await host.RequestCodeCheckAsync(); + + Assert.DoesNotContain(quickFixes.QuickFixes, x => x.Text.Contains("CS0029") && x.FileName == "a.cs"); + } + } + + [Fact] + public async Task AnalysisSupportBuiltInIDEAnalysers() + { + using(var host = GetHost(roslynAnalyzersEnabled: true)) + { + host.AddFilesToWorkspace( + new TestFile("a.cs", "class C1 { int n = true; }")); - Assert.Equal(2, quickFixes.QuickFixes.Count()); + var quickFixes = await host.RequestCodeCheckAsync("a.cs"); + Assert.Contains(quickFixes.QuickFixes, x => x.Text.Contains("IDE0044")); + } } } } diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/DiagnosticsV2Facts.DiagnosticTestEmitter.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/DiagnosticsV2Facts.DiagnosticTestEmitter.cs index 456a4b3a00..5136f0adbf 100644 --- a/tests/OmniSharp.Roslyn.CSharp.Tests/DiagnosticsV2Facts.DiagnosticTestEmitter.cs +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/DiagnosticsV2Facts.DiagnosticTestEmitter.cs @@ -1,4 +1,8 @@ -using System.Collections.Generic; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; using System.Threading.Tasks; using OmniSharp.Eventing; using OmniSharp.Models.Diagnostics; @@ -9,21 +13,36 @@ public partial class DiagnosticsV2Facts { private class DiagnosticTestEmitter : IEventEmitter { - private readonly IList _messages; + public readonly ConcurrentBag Messages = new ConcurrentBag(); + private readonly TaskCompletionSource _tcs; - public Task Emitted => _tcs.Task; + public async Task ExpectForEmitted(Expression> predicate) + { + var asCompiledPredicate = predicate.Compile(); + + // May seem hacky but nothing is more painfull to debug than infinite hanging test ... + for(int i = 0; i < 100; i++) + { + if(Messages.Any(m => asCompiledPredicate(m))) + { + return; + } + + await Task.Delay(250); + } + + throw new InvalidOperationException($"Timeout reached before expected event count reached before prediction {predicate} came true, current diagnostics '{String.Join(";", Messages.SelectMany(x => x.Results))}'"); + } - public DiagnosticTestEmitter(IList messages) + public DiagnosticTestEmitter() { - _messages = messages; _tcs = new TaskCompletionSource(); } public void Emit(string kind, object args) { - _messages.Add((DiagnosticMessage)args); - _tcs.TrySetResult(null); + Messages.Add((DiagnosticMessage)args); } } } diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/DiagnosticsV2Facts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/DiagnosticsV2Facts.cs index 5a0701b59e..72712fec6f 100644 --- a/tests/OmniSharp.Roslyn.CSharp.Tests/DiagnosticsV2Facts.cs +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/DiagnosticsV2Facts.cs @@ -2,8 +2,9 @@ using System.Linq; using System.Threading.Tasks; using OmniSharp.Models.Diagnostics; +using OmniSharp.Options; using OmniSharp.Roslyn.CSharp.Services.Diagnostics; -using OmniSharp.Workers.Diagnostics; +using OmniSharp.Services; using TestUtility; using Xunit; using Xunit.Abstractions; @@ -22,27 +23,28 @@ public DiagnosticsV2Facts(ITestOutputHelper output, SharedOmniSharpHostFixture s [InlineData("a.csx")] public async Task CodeCheckSpecifiedFileOnly(string filename) { + SharedOmniSharpTestHost.ClearWorkspace(); + var testFile = new TestFile(filename, "class C { int n = true; }"); - SharedOmniSharpTestHost.AddFilesToWorkspace(testFile); - var messages = new List(); - var emitter = new DiagnosticTestEmitter(messages); + var emitter = new DiagnosticTestEmitter(); var forwarder = new DiagnosticEventForwarder(emitter) { IsEnabled = true }; - var service = new CSharpDiagnosticService(SharedOmniSharpTestHost.Workspace, forwarder, this.LoggerFactory); - service.QueueDiagnostics(filename); + var service = CreateDiagnosticService(forwarder); + SharedOmniSharpTestHost.AddFilesToWorkspace(testFile); + + var controller = new DiagnosticsService(forwarder, service); + var response = await controller.Handle(new DiagnosticsRequest { FileName = testFile.FileName }); - await emitter.Emitted; + await emitter.ExpectForEmitted(msg => msg.Results.Any(m => m.FileName == filename)); + } - Assert.Single(messages); - var message = messages.First(); - Assert.Single(message.Results); - var result = message.Results.First(); - Assert.Single(result.QuickFixes); - Assert.Equal(filename, result.FileName); + private CSharpDiagnosticWorkerWithAnalyzers CreateDiagnosticService(DiagnosticEventForwarder forwarder) + { + return new CSharpDiagnosticWorkerWithAnalyzers(SharedOmniSharpTestHost.Workspace, Enumerable.Empty(), this.LoggerFactory, forwarder, new RulesetsForProjects()); } [Theory(Skip = "Test needs to be updated for service changes")] @@ -50,31 +52,23 @@ public async Task CodeCheckSpecifiedFileOnly(string filename) [InlineData("a.csx", "b.csx")] public async Task CheckAllFiles(string filename1, string filename2) { + SharedOmniSharpTestHost.ClearWorkspace(); + var testFile1 = new TestFile(filename1, "class C1 { int n = true; }"); var testFile2 = new TestFile(filename2, "class C2 { int n = true; }"); SharedOmniSharpTestHost.AddFilesToWorkspace(testFile1, testFile2); - var messages = new List(); - var emitter = new DiagnosticTestEmitter(messages); + var emitter = new DiagnosticTestEmitter(); var forwarder = new DiagnosticEventForwarder(emitter); - var service = new CSharpDiagnosticService(SharedOmniSharpTestHost.Workspace, forwarder, this.LoggerFactory); + var service = CreateDiagnosticService(forwarder); - var controller = new DiagnosticsService(SharedOmniSharpTestHost.Workspace, forwarder, service); + var controller = new DiagnosticsService(forwarder, service); var response = await controller.Handle(new DiagnosticsRequest()); - await emitter.Emitted; - - Assert.Single(messages); - var message = messages.First(); - Assert.Equal(2, message.Results.Count()); - - var a = message.Results.First(x => x.FileName == filename1); - Assert.Single(a.QuickFixes); - Assert.Equal(filename1, a.FileName); - - var b = message.Results.First(x => x.FileName == filename2); - Assert.Single(b.QuickFixes); - Assert.Equal(filename2, b.FileName); + await emitter.ExpectForEmitted(msg => msg.Results + .Any(r => r.FileName == filename1 && r.QuickFixes.Count() == 1)); + await emitter.ExpectForEmitted(msg => msg.Results + .Any(r => r.FileName == filename2 && r.QuickFixes.Count() == 1)); } [Theory(Skip = "Test needs to be updated for service changes")] @@ -82,15 +76,17 @@ public async Task CheckAllFiles(string filename1, string filename2) [InlineData("a.csx", "b.csx")] public async Task EnablesWhenEndPointIsHit(string filename1, string filename2) { + SharedOmniSharpTestHost.ClearWorkspace(); + var testFile1 = new TestFile(filename1, "class C1 { int n = true; }"); var testFile2 = new TestFile(filename2, "class C2 { int n = true; }"); SharedOmniSharpTestHost.AddFilesToWorkspace(testFile1, testFile2); - var messages = new List(); - var emitter = new DiagnosticTestEmitter(messages); + + var emitter = new DiagnosticTestEmitter(); var forwarder = new DiagnosticEventForwarder(emitter); - var service = new CSharpDiagnosticService(SharedOmniSharpTestHost.Workspace, forwarder, this.LoggerFactory); + var service = CreateDiagnosticService(forwarder); - var controller = new DiagnosticsService(SharedOmniSharpTestHost.Workspace, forwarder, service); + var controller = new DiagnosticsService(forwarder, service); var response = await controller.Handle(new DiagnosticsRequest()); Assert.True(forwarder.IsEnabled); diff --git a/tests/TestUtility/OmniSharpTestHost.cs b/tests/TestUtility/OmniSharpTestHost.cs index e20be7cfa0..e81b14f6e6 100644 --- a/tests/TestUtility/OmniSharpTestHost.cs +++ b/tests/TestUtility/OmniSharpTestHost.cs @@ -4,6 +4,7 @@ using System.Composition.Hosting.Core; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.Extensions.DependencyInjection; @@ -42,6 +43,7 @@ public class OmniSharpTestHost : DisposableObject private readonly CompositionHost _compositionHost; private Dictionary<(string name, string language), Lazy> _handlers; + private readonly string _originalCreatorToTrackDownMissedDisposes; public OmniSharpWorkspace Workspace { get; } public ILoggerFactory LoggerFactory { get; } @@ -49,7 +51,8 @@ public class OmniSharpTestHost : DisposableObject private OmniSharpTestHost( IServiceProvider serviceProvider, - CompositionHost compositionHost) + CompositionHost compositionHost, + string originalCreatorToTrackDownMissedDisposes) { _serviceProvider = serviceProvider; _compositionHost = compositionHost; @@ -57,11 +60,12 @@ private OmniSharpTestHost( this.Workspace = compositionHost.GetExport(); this.LoggerFactory = _serviceProvider.GetRequiredService(); this.Logger = this.LoggerFactory.CreateLogger(); + _originalCreatorToTrackDownMissedDisposes = originalCreatorToTrackDownMissedDisposes; } ~OmniSharpTestHost() { - throw new InvalidOperationException($"{nameof(OmniSharpTestHost)}.{nameof(Dispose)}() not called."); + throw new InvalidOperationException($"{nameof(OmniSharpTestHost)}.{nameof(Dispose)}() not called, creation of object originated from {_originalCreatorToTrackDownMissedDisposes}."); } protected override void DisposeCore(bool disposing) @@ -75,14 +79,15 @@ protected override void DisposeCore(bool disposing) public static OmniSharpTestHost Create( IServiceProvider serviceProvider, - IEnumerable additionalExports = null) + IEnumerable additionalExports = null, + [CallerMemberName] string callerName = "") { var compositionHost = new CompositionHostBuilder(serviceProvider, s_lazyAssemblies.Value, additionalExports) .Build(); WorkspaceInitializer.Initialize(serviceProvider, compositionHost); - var host = new OmniSharpTestHost(serviceProvider, compositionHost); + var host = new OmniSharpTestHost(serviceProvider, compositionHost, callerName); // Force workspace to be updated var service = host.GetWorkspaceInformationService(); @@ -96,12 +101,14 @@ public static OmniSharpTestHost Create( ITestOutputHelper testOutput = null, IEnumerable> configurationData = null, DotNetCliVersion dotNetCliVersion = DotNetCliVersion.Current, - IEnumerable additionalExports = null) + IEnumerable additionalExports = null, + [CallerMemberName] string callerName = "") { var environment = new OmniSharpEnvironment(path, logLevel: LogLevel.Trace); + var serviceProvider = TestServiceProvider.Create(testOutput, environment, configurationData, dotNetCliVersion); - return Create(serviceProvider, additionalExports); + return Create(serviceProvider, additionalExports, callerName); } public T GetExport() diff --git a/tests/TestUtility/TestHelpers.cs b/tests/TestUtility/TestHelpers.cs index 1820c81fa0..260197aca3 100644 --- a/tests/TestUtility/TestHelpers.cs +++ b/tests/TestUtility/TestHelpers.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.IO; using System.Linq; using System.Reflection; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Scripting.Hosting; using Microsoft.Extensions.Logging; using OmniSharp; @@ -39,11 +41,12 @@ public static void AddCsxProjectToWorkspace(OmniSharpWorkspace workspace, TestFi workspace.AddDocument(documentInfo); } - public static void AddProjectToWorkspace(OmniSharpWorkspace workspace, string filePath, string[] frameworks, TestFile[] testFiles) + public static IEnumerable AddProjectToWorkspace(OmniSharpWorkspace workspace, string filePath, string[] frameworks, TestFile[] testFiles, ImmutableArray analyzerRefs = default) { var versionStamp = VersionStamp.Create(); var references = GetReferences(); frameworks = frameworks ?? new[] { string.Empty }; + var projectsIds = new List(); foreach (var framework in frameworks) { @@ -54,7 +57,8 @@ public static void AddProjectToWorkspace(OmniSharpWorkspace workspace, string fi assemblyName: "AssemblyName", language: LanguageNames.CSharp, filePath: filePath, - metadataReferences: references); + metadataReferences: references, + analyzerReferences: analyzerRefs); workspace.AddProject(projectInfo); @@ -69,7 +73,11 @@ public static void AddProjectToWorkspace(OmniSharpWorkspace workspace, string fi workspace.AddDocument(documentInfo); } + + projectsIds.Add(projectInfo.Id); } + + return projectsIds; } private static IEnumerable GetReferences() @@ -110,5 +118,19 @@ public static MSBuildInstance AddDotNetCoreToFakeInstance(this MSBuildInstance i return instance; } + + public static Dictionary GetConfigurationDataWithAnalyzerConfig(bool roslynAnalyzersEnabled = false, Dictionary existingConfiguration = null) + { + if(existingConfiguration == null) + { + return new Dictionary() { { "RoslynExtensionsOptions:EnableAnalyzersSupport", roslynAnalyzersEnabled.ToString() } }; + } + + var copyOfExistingConfigs = existingConfiguration.ToDictionary(x => x.Key, x => x.Value); + copyOfExistingConfigs.Add("RoslynExtensionsOptions:EnableAnalyzersSupport", roslynAnalyzersEnabled.ToString()); + + return copyOfExistingConfigs; + + } } } diff --git a/tests/TestUtility/TestHostExtensions.cs b/tests/TestUtility/TestHostExtensions.cs index 398f1bdbce..042925dcc1 100644 --- a/tests/TestUtility/TestHostExtensions.cs +++ b/tests/TestUtility/TestHostExtensions.cs @@ -30,11 +30,11 @@ public static async Task RequestMSBuildWorkspaceInfoAsync( return (MSBuildWorkspaceInfo)response["MsBuild"]; } - public static async Task RequestCodeCheckAsync(this OmniSharpTestHost host, string filePath) + public static async Task RequestCodeCheckAsync(this OmniSharpTestHost host, string filePath = null) { var service = host.GetCodeCheckService(); - var request = new CodeCheckRequest { FileName = filePath }; + var request = filePath == null ? new CodeCheckRequest() : new CodeCheckRequest { FileName = filePath }; return await service.Handle(request); } diff --git a/tests/TestUtility/TestServiceProvider.cs b/tests/TestUtility/TestServiceProvider.cs index 15efceda42..3babee6ec5 100644 --- a/tests/TestUtility/TestServiceProvider.cs +++ b/tests/TestUtility/TestServiceProvider.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using Microsoft.CodeAnalysis; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; @@ -46,6 +47,7 @@ private TestServiceProvider( AddService(dotNetCliService); AddService(configuration); AddService(optionsMonitor); + _services[typeof(IAnalyzerAssemblyLoader)] = assemblyLoader; } public static IServiceProvider Create(