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