From 17fa11defadefad15d99b7d3b7cd02a9ca3a485e Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Wed, 1 Nov 2017 06:22:44 -0700 Subject: [PATCH 01/10] Properly add/remove metadata references in MSBuild project system I recently noticed an issue that has likely been present for a long while. Essentially, metadata references are never removed from the workspace. The reason for this is because Roslyn's MetadataReference class does not implement value equality -- it just inherits the default `Equals()`/`GetHashCode()` implementations from `System.Object`. However, the algorithm for adding and removing metadata references assumes value equality. This is easily fixed by adding our own `IEqualityComparer` that first checks to ensure that `MetadataReferences` are `PortableExecutableMetadataReferences` and then uses their file paths for equality. --- src/OmniSharp.MSBuild/MSBuildProjectSystem.cs | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/src/OmniSharp.MSBuild/MSBuildProjectSystem.cs b/src/OmniSharp.MSBuild/MSBuildProjectSystem.cs index dfe07e6f05..3d526a6649 100644 --- a/src/OmniSharp.MSBuild/MSBuildProjectSystem.cs +++ b/src/OmniSharp.MSBuild/MSBuildProjectSystem.cs @@ -510,12 +510,27 @@ private void UpdateProjectReferences(Project project, ImmutableArray pro } } - private void UpdateReferences(Project project, ImmutableArray references) + private class MetadataReferenceComparer : IEqualityComparer { - var existingReferences = new HashSet(project.MetadataReferences); - var addedReferences = new HashSet(); + public static MetadataReferenceComparer Instance { get; } = new MetadataReferenceComparer(); - foreach (var referencePath in references) + public bool Equals(MetadataReference x, MetadataReference y) + => x is PortableExecutableReference pe1 && y is PortableExecutableReference pe2 + ? StringComparer.OrdinalIgnoreCase.Equals(pe1.FilePath, pe2.FilePath) + : EqualityComparer.Default.Equals(x, y); + + public int GetHashCode(MetadataReference obj) + => obj is PortableExecutableReference pe + ? StringComparer.OrdinalIgnoreCase.GetHashCode(pe.FilePath) + : EqualityComparer.Default.GetHashCode(obj); + } + + private void UpdateReferences(Project project, ImmutableArray referencePaths) + { + var referencesToRemove = new HashSet(project.MetadataReferences, MetadataReferenceComparer.Instance); + var referencesToAdd = new HashSet(MetadataReferenceComparer.Instance); + + foreach (var referencePath in referencePaths) { if (!File.Exists(referencePath)) { @@ -523,25 +538,25 @@ private void UpdateReferences(Project project, ImmutableArray references } else { - var metadataReference = _metadataFileReferenceCache.GetMetadataReference(referencePath); + var reference = _metadataFileReferenceCache.GetMetadataReference(referencePath); - if (existingReferences.Remove(metadataReference)) + if (referencesToRemove.Remove(reference)) { continue; } - if (!addedReferences.Contains(metadataReference)) + if (!referencesToAdd.Contains(reference)) { _logger.LogDebug($"Adding reference '{referencePath}' to '{project.Name}'."); - _workspace.AddMetadataReference(project.Id, metadataReference); - addedReferences.Add(metadataReference); + _workspace.AddMetadataReference(project.Id, reference); + referencesToAdd.Add(reference); } } } - foreach (var existingReference in existingReferences) + foreach (var reference in referencesToRemove) { - _workspace.RemoveMetadataReference(project.Id, existingReference); + _workspace.RemoveMetadataReference(project.Id, reference); } } From 797455a529831f44f09fc2dc008aa3b87996ca6e Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Thu, 2 Nov 2017 07:36:57 -0700 Subject: [PATCH 02/10] Refactor file watching/notification This change unifies file and directory watching, separates watching and notification concerns in the API, and fixes the following issues: 1. Paths are treated case-insensitively as they are in the rest of OmniSharp. 2. Access to the internal dictionary of callbacks is synchronized. --- .../FileWatching/IFileSystemNotifier.cs | 14 ++++++ .../FileWatching/IFileSystemWatcher.cs | 18 +++---- src/OmniSharp.Host/CompositionHostBuilder.cs | 1 + .../FileWatching/ManualFileSystemWatcher.cs | 50 ++++++++++--------- src/OmniSharp.MSBuild/MSBuildProjectSystem.cs | 3 +- .../Services/Files/OnFilesChangedService.cs | 10 ++-- .../FilesChangedFacts.cs | 4 +- 7 files changed, 59 insertions(+), 41 deletions(-) create mode 100644 src/OmniSharp.Abstractions/FileWatching/IFileSystemNotifier.cs diff --git a/src/OmniSharp.Abstractions/FileWatching/IFileSystemNotifier.cs b/src/OmniSharp.Abstractions/FileWatching/IFileSystemNotifier.cs new file mode 100644 index 0000000000..35c4cd2b64 --- /dev/null +++ b/src/OmniSharp.Abstractions/FileWatching/IFileSystemNotifier.cs @@ -0,0 +1,14 @@ +using OmniSharp.Models.FilesChanged; + +namespace OmniSharp.FileWatching +{ + public interface IFileSystemNotifier + { + /// + /// Notifiers any relevant file system watchers when a file is created, changed, or deleted. + /// + /// The path to the file that was changed. + /// The type of change. Hosts are not required to pass a change type. + void Notify(string filePath, FileChangeType changeType = FileChangeType.Unspecified); + } +} diff --git a/src/OmniSharp.Abstractions/FileWatching/IFileSystemWatcher.cs b/src/OmniSharp.Abstractions/FileWatching/IFileSystemWatcher.cs index 71d7d146de..9e6f4d0c46 100644 --- a/src/OmniSharp.Abstractions/FileWatching/IFileSystemWatcher.cs +++ b/src/OmniSharp.Abstractions/FileWatching/IFileSystemWatcher.cs @@ -1,20 +1,16 @@ -using System; -using OmniSharp.Models.FilesChanged; +using OmniSharp.Models.FilesChanged; namespace OmniSharp.FileWatching { - // TODO: Flesh out this API more + public delegate void FileSystemNotificationCallback(string filePath, FileChangeType changeType); + public interface IFileSystemWatcher { - void Watch(string path, Action callback); - /// - /// Called when a file is created, changed, or deleted. + /// Call to watch a file or directory path for changes. /// - /// The path to the file - /// The type of change. Hosts are not required to pass a change type - void TriggerChange(string path, FileChangeType changeType); - - void WatchDirectory(string path, Action callback); + /// The file or directory path to watch. + /// The callback that will be invoked when a change occurs in the watched file or directory. + void Watch(string fileOrDirectoryPath, FileSystemNotificationCallback callback); } } diff --git a/src/OmniSharp.Host/CompositionHostBuilder.cs b/src/OmniSharp.Host/CompositionHostBuilder.cs index 65967b5818..6a786d8e5e 100644 --- a/src/OmniSharp.Host/CompositionHostBuilder.cs +++ b/src/OmniSharp.Host/CompositionHostBuilder.cs @@ -70,6 +70,7 @@ public CompositionHost Build() config = config .WithProvider(MefValueProvider.From(_serviceProvider)) + .WithProvider(MefValueProvider.From(fileSystemWatcher)) .WithProvider(MefValueProvider.From(fileSystemWatcher)) .WithProvider(MefValueProvider.From(memoryCache)) .WithProvider(MefValueProvider.From(loggerFactory)) diff --git a/src/OmniSharp.Host/FileWatching/ManualFileSystemWatcher.cs b/src/OmniSharp.Host/FileWatching/ManualFileSystemWatcher.cs index 548f3dbd43..4c85f97897 100644 --- a/src/OmniSharp.Host/FileWatching/ManualFileSystemWatcher.cs +++ b/src/OmniSharp.Host/FileWatching/ManualFileSystemWatcher.cs @@ -5,39 +5,43 @@ namespace OmniSharp.FileWatching { - public class ManualFileSystemWatcher : IFileSystemWatcher + internal class ManualFileSystemWatcher : IFileSystemWatcher, IFileSystemNotifier { - private readonly Dictionary> _callbacks = new Dictionary>(); - private readonly Dictionary> _directoryCallBacks = new Dictionary>(); + private readonly object _gate = new object(); + private readonly Dictionary _callbacks; - public void TriggerChange(string path, FileChangeType changeType) + public ManualFileSystemWatcher() { - if (_callbacks.TryGetValue(path, out var callback)) - { - callback(path, changeType); - } - - var directoryPath = Path.GetDirectoryName(path); - if (_directoryCallBacks.TryGetValue(directoryPath, out var fileCallback)) - { - fileCallback(path, changeType); - } + _callbacks = new Dictionary(StringComparer.OrdinalIgnoreCase); } - public void Watch(string path, Action callback) + public void Notify(string filePath, FileChangeType changeType = FileChangeType.Unspecified) { - _callbacks[path] = callback; + lock (_gate) + { + if (_callbacks.TryGetValue(filePath, out var fileCallback)) + { + fileCallback(filePath, changeType); + } + + var directoryPath = Path.GetDirectoryName(filePath); + if (_callbacks.TryGetValue(directoryPath, out var directoryCallback)) + { + directoryCallback(filePath, changeType); + } + } } - public void WatchDirectory(string path, Action callback) + public void Watch(string fileOrDirectoryPath, FileSystemNotificationCallback callback) { - if (_directoryCallBacks.TryGetValue(path, out var existingCallback)) + lock (_gate) { - _directoryCallBacks[path] = callback + existingCallback; - } - else - { - _directoryCallBacks[path] = callback; + if (_callbacks.TryGetValue(fileOrDirectoryPath, out var existingCallback)) + { + callback = callback + existingCallback; + } + + _callbacks[fileOrDirectoryPath] = callback; } } } diff --git a/src/OmniSharp.MSBuild/MSBuildProjectSystem.cs b/src/OmniSharp.MSBuild/MSBuildProjectSystem.cs index 3d526a6649..c171d9c194 100644 --- a/src/OmniSharp.MSBuild/MSBuildProjectSystem.cs +++ b/src/OmniSharp.MSBuild/MSBuildProjectSystem.cs @@ -402,7 +402,8 @@ private void UpdateSourceFiles(Project project, IList sourceFiles) } } - private void WatchDirectoryContainingFile(string sourceFile) => _fileSystemWatcher.WatchDirectory(Path.GetDirectoryName(sourceFile), OnDirectoryFileChanged); + private void WatchDirectoryContainingFile(string sourceFile) + => _fileSystemWatcher.Watch(Path.GetDirectoryName(sourceFile), OnDirectoryFileChanged); private void OnDirectoryFileChanged(string path, FileChangeType changeType) { diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Files/OnFilesChangedService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Files/OnFilesChangedService.cs index 53148f8ef5..d7bc36fe8e 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Files/OnFilesChangedService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Files/OnFilesChangedService.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Composition; using System.Threading.Tasks; @@ -11,20 +12,21 @@ namespace OmniSharp.Roslyn.CSharp.Services.Files [OmniSharpHandler(OmniSharpEndpoints.FilesChanged, LanguageNames.CSharp)] public class OnFilesChangedService : IRequestHandler, FilesChangedResponse> { - private readonly IFileSystemWatcher _watcher; + private readonly IFileSystemNotifier _notifier; [ImportingConstructor] - public OnFilesChangedService(IFileSystemWatcher watcher) + public OnFilesChangedService(IFileSystemNotifier notifier) { - _watcher = watcher; + _notifier = notifier ?? throw new ArgumentNullException(nameof(notifier)); } public Task Handle(IEnumerable requests) { foreach (var request in requests) { - _watcher.TriggerChange(request.FileName, request.ChangeType); + _notifier.Notify(request.FileName, request.ChangeType); } + return Task.FromResult(new FilesChangedResponse()); } } diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/FilesChangedFacts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/FilesChangedFacts.cs index daac500e3a..799cfb764f 100644 --- a/tests/OmniSharp.Roslyn.CSharp.Tests/FilesChangedFacts.cs +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/FilesChangedFacts.cs @@ -45,8 +45,8 @@ public void TestMultipleDirectoryWatchers() bool firstWatcherCalled = false; bool secondWatcherCalled = false; - watcher.WatchDirectory("", (path, changeType) => { firstWatcherCalled = true; }); - watcher.WatchDirectory("", (path, changeType) => { secondWatcherCalled = true; }); + watcher.Watch("", (path, changeType) => { firstWatcherCalled = true; }); + watcher.Watch("", (path, changeType) => { secondWatcherCalled = true; }); var handler = GetRequestHandler(host); handler.Handle(new[] { new FilesChangedRequest() { FileName = "FileName.cs", ChangeType = FileChangeType.Create } }); From a38a707ab77f710d83d35db99b66ebee73be81a5 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Thu, 2 Nov 2017 10:38:14 -0700 Subject: [PATCH 03/10] Clean up MSBuild logging somewhat --- .../Logging/MSBuildDiagnostic.cs | 43 +++++++++++ .../Logging/MSBuildDiagnosticSeverity.cs | 8 ++ .../Logging/MSBuildLogger.cs | 39 ++++++++++ src/OmniSharp.MSBuild/MSBuildLogForwarder.cs | 77 ------------------- src/OmniSharp.MSBuild/MSBuildProjectSystem.cs | 11 ++- .../Models/Events/IEventEmitterExtensions.cs | 13 +++- .../Events/MSBuildDiagnosticsMessage.cs | 16 +++- .../ProjectFile/ProjectFileInfo.cs | 44 ++++++----- .../ProjectFileInfoTests.cs | 5 +- 9 files changed, 149 insertions(+), 107 deletions(-) create mode 100644 src/OmniSharp.MSBuild/Logging/MSBuildDiagnostic.cs create mode 100644 src/OmniSharp.MSBuild/Logging/MSBuildDiagnosticSeverity.cs create mode 100644 src/OmniSharp.MSBuild/Logging/MSBuildLogger.cs delete mode 100644 src/OmniSharp.MSBuild/MSBuildLogForwarder.cs diff --git a/src/OmniSharp.MSBuild/Logging/MSBuildDiagnostic.cs b/src/OmniSharp.MSBuild/Logging/MSBuildDiagnostic.cs new file mode 100644 index 0000000000..8cc9cf1baf --- /dev/null +++ b/src/OmniSharp.MSBuild/Logging/MSBuildDiagnostic.cs @@ -0,0 +1,43 @@ +namespace OmniSharp.MSBuild.Logging +{ + public class MSBuildDiagnostic + { + public MSBuildDiagnosticSeverity Severity { get; } + public string Message { get; } + public string File { get; } + public string ProjectFile { get; } + public string Subcategory { get; } + public string Code { get; } + public int LineNumber { get; } + public int ColumnNumber { get; } + public int EndLineNumber { get; } + public int EndColumnNumber { get; } + + private MSBuildDiagnostic( + MSBuildDiagnosticSeverity severity, + string message, string file, string projectFile, string subcategory, string code, + int lineNumber, int columnNumber, int endLineNumber, int endColumnNumber) + { + Severity = severity; + Message = message; + File = file; + ProjectFile = projectFile; + Subcategory = subcategory; + Code = code; + LineNumber = lineNumber; + ColumnNumber = columnNumber; + EndLineNumber = endLineNumber; + EndColumnNumber = endColumnNumber; + } + + public static MSBuildDiagnostic CreateFrom(Microsoft.Build.Framework.BuildErrorEventArgs args) + => new MSBuildDiagnostic(MSBuildDiagnosticSeverity.Error, + args.Message, args.File, args.ProjectFile, args.Subcategory, args.Code, + args.LineNumber, args.ColumnNumber, args.EndLineNumber, args.EndColumnNumber); + + public static MSBuildDiagnostic CreateFrom(Microsoft.Build.Framework.BuildWarningEventArgs args) + => new MSBuildDiagnostic(MSBuildDiagnosticSeverity.Error, + args.Message, args.File, args.ProjectFile, args.Subcategory, args.Code, + args.LineNumber, args.ColumnNumber, args.EndLineNumber, args.EndColumnNumber); + } +} diff --git a/src/OmniSharp.MSBuild/Logging/MSBuildDiagnosticSeverity.cs b/src/OmniSharp.MSBuild/Logging/MSBuildDiagnosticSeverity.cs new file mode 100644 index 0000000000..ec771e3830 --- /dev/null +++ b/src/OmniSharp.MSBuild/Logging/MSBuildDiagnosticSeverity.cs @@ -0,0 +1,8 @@ +namespace OmniSharp.MSBuild.Logging +{ + public enum MSBuildDiagnosticSeverity + { + Error, + Warning + } +} diff --git a/src/OmniSharp.MSBuild/Logging/MSBuildLogger.cs b/src/OmniSharp.MSBuild/Logging/MSBuildLogger.cs new file mode 100644 index 0000000000..12a652a731 --- /dev/null +++ b/src/OmniSharp.MSBuild/Logging/MSBuildLogger.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using Microsoft.Extensions.Logging; + +namespace OmniSharp.MSBuild.Logging +{ + internal class MSBuildLogger : Microsoft.Build.Utilities.Logger + { + private readonly ILogger _logger; + private readonly List _diagnostics; + + public MSBuildLogger(ILogger logger) + { + _logger = logger; + _diagnostics = new List(); + } + + public override void Initialize(Microsoft.Build.Framework.IEventSource eventSource) + { + eventSource.ErrorRaised += OnError; + eventSource.WarningRaised += OnWarning; + } + + public ImmutableArray GetDiagnostics() => + _diagnostics.ToImmutableArray(); + + private void OnError(object sender, Microsoft.Build.Framework.BuildErrorEventArgs args) + { + _logger.LogError(args.Message); + _diagnostics.Add(MSBuildDiagnostic.CreateFrom(args)); + } + + private void OnWarning(object sender, Microsoft.Build.Framework.BuildWarningEventArgs args) + { + _logger.LogWarning(args.Message); + _diagnostics.Add(MSBuildDiagnostic.CreateFrom(args)); + } + } +} diff --git a/src/OmniSharp.MSBuild/MSBuildLogForwarder.cs b/src/OmniSharp.MSBuild/MSBuildLogForwarder.cs deleted file mode 100644 index 39d06afeb2..0000000000 --- a/src/OmniSharp.MSBuild/MSBuildLogForwarder.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.Extensions.Logging; -using OmniSharp.MSBuild.Models.Events; - -namespace OmniSharp.MSBuild -{ - public class MSBuildLogForwarder : Microsoft.Build.Framework.ILogger - { - private readonly ILogger _logger; - private readonly ICollection _diagnostics; - private readonly IList _callOnShutdown; - - public MSBuildLogForwarder(ILogger logger, ICollection diagnostics) - { - _logger = logger; - _diagnostics = diagnostics; - _callOnShutdown = new List(); - } - - public string Parameters { get; set; } - - public Microsoft.Build.Framework.LoggerVerbosity Verbosity { get; set; } - - public void Initialize(Microsoft.Build.Framework.IEventSource eventSource) - { - eventSource.ErrorRaised += OnError; - eventSource.WarningRaised += OnWarning; - _callOnShutdown.Add(() => - { - eventSource.ErrorRaised -= OnError; - eventSource.WarningRaised -= OnWarning; - }); - } - - public void Shutdown() - { - foreach (var action in _callOnShutdown) - { - action(); - } - } - - private void AddDiagnostic(string logLevel, string fileName, string message, int lineNumber, int columnNumber, int endLineNumber, int endColumnNumber) - { - if (_diagnostics == null) - { - return; - } - - _diagnostics.Add(new MSBuildDiagnosticsMessage() - { - LogLevel = logLevel, - FileName = fileName, - Text = message, - StartLine = lineNumber, - StartColumn = columnNumber, - EndLine = endLineNumber, - EndColumn = endColumnNumber - }); - } - - private void OnError(object sender, Microsoft.Build.Framework.BuildErrorEventArgs args) - { - _logger.LogError(args.Message); - - AddDiagnostic("Error", args.File, args.Message, args.LineNumber, args.ColumnNumber, args.EndLineNumber, args.EndColumnNumber); - } - - private void OnWarning(object sender, Microsoft.Build.Framework.BuildWarningEventArgs args) - { - _logger.LogWarning(args.Message); - - AddDiagnostic("Warning", args.File, args.Message, args.LineNumber, args.ColumnNumber, args.EndLineNumber, args.EndColumnNumber); - } - } -} \ No newline at end of file diff --git a/src/OmniSharp.MSBuild/MSBuildProjectSystem.cs b/src/OmniSharp.MSBuild/MSBuildProjectSystem.cs index c171d9c194..49452c5190 100644 --- a/src/OmniSharp.MSBuild/MSBuildProjectSystem.cs +++ b/src/OmniSharp.MSBuild/MSBuildProjectSystem.cs @@ -16,6 +16,7 @@ using OmniSharp.Models.UpdateBuffer; using OmniSharp.Models.WorkspaceInformation; using OmniSharp.MSBuild.Discovery; +using OmniSharp.MSBuild.Logging; using OmniSharp.MSBuild.Models; using OmniSharp.MSBuild.Models.Events; using OmniSharp.MSBuild.ProjectFile; @@ -311,11 +312,11 @@ private ProjectFileInfo LoadProject(string projectFilePath) _logger.LogInformation($"Loading project: {projectFilePath}"); ProjectFileInfo project; - var diagnostics = new List(); + ImmutableArray diagnostics; try { - project = ProjectFileInfo.Create(projectFilePath, _environment.TargetDirectory, _loggerFactory.CreateLogger(), _msbuildInstance, _options, diagnostics); + (project, diagnostics) = ProjectFileInfo.Create(projectFilePath, _environment.TargetDirectory, _loggerFactory.CreateLogger(), _msbuildInstance, _options); if (project == null) { @@ -340,8 +341,10 @@ private void OnProjectChanged(string projectFilePath, bool allowAutoRestore) { if (_projects.TryGetValue(projectFilePath, out var oldProjectFileInfo)) { - var diagnostics = new List(); - var newProjectFileInfo = oldProjectFileInfo.Reload(_environment.TargetDirectory, _loggerFactory.CreateLogger(), _msbuildInstance, _options, diagnostics); + ProjectFileInfo newProjectFileInfo; + ImmutableArray diagnostics; + + (newProjectFileInfo, diagnostics) = oldProjectFileInfo.Reload(_environment.TargetDirectory, _loggerFactory.CreateLogger(), _msbuildInstance, _options); if (newProjectFileInfo != null) { diff --git a/src/OmniSharp.MSBuild/Models/Events/IEventEmitterExtensions.cs b/src/OmniSharp.MSBuild/Models/Events/IEventEmitterExtensions.cs index 3adbb459f6..eb01544eb7 100644 --- a/src/OmniSharp.MSBuild/Models/Events/IEventEmitterExtensions.cs +++ b/src/OmniSharp.MSBuild/Models/Events/IEventEmitterExtensions.cs @@ -1,19 +1,26 @@ using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using OmniSharp.Eventing; +using OmniSharp.MSBuild.Logging; namespace OmniSharp.MSBuild.Models.Events { internal static class IEventEmitterExtensions { - public static void MSBuildProjectDiagnostics(this IEventEmitter eventEmitter, string projectFilePath, IEnumerable diagnostics) + public static void MSBuildProjectDiagnostics(this IEventEmitter eventEmitter, string projectFilePath, ImmutableArray diagnostics) { eventEmitter.Emit(MSBuildProjectDiagnosticsEvent.Id, new MSBuildProjectDiagnosticsEvent() { FileName = projectFilePath, - Warnings = diagnostics.Where(d => d.LogLevel == "Warning"), - Errors = diagnostics.Where(d => d.LogLevel == "Error"), + Warnings = SelectMessages(diagnostics, MSBuildDiagnosticSeverity.Warning), + Errors = SelectMessages(diagnostics, MSBuildDiagnosticSeverity.Error) }); } + + private static IEnumerable SelectMessages(ImmutableArray diagnostics, MSBuildDiagnosticSeverity severity) + => diagnostics + .Where(d => d.Severity == severity) + .Select(MSBuildDiagnosticsMessage.FromDiagnostic); } } diff --git a/src/OmniSharp.MSBuild/Models/Events/MSBuildDiagnosticsMessage.cs b/src/OmniSharp.MSBuild/Models/Events/MSBuildDiagnosticsMessage.cs index 20c9d19baf..80b96c5b3f 100644 --- a/src/OmniSharp.MSBuild/Models/Events/MSBuildDiagnosticsMessage.cs +++ b/src/OmniSharp.MSBuild/Models/Events/MSBuildDiagnosticsMessage.cs @@ -1,3 +1,5 @@ +using OmniSharp.MSBuild.Logging; + namespace OmniSharp.MSBuild.Models.Events { public class MSBuildDiagnosticsMessage @@ -9,5 +11,17 @@ public class MSBuildDiagnosticsMessage public int StartColumn { get; set; } public int EndLine { get; set; } public int EndColumn { get; set; } + + public static MSBuildDiagnosticsMessage FromDiagnostic(MSBuildDiagnostic diagnostic) + => new MSBuildDiagnosticsMessage() + { + LogLevel = diagnostic.Severity.ToString(), + FileName = diagnostic.File, + Text = diagnostic.Message, + StartLine = diagnostic.LineNumber, + StartColumn = diagnostic.ColumnNumber, + EndLine = diagnostic.EndLineNumber, + EndColumn = diagnostic.EndColumnNumber + }; } -} \ No newline at end of file +} diff --git a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.cs b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.cs index 5e24def329..3aa67eb484 100644 --- a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.cs +++ b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.cs @@ -11,7 +11,7 @@ using Microsoft.Extensions.Logging; using NuGet.Packaging.Core; using OmniSharp.MSBuild.Discovery; -using OmniSharp.MSBuild.Models.Events; +using OmniSharp.MSBuild.Logging; using OmniSharp.Options; namespace OmniSharp.MSBuild.ProjectFile @@ -69,30 +69,30 @@ private ProjectFileInfo( _data = data; } - public static ProjectFileInfo Create( - string filePath, string solutionDirectory, ILogger logger, - MSBuildInstance msbuildInstance, MSBuildOptions options = null, ICollection diagnostics = null) + public static (ProjectFileInfo projectFileInfo, ImmutableArray diagnostics) Create( + string filePath, string solutionDirectory, ILogger logger, MSBuildInstance msbuildInstance, MSBuildOptions options = null) { if (!File.Exists(filePath)) { - return null; + return (null, ImmutableArray.Empty); } - var projectInstance = LoadProject(filePath, solutionDirectory, logger, msbuildInstance, options, diagnostics, out var targetFrameworks); + var (projectInstance, diagnostics) = LoadProject(filePath, solutionDirectory, logger, msbuildInstance, options, out var targetFrameworks); if (projectInstance == null) { - return null; + return (null, diagnostics); } var id = ProjectId.CreateNewId(debugName: filePath); var data = CreateProjectData(projectInstance, targetFrameworks); + var projectFileInfo = new ProjectFileInfo(id, filePath, data); - return new ProjectFileInfo(id, filePath, data); + return (projectFileInfo, diagnostics); } - private static ProjectInstance LoadProject( + private static (ProjectInstance projectInstance, ImmutableArray diagnostics) LoadProject( string filePath, string solutionDirectory, ILogger logger, - MSBuildInstance msbuildInstance, MSBuildOptions options, ICollection diagnostics, out ImmutableArray targetFrameworks) + MSBuildInstance msbuildInstance, MSBuildOptions options, out ImmutableArray targetFrameworks) { options = options ?? new MSBuildOptions(); @@ -130,12 +130,16 @@ private static ProjectInstance LoadProject( } var projectInstance = project.CreateProjectInstance(); - var buildResult = projectInstance.Build(new string[] { TargetNames.Compile, TargetNames.CoreCompile }, - new[] { new MSBuildLogForwarder(logger, diagnostics) }); + var msbuildLogger = new MSBuildLogger(logger); + var buildResult = projectInstance.Build( + targets: new string[] { TargetNames.Compile, TargetNames.CoreCompile }, + loggers: new[] { msbuildLogger }); + + var diagnostics = msbuildLogger.GetDiagnostics(); return buildResult - ? projectInstance - : null; + ? (projectInstance, diagnostics) + : (null, diagnostics); } private static string GetLegalToolsetVersion(string toolsVersion, ICollection toolsets) @@ -219,19 +223,19 @@ private static ProjectData CreateProjectData(ProjectInstance projectInstance, Im sourceFiles, projectReferences, references, packageReferences, analyzers); } - public ProjectFileInfo Reload( - string solutionDirectory, ILogger logger, - MSBuildInstance msbuildInstance, MSBuildOptions options = null, ICollection diagnostics = null) + public (ProjectFileInfo projectFileInfo, ImmutableArray diagnostics) Reload( + string solutionDirectory, ILogger logger, MSBuildInstance msbuildInstance, MSBuildOptions options = null) { - var projectInstance = LoadProject(FilePath, solutionDirectory, logger, msbuildInstance, options, diagnostics, out var targetFrameworks); + var (projectInstance, diagnostics) = LoadProject(FilePath, solutionDirectory, logger, msbuildInstance, options, out var targetFrameworks); if (projectInstance == null) { - return null; + return (null, diagnostics); } var data = CreateProjectData(projectInstance, targetFrameworks); + var projectFileInfo = new ProjectFileInfo(Id, FilePath, data); - return new ProjectFileInfo(Id, FilePath, data); + return (projectFileInfo, diagnostics); } public bool IsUnityProject() diff --git a/tests/OmniSharp.MSBuild.Tests/ProjectFileInfoTests.cs b/tests/OmniSharp.MSBuild.Tests/ProjectFileInfoTests.cs index 71210e14d4..6a20d42ff6 100644 --- a/tests/OmniSharp.MSBuild.Tests/ProjectFileInfoTests.cs +++ b/tests/OmniSharp.MSBuild.Tests/ProjectFileInfoTests.cs @@ -4,7 +4,6 @@ using Microsoft.Extensions.Logging; using OmniSharp.MSBuild.Discovery; using OmniSharp.MSBuild.ProjectFile; -using OmniSharp.Services; using TestUtility; using Xunit; using Xunit.Abstractions; @@ -27,7 +26,9 @@ private ProjectFileInfo CreateProjectFileInfo(OmniSharpTestHost host, ITestProje { var msbuildLocator = host.GetExport(); - return ProjectFileInfo.Create(projectFilePath, testProject.Directory, this._logger, msbuildLocator.RegisteredInstance); + var (projectFileInfo, _) = ProjectFileInfo.Create(projectFilePath, testProject.Directory, this._logger, msbuildLocator.RegisteredInstance); + + return projectFileInfo; } [Fact] From 2560f7a6932a336afc56f5d5f6dc665ea9597def Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Thu, 2 Nov 2017 10:45:10 -0700 Subject: [PATCH 04/10] Move logic to create ProjectData into ProjectData class --- .../ProjectFileInfo.ProjectData.cs | 103 +++++++++++++++++- .../ProjectFile/ProjectFileInfo.cs | 101 +---------------- 2 files changed, 108 insertions(+), 96 deletions(-) diff --git a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.ProjectData.cs b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.ProjectData.cs index 0bda8a2bf6..8994cc6c0a 100644 --- a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.ProjectData.cs +++ b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.ProjectData.cs @@ -1,8 +1,13 @@ using System; +using System.Collections.Generic; using System.Collections.Immutable; +using System.IO; +using System.Linq; using System.Runtime.Versioning; +using Microsoft.Build.Execution; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using NuGet.Packaging.Core; namespace OmniSharp.MSBuild.ProjectFile { @@ -37,7 +42,7 @@ private class ProjectData public ImmutableArray PackageReferences { get; } public ImmutableArray Analyzers { get; } - public ProjectData( + private ProjectData( Guid guid, string name, string assemblyName, string targetPath, string outputPath, string projectAssetsFile, FrameworkName targetFramework, @@ -83,6 +88,102 @@ public ProjectData( PackageReferences = packageReferences; Analyzers = analyzers; } + + public static ProjectData Create(ProjectInstance projectInstance) + { + var guid = PropertyConverter.ToGuid(projectInstance.GetPropertyValue(PropertyNames.ProjectGuid)); + var name = projectInstance.GetPropertyValue(PropertyNames.ProjectName); + var assemblyName = projectInstance.GetPropertyValue(PropertyNames.AssemblyName); + var targetPath = projectInstance.GetPropertyValue(PropertyNames.TargetPath); + var outputPath = projectInstance.GetPropertyValue(PropertyNames.OutputPath); + var projectAssetsFile = projectInstance.GetPropertyValue(PropertyNames.ProjectAssetsFile); + + var targetFramework = new FrameworkName(projectInstance.GetPropertyValue(PropertyNames.TargetFrameworkMoniker)); + + var targetFrameworkValue = projectInstance.GetPropertyValue(PropertyNames.TargetFramework); + var targetFrameworks = PropertyConverter.SplitList(projectInstance.GetPropertyValue(PropertyNames.TargetFrameworks), ';'); + + if (!string.IsNullOrWhiteSpace(targetFrameworkValue) && targetFrameworks.Length == 0) + { + targetFrameworks = ImmutableArray.Create(targetFrameworkValue); + } + + var languageVersion = PropertyConverter.ToLanguageVersion(projectInstance.GetPropertyValue(PropertyNames.LangVersion)); + var allowUnsafeCode = PropertyConverter.ToBoolean(projectInstance.GetPropertyValue(PropertyNames.AllowUnsafeBlocks), defaultValue: false); + var outputKind = PropertyConverter.ToOutputKind(projectInstance.GetPropertyValue(PropertyNames.OutputType)); + var documentationFile = projectInstance.GetPropertyValue(PropertyNames.DocumentationFile); + var preprocessorSymbolNames = PropertyConverter.ToPreprocessorSymbolNames(projectInstance.GetPropertyValue(PropertyNames.DefineConstants)); + var suppressDiagnosticIds = PropertyConverter.ToSuppressDiagnosticIds(projectInstance.GetPropertyValue(PropertyNames.NoWarn)); + var signAssembly = PropertyConverter.ToBoolean(projectInstance.GetPropertyValue(PropertyNames.SignAssembly), defaultValue: false); + var assemblyOriginatorKeyFile = projectInstance.GetPropertyValue(PropertyNames.AssemblyOriginatorKeyFile); + + var sourceFiles = GetFullPaths( + projectInstance.GetItems(ItemNames.Compile), filter: FileNameIsNotGenerated); + var projectReferences = GetFullPaths(projectInstance.GetItems(ItemNames.ProjectReference)); + var references = GetFullPaths( + projectInstance.GetItems(ItemNames.ReferencePath).Where(ReferenceSourceTargetIsNotProjectReference)); + var packageReferences = GetPackageReferences(projectInstance.GetItems(ItemNames.PackageReference)); + var analyzers = GetFullPaths(projectInstance.GetItems(ItemNames.Analyzer)); + + return new ProjectData(guid, name, + assemblyName, targetPath, outputPath, projectAssetsFile, + targetFramework, targetFrameworks, + outputKind, languageVersion, allowUnsafeCode, documentationFile, preprocessorSymbolNames, suppressDiagnosticIds, + signAssembly, assemblyOriginatorKeyFile, + sourceFiles, projectReferences, references, packageReferences, analyzers); + } + + private static bool ReferenceSourceTargetIsNotProjectReference(ProjectItemInstance item) + => item.GetMetadataValue(MetadataNames.ReferenceSourceTarget) != ItemNames.ProjectReference; + + private static bool FileNameIsNotGenerated(string filePath) + => !Path.GetFileName(filePath).StartsWith("TemporaryGeneratedFile_"); + + private static ImmutableArray GetFullPaths(IEnumerable items, Func filter = null) + { + var builder = ImmutableArray.CreateBuilder(); + var addedSet = new HashSet(); + + filter = filter ?? (_ => true); + + foreach (var item in items) + { + var fullPath = item.GetMetadataValue(MetadataNames.FullPath); + + if (filter(fullPath) && addedSet.Add(fullPath)) + { + builder.Add(fullPath); + } + } + + return builder.ToImmutable(); + } + + private static ImmutableArray GetPackageReferences(ICollection items) + { + var builder = ImmutableArray.CreateBuilder(items.Count); + var addedSet = new HashSet(); + + foreach (var item in items) + { + var name = item.EvaluatedInclude; + var versionValue = item.GetMetadataValue(MetadataNames.Version); + var versionRange = PropertyConverter.ToVersionRange(versionValue); + var dependency = new PackageDependency(name, versionRange); + + var isImplicitlyDefinedValue = item.GetMetadataValue(MetadataNames.IsImplicitlyDefined); + var isImplicitlyDefined = PropertyConverter.ToBoolean(isImplicitlyDefinedValue, defaultValue: false); + + var packageReference = new PackageReference(dependency, isImplicitlyDefined); + + if (addedSet.Add(packageReference)) + { + builder.Add(packageReference); + } + } + + return builder.ToImmutable(); + } } } } diff --git a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.cs b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.cs index 3aa67eb484..475143316b 100644 --- a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.cs +++ b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.cs @@ -9,7 +9,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.Extensions.Logging; -using NuGet.Packaging.Core; using OmniSharp.MSBuild.Discovery; using OmniSharp.MSBuild.Logging; using OmniSharp.Options; @@ -77,14 +76,14 @@ public static (ProjectFileInfo projectFileInfo, ImmutableArray.Empty); } - var (projectInstance, diagnostics) = LoadProject(filePath, solutionDirectory, logger, msbuildInstance, options, out var targetFrameworks); + var (projectInstance, diagnostics) = LoadProject(filePath, solutionDirectory, logger, msbuildInstance, options); if (projectInstance == null) { return (null, diagnostics); } var id = ProjectId.CreateNewId(debugName: filePath); - var data = CreateProjectData(projectInstance, targetFrameworks); + var data = ProjectData.Create(projectInstance); var projectFileInfo = new ProjectFileInfo(id, filePath, data); return (projectFileInfo, diagnostics); @@ -92,7 +91,7 @@ public static (ProjectFileInfo projectFileInfo, ImmutableArray diagnostics) LoadProject( string filePath, string solutionDirectory, ILogger logger, - MSBuildInstance msbuildInstance, MSBuildOptions options, out ImmutableArray targetFrameworks) + MSBuildInstance msbuildInstance, MSBuildOptions options) { options = options ?? new MSBuildOptions(); @@ -112,7 +111,7 @@ private static (ProjectInstance projectInstance, ImmutableArray targetFrameworks) - { - var guid = PropertyConverter.ToGuid(projectInstance.GetPropertyValue(PropertyNames.ProjectGuid)); - var name = projectInstance.GetPropertyValue(PropertyNames.ProjectName); - var assemblyName = projectInstance.GetPropertyValue(PropertyNames.AssemblyName); - var targetPath = projectInstance.GetPropertyValue(PropertyNames.TargetPath); - var outputPath = projectInstance.GetPropertyValue(PropertyNames.OutputPath); - var projectAssetsFile = projectInstance.GetPropertyValue(PropertyNames.ProjectAssetsFile); - - var targetFramework = new FrameworkName(projectInstance.GetPropertyValue(PropertyNames.TargetFrameworkMoniker)); - - var languageVersion = PropertyConverter.ToLanguageVersion(projectInstance.GetPropertyValue(PropertyNames.LangVersion)); - var allowUnsafeCode = PropertyConverter.ToBoolean(projectInstance.GetPropertyValue(PropertyNames.AllowUnsafeBlocks), defaultValue: false); - var outputKind = PropertyConverter.ToOutputKind(projectInstance.GetPropertyValue(PropertyNames.OutputType)); - var documentationFile = projectInstance.GetPropertyValue(PropertyNames.DocumentationFile); - var preprocessorSymbolNames = PropertyConverter.ToPreprocessorSymbolNames(projectInstance.GetPropertyValue(PropertyNames.DefineConstants)); - var suppressDiagnosticIds = PropertyConverter.ToSuppressDiagnosticIds(projectInstance.GetPropertyValue(PropertyNames.NoWarn)); - var signAssembly = PropertyConverter.ToBoolean(projectInstance.GetPropertyValue(PropertyNames.SignAssembly), defaultValue: false); - var assemblyOriginatorKeyFile = projectInstance.GetPropertyValue(PropertyNames.AssemblyOriginatorKeyFile); - - var sourceFiles = GetFullPaths( - projectInstance.GetItems(ItemNames.Compile), filter: FileNameIsNotGenerated); - var projectReferences = GetFullPaths(projectInstance.GetItems(ItemNames.ProjectReference)); - var references = GetFullPaths( - projectInstance.GetItems(ItemNames.ReferencePath).Where(ReferenceSourceTargetIsNotProjectReference)); - var packageReferences = GetPackageReferences(projectInstance.GetItems(ItemNames.PackageReference)); - var analyzers = GetFullPaths(projectInstance.GetItems(ItemNames.Analyzer)); - - return new ProjectData(guid, name, - assemblyName, targetPath, outputPath, projectAssetsFile, - targetFramework, targetFrameworks, - outputKind, languageVersion, allowUnsafeCode, documentationFile, preprocessorSymbolNames, suppressDiagnosticIds, - signAssembly, assemblyOriginatorKeyFile, - sourceFiles, projectReferences, references, packageReferences, analyzers); - } - public (ProjectFileInfo projectFileInfo, ImmutableArray diagnostics) Reload( string solutionDirectory, ILogger logger, MSBuildInstance msbuildInstance, MSBuildOptions options = null) { - var (projectInstance, diagnostics) = LoadProject(FilePath, solutionDirectory, logger, msbuildInstance, options, out var targetFrameworks); + var (projectInstance, diagnostics) = LoadProject(FilePath, solutionDirectory, logger, msbuildInstance, options); if (projectInstance == null) { return (null, diagnostics); } - var data = CreateProjectData(projectInstance, targetFrameworks); + var data = ProjectData.Create(projectInstance); var projectFileInfo = new ProjectFileInfo(Id, FilePath, data); return (projectFileInfo, diagnostics); @@ -276,57 +239,5 @@ private static Dictionary GetGlobalProperties(MSBuildInstance ms return globalProperties; } - - private static bool ReferenceSourceTargetIsNotProjectReference(ProjectItemInstance item) - => item.GetMetadataValue(MetadataNames.ReferenceSourceTarget) != ItemNames.ProjectReference; - - private static bool FileNameIsNotGenerated(string filePath) - => !Path.GetFileName(filePath).StartsWith("TemporaryGeneratedFile_"); - - private static ImmutableArray GetFullPaths(IEnumerable items, Func filter = null) - { - var builder = ImmutableArray.CreateBuilder(); - var addedSet = new HashSet(); - - filter = filter ?? (_ => true); - - foreach (var item in items) - { - var fullPath = item.GetMetadataValue(MetadataNames.FullPath); - - if (filter(fullPath) && addedSet.Add(fullPath)) - { - builder.Add(fullPath); - } - } - - return builder.ToImmutable(); - } - - private static ImmutableArray GetPackageReferences(ICollection items) - { - var builder = ImmutableArray.CreateBuilder(items.Count); - var addedSet = new HashSet(); - - foreach (var item in items) - { - var name = item.EvaluatedInclude; - var versionValue = item.GetMetadataValue(MetadataNames.Version); - var versionRange = PropertyConverter.ToVersionRange(versionValue); - var dependency = new PackageDependency(name, versionRange); - - var isImplicitlyDefinedValue = item.GetMetadataValue(MetadataNames.IsImplicitlyDefined); - var isImplicitlyDefined = PropertyConverter.ToBoolean(isImplicitlyDefinedValue, defaultValue: false); - - var packageReference = new PackageReference(dependency, isImplicitlyDefined); - - if (addedSet.Add(packageReference)) - { - builder.Add(packageReference); - } - } - - return builder.ToImmutable(); - } } } From 941d52effb15bea06f5713bb80ff432092dcd3a6 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Thu, 2 Nov 2017 11:25:28 -0700 Subject: [PATCH 05/10] Introduce ProjectLoader class to handle responsibilities of loading a project --- src/OmniSharp.MSBuild/MSBuildProjectSystem.cs | 11 +- .../Models/MSBuildProjectInfo.cs | 2 +- .../Models/MSBuildWorkspaceInfo.cs | 2 +- .../ProjectFile/ItemNames.cs | 11 ++ .../ProjectFile/MetadataNames.cs | 11 ++ .../ProjectFile/ProjectFileInfo.ItemNames.cs | 14 -- .../ProjectFileInfo.MetadataNames.cs | 14 -- .../ProjectFileInfo.ProjectData.cs | 2 +- .../ProjectFileInfo.PropertyNames.cs | 43 ----- .../ProjectFileInfo.TargetNames.cs | 12 -- .../ProjectFile/ProjectFileInfo.cs | 156 ++---------------- .../ProjectFile/ProjectFileInfoCollection.cs | 4 +- .../ProjectFile/PropertyNames.cs | 40 +++++ .../ProjectFile/TargetNames.cs | 9 + src/OmniSharp.MSBuild/ProjectLoader.cs | 153 +++++++++++++++++ .../ProjectFileInfoTests.cs | 11 +- 16 files changed, 254 insertions(+), 241 deletions(-) create mode 100644 src/OmniSharp.MSBuild/ProjectFile/ItemNames.cs create mode 100644 src/OmniSharp.MSBuild/ProjectFile/MetadataNames.cs delete mode 100644 src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.ItemNames.cs delete mode 100644 src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.MetadataNames.cs delete mode 100644 src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.PropertyNames.cs delete mode 100644 src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.TargetNames.cs create mode 100644 src/OmniSharp.MSBuild/ProjectFile/PropertyNames.cs create mode 100644 src/OmniSharp.MSBuild/ProjectFile/TargetNames.cs create mode 100644 src/OmniSharp.MSBuild/ProjectLoader.cs diff --git a/src/OmniSharp.MSBuild/MSBuildProjectSystem.cs b/src/OmniSharp.MSBuild/MSBuildProjectSystem.cs index 49452c5190..7d559e16ed 100644 --- a/src/OmniSharp.MSBuild/MSBuildProjectSystem.cs +++ b/src/OmniSharp.MSBuild/MSBuildProjectSystem.cs @@ -32,7 +32,7 @@ public class MSBuildProjectSystem : IProjectSystem { private readonly IOmniSharpEnvironment _environment; private readonly OmniSharpWorkspace _workspace; - private readonly MSBuildInstance _msbuildInstance; + private readonly ImmutableDictionary _propertyOverrides; private readonly DotNetCliService _dotNetCli; private readonly MetadataFileReferenceCache _metadataFileReferenceCache; private readonly IEventEmitter _eventEmitter; @@ -45,6 +45,7 @@ public class MSBuildProjectSystem : IProjectSystem private readonly Queue _projectsToProcess; private readonly ProjectFileInfoCollection _projects; + private ProjectLoader _loader; private MSBuildOptions _options; private string _solutionFileOrRootPath; @@ -65,7 +66,7 @@ public MSBuildProjectSystem( { _environment = environment; _workspace = workspace; - _msbuildInstance = msbuildLocator.RegisteredInstance; + _propertyOverrides = msbuildLocator.RegisteredInstance.PropertyOverrides; _dotNetCli = dotNetCliService; _metadataFileReferenceCache = metadataFileReferenceCache; _eventEmitter = eventEmitter; @@ -89,6 +90,8 @@ public void Initalize(IConfiguration configuration) _logger.LogDebug($"MSBuild environment: {Environment.NewLine}{buildEnvironmentInfo}"); } + _loader = new ProjectLoader(_options, _environment.TargetDirectory, _propertyOverrides, _loggerFactory); + var initialProjectPaths = GetInitialProjectPaths(); foreach (var projectPath in initialProjectPaths) @@ -316,7 +319,7 @@ private ProjectFileInfo LoadProject(string projectFilePath) try { - (project, diagnostics) = ProjectFileInfo.Create(projectFilePath, _environment.TargetDirectory, _loggerFactory.CreateLogger(), _msbuildInstance, _options); + (project, diagnostics) = ProjectFileInfo.Create(projectFilePath, _loader); if (project == null) { @@ -344,7 +347,7 @@ private void OnProjectChanged(string projectFilePath, bool allowAutoRestore) ProjectFileInfo newProjectFileInfo; ImmutableArray diagnostics; - (newProjectFileInfo, diagnostics) = oldProjectFileInfo.Reload(_environment.TargetDirectory, _loggerFactory.CreateLogger(), _msbuildInstance, _options); + (newProjectFileInfo, diagnostics) = oldProjectFileInfo.Reload(_loader); if (newProjectFileInfo != null) { diff --git a/src/OmniSharp.MSBuild/Models/MSBuildProjectInfo.cs b/src/OmniSharp.MSBuild/Models/MSBuildProjectInfo.cs index 2ab6241a7a..cccb3f34b3 100644 --- a/src/OmniSharp.MSBuild/Models/MSBuildProjectInfo.cs +++ b/src/OmniSharp.MSBuild/Models/MSBuildProjectInfo.cs @@ -19,7 +19,7 @@ public class MSBuildProjectInfo public bool IsExe { get; set; } public bool IsUnityProject { get; set; } - public MSBuildProjectInfo(ProjectFileInfo projectFileInfo) + internal MSBuildProjectInfo(ProjectFileInfo projectFileInfo) { AssemblyName = projectFileInfo.AssemblyName; Path = projectFileInfo.FilePath; diff --git a/src/OmniSharp.MSBuild/Models/MSBuildWorkspaceInfo.cs b/src/OmniSharp.MSBuild/Models/MSBuildWorkspaceInfo.cs index ad788299e0..ce822f473b 100644 --- a/src/OmniSharp.MSBuild/Models/MSBuildWorkspaceInfo.cs +++ b/src/OmniSharp.MSBuild/Models/MSBuildWorkspaceInfo.cs @@ -6,7 +6,7 @@ namespace OmniSharp.MSBuild.Models { public class MSBuildWorkspaceInfo { - public MSBuildWorkspaceInfo(string solutionFilePath, IEnumerable projects, bool excludeSourceFiles) + internal MSBuildWorkspaceInfo(string solutionFilePath, IEnumerable projects, bool excludeSourceFiles) { SolutionPath = solutionFilePath; diff --git a/src/OmniSharp.MSBuild/ProjectFile/ItemNames.cs b/src/OmniSharp.MSBuild/ProjectFile/ItemNames.cs new file mode 100644 index 0000000000..641656c933 --- /dev/null +++ b/src/OmniSharp.MSBuild/ProjectFile/ItemNames.cs @@ -0,0 +1,11 @@ +namespace OmniSharp.MSBuild.ProjectFile +{ + internal static class ItemNames + { + public const string Analyzer = nameof(Analyzer); + public const string Compile = nameof(Compile); + public const string PackageReference = nameof(PackageReference); + public const string ProjectReference = nameof(ProjectReference); + public const string ReferencePath = nameof(ReferencePath); + } +} diff --git a/src/OmniSharp.MSBuild/ProjectFile/MetadataNames.cs b/src/OmniSharp.MSBuild/ProjectFile/MetadataNames.cs new file mode 100644 index 0000000000..a81bb110c4 --- /dev/null +++ b/src/OmniSharp.MSBuild/ProjectFile/MetadataNames.cs @@ -0,0 +1,11 @@ +namespace OmniSharp.MSBuild.ProjectFile +{ + internal static class MetadataNames + { + public const string FullPath = nameof(FullPath); + public const string IsImplicitlyDefined = nameof(IsImplicitlyDefined); + public const string Project = nameof(Project); + public const string ReferenceSourceTarget = nameof(ReferenceSourceTarget); + public const string Version = nameof(Version); + } +} diff --git a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.ItemNames.cs b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.ItemNames.cs deleted file mode 100644 index 13e50a66c8..0000000000 --- a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.ItemNames.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace OmniSharp.MSBuild.ProjectFile -{ - public partial class ProjectFileInfo - { - private static class ItemNames - { - public const string Analyzer = nameof(Analyzer); - public const string Compile = nameof(Compile); - public const string PackageReference = nameof(PackageReference); - public const string ProjectReference = nameof(ProjectReference); - public const string ReferencePath = nameof(ReferencePath); - } - } -} diff --git a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.MetadataNames.cs b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.MetadataNames.cs deleted file mode 100644 index 508e05ee4f..0000000000 --- a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.MetadataNames.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace OmniSharp.MSBuild.ProjectFile -{ - public partial class ProjectFileInfo - { - private static class MetadataNames - { - public const string FullPath = nameof(FullPath); - public const string IsImplicitlyDefined = nameof(IsImplicitlyDefined); - public const string Project = nameof(Project); - public const string ReferenceSourceTarget = nameof(ReferenceSourceTarget); - public const string Version = nameof(Version); - } - } -} diff --git a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.ProjectData.cs b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.ProjectData.cs index 8994cc6c0a..61f3dae3c5 100644 --- a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.ProjectData.cs +++ b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.ProjectData.cs @@ -11,7 +11,7 @@ namespace OmniSharp.MSBuild.ProjectFile { - public partial class ProjectFileInfo + internal partial class ProjectFileInfo { private class ProjectData { diff --git a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.PropertyNames.cs b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.PropertyNames.cs deleted file mode 100644 index c5018d9ec1..0000000000 --- a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.PropertyNames.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace OmniSharp.MSBuild.ProjectFile -{ - public partial class ProjectFileInfo - { - private static class PropertyNames - { - public const string AllowUnsafeBlocks = nameof(AllowUnsafeBlocks); - public const string AssemblyName = nameof(AssemblyName); - public const string AssemblyOriginatorKeyFile = nameof(AssemblyOriginatorKeyFile); - public const string BuildProjectReferences = nameof(BuildProjectReferences); - public const string BuildingInsideVisualStudio = nameof(BuildingInsideVisualStudio); - public const string Configuration = nameof(Configuration); - public const string CscToolExe = nameof(CscToolExe); - public const string CscToolPath = nameof(CscToolPath); - public const string DefineConstants = nameof(DefineConstants); - public const string DesignTimeBuild = nameof(DesignTimeBuild); - public const string DocumentationFile = nameof(DocumentationFile); - public const string LangVersion = nameof(LangVersion); - public const string OutputType = nameof(OutputType); - public const string MSBuildExtensionsPath = nameof(MSBuildExtensionsPath); - public const string MSBuildSDKsPath = nameof(MSBuildSDKsPath); - public const string NoWarn = nameof(NoWarn); - public const string OutputPath = nameof(OutputPath); - public const string Platform = nameof(Platform); - public const string ProjectAssetsFile = nameof(ProjectAssetsFile); - public const string ProvideCommandLineArgs = nameof(ProvideCommandLineArgs); - public const string ProjectGuid = nameof(ProjectGuid); - public const string ProjectName = nameof(ProjectName); - public const string _ResolveReferenceDependencies = nameof(_ResolveReferenceDependencies); - public const string RoslynTargetsPath = nameof(RoslynTargetsPath); - public const string SignAssembly = nameof(SignAssembly); - public const string SkipCompilerExecution = nameof(SkipCompilerExecution); - public const string SolutionDir = nameof(SolutionDir); - public const string TargetFramework = nameof(TargetFramework); - public const string TargetFrameworkMoniker = nameof(TargetFrameworkMoniker); - public const string TargetFrameworkRootPath = nameof(TargetFrameworkRootPath); - public const string TargetFrameworks = nameof(TargetFrameworks); - public const string TargetPath = nameof(TargetPath); - public const string VisualStudioVersion = nameof(VisualStudioVersion); - public const string VsInstallRoot = nameof(VsInstallRoot); - } - } -} diff --git a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.TargetNames.cs b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.TargetNames.cs deleted file mode 100644 index c047c80ab5..0000000000 --- a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.TargetNames.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace OmniSharp.MSBuild.ProjectFile -{ - public partial class ProjectFileInfo - { - private static class TargetNames - { - public const string Compile = nameof(Compile); - public const string CoreCompile = nameof(CoreCompile); - public const string ResolveReferences = nameof(ResolveReferences); - } - } -} diff --git a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.cs b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.cs index 475143316b..13265a9c7a 100644 --- a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.cs +++ b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.cs @@ -4,18 +4,13 @@ using System.IO; using System.Linq; using System.Runtime.Versioning; -using Microsoft.Build.Evaluation; -using Microsoft.Build.Execution; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; -using Microsoft.Extensions.Logging; -using OmniSharp.MSBuild.Discovery; using OmniSharp.MSBuild.Logging; -using OmniSharp.Options; namespace OmniSharp.MSBuild.ProjectFile { - public partial class ProjectFileInfo + internal partial class ProjectFileInfo { private readonly ProjectData _data; @@ -68,15 +63,14 @@ private ProjectFileInfo( _data = data; } - public static (ProjectFileInfo projectFileInfo, ImmutableArray diagnostics) Create( - string filePath, string solutionDirectory, ILogger logger, MSBuildInstance msbuildInstance, MSBuildOptions options = null) + public static (ProjectFileInfo projectFileInfo, ImmutableArray diagnostics) Create(string filePath, ProjectLoader loader) { if (!File.Exists(filePath)) { return (null, ImmutableArray.Empty); } - var (projectInstance, diagnostics) = LoadProject(filePath, solutionDirectory, logger, msbuildInstance, options); + var (projectInstance, diagnostics) = loader.BuildProject(filePath); if (projectInstance == null) { return (null, diagnostics); @@ -89,107 +83,9 @@ public static (ProjectFileInfo projectFileInfo, ImmutableArray diagnostics) LoadProject( - string filePath, string solutionDirectory, ILogger logger, - MSBuildInstance msbuildInstance, MSBuildOptions options) + public (ProjectFileInfo projectFileInfo, ImmutableArray diagnostics) Reload(ProjectLoader loader) { - options = options ?? new MSBuildOptions(); - - var globalProperties = GetGlobalProperties(msbuildInstance, options, solutionDirectory, logger); - - var collection = new ProjectCollection(globalProperties); - - var toolsVersion = options.ToolsVersion; - if (string.IsNullOrEmpty(toolsVersion) || Version.TryParse(toolsVersion, out _)) - { - toolsVersion = collection.DefaultToolsVersion; - } - - toolsVersion = GetLegalToolsetVersion(toolsVersion, collection.Toolsets); - - // Evaluate the MSBuild project - var project = collection.LoadProject(filePath, toolsVersion); - - var targetFramework = project.GetPropertyValue(PropertyNames.TargetFramework); - var targetFrameworks = PropertyConverter.SplitList(project.GetPropertyValue(PropertyNames.TargetFrameworks), ';'); - - // If the project supports multiple target frameworks and specific framework isn't - // selected, we must pick one before execution. Otherwise, the ResolveReferences - // target might not be available to us. - if (string.IsNullOrWhiteSpace(targetFramework) && targetFrameworks.Length > 0) - { - // For now, we'll just pick the first target framework. Eventually, we'll need to - // do better and potentially allow OmniSharp hosts to select a target framework. - targetFramework = targetFrameworks[0]; - project.SetProperty(PropertyNames.TargetFramework, targetFramework); - } - else if (!string.IsNullOrWhiteSpace(targetFramework) && targetFrameworks.Length == 0) - { - targetFrameworks = ImmutableArray.Create(targetFramework); - } - - var projectInstance = project.CreateProjectInstance(); - var msbuildLogger = new MSBuildLogger(logger); - var buildResult = projectInstance.Build( - targets: new string[] { TargetNames.Compile, TargetNames.CoreCompile }, - loggers: new[] { msbuildLogger }); - - var diagnostics = msbuildLogger.GetDiagnostics(); - - return buildResult - ? (projectInstance, diagnostics) - : (null, diagnostics); - } - - private static string GetLegalToolsetVersion(string toolsVersion, ICollection toolsets) - { - // It's entirely possible the the toolset specified does not exist. In that case, we'll try to use - // the highest version available. - var version = new Version(toolsVersion); - - bool exists = false; - Version highestVersion = null; - - var legalToolsets = new SortedList(toolsets.Count); - foreach (var toolset in toolsets) - { - // Only consider this toolset if it has a legal version, we haven't seen it, and its path exists. - if (Version.TryParse(toolset.ToolsVersion, out var toolsetVersion) && - !legalToolsets.ContainsKey(toolsetVersion) && - System.IO.Directory.Exists(toolset.ToolsPath)) - { - legalToolsets.Add(toolsetVersion, toolset); - - if (highestVersion == null || - toolsetVersion > highestVersion) - { - highestVersion = toolsetVersion; - } - - if (toolsetVersion == version) - { - exists = true; - } - } - } - - if (highestVersion == null) - { - throw new InvalidOperationException("No legal MSBuild toolsets available."); - } - - if (!exists) - { - toolsVersion = legalToolsets[highestVersion].ToolsPath; - } - - return toolsVersion; - } - - public (ProjectFileInfo projectFileInfo, ImmutableArray diagnostics) Reload( - string solutionDirectory, ILogger logger, MSBuildInstance msbuildInstance, MSBuildOptions options = null) - { - var (projectInstance, diagnostics) = LoadProject(FilePath, solutionDirectory, logger, msbuildInstance, options); + var (projectInstance, diagnostics) = loader.BuildProject(FilePath); if (projectInstance == null) { return (null, diagnostics); @@ -202,42 +98,12 @@ private static string GetLegalToolsetVersion(string toolsVersion, ICollection - { - var fileName = Path.GetFileName(filePath); - - return string.Equals(fileName, "UnityEngine.dll", StringComparison.OrdinalIgnoreCase) - || string.Equals(fileName, "UnityEditor.dll", StringComparison.OrdinalIgnoreCase); - }); - } + => References.Any(filePath => + { + var fileName = Path.GetFileName(filePath); - private static Dictionary GetGlobalProperties(MSBuildInstance msbuildInstance, MSBuildOptions options, string solutionDirectory, ILogger logger) - { - var globalProperties = new Dictionary - { - { PropertyNames.DesignTimeBuild, "true" }, - { PropertyNames.BuildingInsideVisualStudio, "true" }, - { PropertyNames.BuildProjectReferences, "false" }, - { PropertyNames._ResolveReferenceDependencies, "true" }, - { PropertyNames.SolutionDir, solutionDirectory + Path.DirectorySeparatorChar }, - - // This properties allow the design-time build to handle the Compile target without actually invoking the compiler. - // See https://github.com/dotnet/roslyn/pull/4604 for details. - { PropertyNames.ProvideCommandLineArgs, "true" }, - { PropertyNames.SkipCompilerExecution, "true" } - }; - - globalProperties.AddPropertyOverride(PropertyNames.MSBuildExtensionsPath, options.MSBuildExtensionsPath, msbuildInstance.PropertyOverrides, logger); - globalProperties.AddPropertyOverride(PropertyNames.TargetFrameworkRootPath, options.TargetFrameworkRootPath, msbuildInstance.PropertyOverrides, logger); - globalProperties.AddPropertyOverride(PropertyNames.RoslynTargetsPath, options.RoslynTargetsPath, msbuildInstance.PropertyOverrides, logger); - globalProperties.AddPropertyOverride(PropertyNames.CscToolPath, options.CscToolPath, msbuildInstance.PropertyOverrides, logger); - globalProperties.AddPropertyOverride(PropertyNames.CscToolExe, options.CscToolExe, msbuildInstance.PropertyOverrides, logger); - globalProperties.AddPropertyOverride(PropertyNames.VisualStudioVersion, options.VisualStudioVersion, msbuildInstance.PropertyOverrides, logger); - globalProperties.AddPropertyOverride(PropertyNames.Configuration, options.Configuration, msbuildInstance.PropertyOverrides, logger); - globalProperties.AddPropertyOverride(PropertyNames.Platform, options.Platform, msbuildInstance.PropertyOverrides, logger); - - return globalProperties; - } + return string.Equals(fileName, "UnityEngine.dll", StringComparison.OrdinalIgnoreCase) + || string.Equals(fileName, "UnityEditor.dll", StringComparison.OrdinalIgnoreCase); + }); } } diff --git a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfoCollection.cs b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfoCollection.cs index 65425dbd38..36db94057e 100644 --- a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfoCollection.cs +++ b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfoCollection.cs @@ -4,7 +4,7 @@ namespace OmniSharp.MSBuild.ProjectFile { - public class ProjectFileInfoCollection : IEnumerable + internal class ProjectFileInfoCollection : IEnumerable { private readonly List _items; private readonly Dictionary _itemMap; @@ -77,4 +77,4 @@ public ProjectFileInfo this[string filePath] } } } -} \ No newline at end of file +} diff --git a/src/OmniSharp.MSBuild/ProjectFile/PropertyNames.cs b/src/OmniSharp.MSBuild/ProjectFile/PropertyNames.cs new file mode 100644 index 0000000000..572036a5fd --- /dev/null +++ b/src/OmniSharp.MSBuild/ProjectFile/PropertyNames.cs @@ -0,0 +1,40 @@ +namespace OmniSharp.MSBuild.ProjectFile +{ + internal static class PropertyNames + { + public const string AllowUnsafeBlocks = nameof(AllowUnsafeBlocks); + public const string AssemblyName = nameof(AssemblyName); + public const string AssemblyOriginatorKeyFile = nameof(AssemblyOriginatorKeyFile); + public const string BuildProjectReferences = nameof(BuildProjectReferences); + public const string BuildingInsideVisualStudio = nameof(BuildingInsideVisualStudio); + public const string Configuration = nameof(Configuration); + public const string CscToolExe = nameof(CscToolExe); + public const string CscToolPath = nameof(CscToolPath); + public const string DefineConstants = nameof(DefineConstants); + public const string DesignTimeBuild = nameof(DesignTimeBuild); + public const string DocumentationFile = nameof(DocumentationFile); + public const string LangVersion = nameof(LangVersion); + public const string OutputType = nameof(OutputType); + public const string MSBuildExtensionsPath = nameof(MSBuildExtensionsPath); + public const string MSBuildSDKsPath = nameof(MSBuildSDKsPath); + public const string NoWarn = nameof(NoWarn); + public const string OutputPath = nameof(OutputPath); + public const string Platform = nameof(Platform); + public const string ProjectAssetsFile = nameof(ProjectAssetsFile); + public const string ProvideCommandLineArgs = nameof(ProvideCommandLineArgs); + public const string ProjectGuid = nameof(ProjectGuid); + public const string ProjectName = nameof(ProjectName); + public const string _ResolveReferenceDependencies = nameof(_ResolveReferenceDependencies); + public const string RoslynTargetsPath = nameof(RoslynTargetsPath); + public const string SignAssembly = nameof(SignAssembly); + public const string SkipCompilerExecution = nameof(SkipCompilerExecution); + public const string SolutionDir = nameof(SolutionDir); + public const string TargetFramework = nameof(TargetFramework); + public const string TargetFrameworkMoniker = nameof(TargetFrameworkMoniker); + public const string TargetFrameworkRootPath = nameof(TargetFrameworkRootPath); + public const string TargetFrameworks = nameof(TargetFrameworks); + public const string TargetPath = nameof(TargetPath); + public const string VisualStudioVersion = nameof(VisualStudioVersion); + public const string VsInstallRoot = nameof(VsInstallRoot); + } +} diff --git a/src/OmniSharp.MSBuild/ProjectFile/TargetNames.cs b/src/OmniSharp.MSBuild/ProjectFile/TargetNames.cs new file mode 100644 index 0000000000..5139e9430e --- /dev/null +++ b/src/OmniSharp.MSBuild/ProjectFile/TargetNames.cs @@ -0,0 +1,9 @@ +namespace OmniSharp.MSBuild.ProjectFile +{ + internal static class TargetNames + { + public const string Compile = nameof(Compile); + public const string CoreCompile = nameof(CoreCompile); + public const string ResolveReferences = nameof(ResolveReferences); + } +} diff --git a/src/OmniSharp.MSBuild/ProjectLoader.cs b/src/OmniSharp.MSBuild/ProjectLoader.cs new file mode 100644 index 0000000000..87995f3f89 --- /dev/null +++ b/src/OmniSharp.MSBuild/ProjectLoader.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using Microsoft.Extensions.Logging; +using OmniSharp.MSBuild.Logging; +using OmniSharp.MSBuild.ProjectFile; +using OmniSharp.Options; + +using MSB = Microsoft.Build; + +namespace OmniSharp.MSBuild +{ + internal class ProjectLoader + { + private readonly ILogger _logger; + private readonly Dictionary _globalProperties; + private readonly MSB.Evaluation.ProjectCollection _projectCollection; + private readonly string _toolsVersion; + + public ProjectLoader(MSBuildOptions options, string solutionDirectory, ImmutableDictionary propertyOverrides, ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + options = options ?? new MSBuildOptions(); + + _globalProperties = CreateGlobalProperties(options, solutionDirectory, propertyOverrides, _logger); + _projectCollection = new MSB.Evaluation.ProjectCollection(_globalProperties); + + var toolsVersion = options.ToolsVersion; + if (string.IsNullOrEmpty(toolsVersion) || Version.TryParse(toolsVersion, out _)) + { + toolsVersion = _projectCollection.DefaultToolsVersion; + } + + _toolsVersion = GetLegalToolsetVersion(toolsVersion, _projectCollection.Toolsets); + } + + private static Dictionary CreateGlobalProperties( + MSBuildOptions options, string solutionDirectory, ImmutableDictionary propertyOverrides, ILogger logger) + { + var globalProperties = new Dictionary + { + { PropertyNames.DesignTimeBuild, "true" }, + { PropertyNames.BuildingInsideVisualStudio, "true" }, + { PropertyNames.BuildProjectReferences, "false" }, + { PropertyNames._ResolveReferenceDependencies, "true" }, + { PropertyNames.SolutionDir, solutionDirectory + Path.DirectorySeparatorChar }, + + // This properties allow the design-time build to handle the Compile target without actually invoking the compiler. + // See https://github.com/dotnet/roslyn/pull/4604 for details. + { PropertyNames.ProvideCommandLineArgs, "true" }, + { PropertyNames.SkipCompilerExecution, "true" } + }; + + globalProperties.AddPropertyOverride(PropertyNames.MSBuildExtensionsPath, options.MSBuildExtensionsPath, propertyOverrides, logger); + globalProperties.AddPropertyOverride(PropertyNames.TargetFrameworkRootPath, options.TargetFrameworkRootPath, propertyOverrides, logger); + globalProperties.AddPropertyOverride(PropertyNames.RoslynTargetsPath, options.RoslynTargetsPath, propertyOverrides, logger); + globalProperties.AddPropertyOverride(PropertyNames.CscToolPath, options.CscToolPath, propertyOverrides, logger); + globalProperties.AddPropertyOverride(PropertyNames.CscToolExe, options.CscToolExe, propertyOverrides, logger); + globalProperties.AddPropertyOverride(PropertyNames.VisualStudioVersion, options.VisualStudioVersion, propertyOverrides, logger); + globalProperties.AddPropertyOverride(PropertyNames.Configuration, options.Configuration, propertyOverrides, logger); + globalProperties.AddPropertyOverride(PropertyNames.Platform, options.Platform, propertyOverrides, logger); + + return globalProperties; + } + + public (MSB.Execution.ProjectInstance projectInstance, ImmutableArray diagnostics) BuildProject(string filePath) + { + // Evaluate the MSBuild project + var evaluatedProject = _projectCollection.LoadProject(filePath, _toolsVersion); + + SetTargetFrameworkIfNeeded(evaluatedProject); + + var projectInstance = evaluatedProject.CreateProjectInstance(); + var msbuildLogger = new MSBuildLogger(_logger); + var buildResult = projectInstance.Build( + targets: new string[] { TargetNames.Compile, TargetNames.CoreCompile }, + loggers: new[] { msbuildLogger }); + + var diagnostics = msbuildLogger.GetDiagnostics(); + + return buildResult + ? (projectInstance, diagnostics) + : (null, diagnostics); + } + + private static void SetTargetFrameworkIfNeeded(MSB.Evaluation.Project evaluatedProject) + { + var targetFramework = evaluatedProject.GetPropertyValue(PropertyNames.TargetFramework); + var targetFrameworks = PropertyConverter.SplitList(evaluatedProject.GetPropertyValue(PropertyNames.TargetFrameworks), ';'); + + // If the project supports multiple target frameworks and specific framework isn't + // selected, we must pick one before execution. Otherwise, the ResolveReferences + // target might not be available to us. + if (string.IsNullOrWhiteSpace(targetFramework) && targetFrameworks.Length > 0) + { + // For now, we'll just pick the first target framework. Eventually, we'll need to + // do better and potentially allow OmniSharp hosts to select a target framework. + targetFramework = targetFrameworks[0]; + evaluatedProject.SetProperty(PropertyNames.TargetFramework, targetFramework); + } + else if (!string.IsNullOrWhiteSpace(targetFramework) && targetFrameworks.Length == 0) + { + targetFrameworks = ImmutableArray.Create(targetFramework); + } + } + + private static string GetLegalToolsetVersion(string toolsVersion, ICollection toolsets) + { + // It's entirely possible the the toolset specified does not exist. In that case, we'll try to use + // the highest version available. + var version = new Version(toolsVersion); + + bool exists = false; + Version highestVersion = null; + + var legalToolsets = new SortedList(toolsets.Count); + foreach (var toolset in toolsets) + { + // Only consider this toolset if it has a legal version, we haven't seen it, and its path exists. + if (Version.TryParse(toolset.ToolsVersion, out var toolsetVersion) && + !legalToolsets.ContainsKey(toolsetVersion) && + Directory.Exists(toolset.ToolsPath)) + { + legalToolsets.Add(toolsetVersion, toolset); + + if (highestVersion == null || + toolsetVersion > highestVersion) + { + highestVersion = toolsetVersion; + } + + if (toolsetVersion == version) + { + exists = true; + } + } + } + + if (highestVersion == null) + { + throw new InvalidOperationException("No legal MSBuild toolsets available."); + } + + if (!exists) + { + toolsVersion = legalToolsets[highestVersion].ToolsPath; + } + + return toolsVersion; + } + } +} diff --git a/tests/OmniSharp.MSBuild.Tests/ProjectFileInfoTests.cs b/tests/OmniSharp.MSBuild.Tests/ProjectFileInfoTests.cs index 6a20d42ff6..3e991bbf74 100644 --- a/tests/OmniSharp.MSBuild.Tests/ProjectFileInfoTests.cs +++ b/tests/OmniSharp.MSBuild.Tests/ProjectFileInfoTests.cs @@ -1,9 +1,9 @@ using System.IO; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CSharp; -using Microsoft.Extensions.Logging; using OmniSharp.MSBuild.Discovery; using OmniSharp.MSBuild.ProjectFile; +using OmniSharp.Options; using TestUtility; using Xunit; using Xunit.Abstractions; @@ -13,20 +13,23 @@ namespace OmniSharp.MSBuild.Tests public class ProjectFileInfoTests : AbstractTestFixture { private readonly TestAssets _testAssets; - private readonly ILogger _logger; public ProjectFileInfoTests(ITestOutputHelper output) : base(output) { this._testAssets = TestAssets.Instance; - this._logger = this.LoggerFactory.CreateLogger(); } private ProjectFileInfo CreateProjectFileInfo(OmniSharpTestHost host, ITestProject testProject, string projectFilePath) { var msbuildLocator = host.GetExport(); + var loader = new ProjectLoader( + options: new MSBuildOptions(), + solutionDirectory: testProject.Directory, + propertyOverrides: msbuildLocator.RegisteredInstance.PropertyOverrides, + loggerFactory: LoggerFactory); - var (projectFileInfo, _) = ProjectFileInfo.Create(projectFilePath, testProject.Directory, this._logger, msbuildLocator.RegisteredInstance); + var (projectFileInfo, _) = ProjectFileInfo.Create(projectFilePath, loader); return projectFileInfo; } From 114148eda179141a5be8a3a136c4a2f27c874417 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Thu, 2 Nov 2017 16:13:11 -0700 Subject: [PATCH 06/10] Introduce ProjectManager class to manage MSBuild project files This is a pretty substantial change that reworks the core logic of the MSBuild project system to enable an important scenario: updating a project when several files change in quick succession. In order to fix an issue with OmniSharp not reloading and updating a project in response to a 'dotnet restore', we must watch four files that might be touched during a restore: * project.asset.json * .nuget.cache * .nuget.g.props * .nuget.g.targets To ensure that we don't reload and update a project multiple times in response to multiple file changes, this PR introduces a simple queue and processing loop using a TPL DataFlow BufferBlock. --- .../FileChangeType.cs | 2 +- .../v1/FilesChanged/FilesChangedRequest.cs | 1 + src/OmniSharp.MSBuild/MSBuildProjectSystem.cs | 637 ------------------ ...esolver.cs => PackageDependencyChecker.cs} | 46 +- .../ProjectFileInfo.ProjectData.cs | 79 ++- .../ProjectFile/ProjectFileInfo.cs | 23 +- .../ProjectFile/ProjectFileInfoCollection.cs | 28 +- .../ProjectFile/ProjectFileInfoExtensions.cs | 71 ++ .../ProjectFile/PropertyConverter.cs | 2 +- src/OmniSharp.MSBuild/ProjectLoader.cs | 35 +- src/OmniSharp.MSBuild/ProjectManager.cs | 469 +++++++++++++ src/OmniSharp.MSBuild/ProjectSystem.cs | 202 ++++++ .../MSBuildProjectSystemTests.cs | 8 +- .../ProjectFileInfoTests.cs | 2 +- tests/TestUtility/OmniSharpTestHost.cs | 11 +- 15 files changed, 917 insertions(+), 699 deletions(-) rename src/OmniSharp.Abstractions/{Models/v1/FilesChanged => FileWatching}/FileChangeType.cs (73%) delete mode 100644 src/OmniSharp.MSBuild/MSBuildProjectSystem.cs rename src/OmniSharp.MSBuild/{Resolution/PackageDependencyResolver.cs => PackageDependencyChecker.cs} (69%) create mode 100644 src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfoExtensions.cs create mode 100644 src/OmniSharp.MSBuild/ProjectManager.cs create mode 100644 src/OmniSharp.MSBuild/ProjectSystem.cs diff --git a/src/OmniSharp.Abstractions/Models/v1/FilesChanged/FileChangeType.cs b/src/OmniSharp.Abstractions/FileWatching/FileChangeType.cs similarity index 73% rename from src/OmniSharp.Abstractions/Models/v1/FilesChanged/FileChangeType.cs rename to src/OmniSharp.Abstractions/FileWatching/FileChangeType.cs index 7297a8343a..218513e78b 100644 --- a/src/OmniSharp.Abstractions/Models/v1/FilesChanged/FileChangeType.cs +++ b/src/OmniSharp.Abstractions/FileWatching/FileChangeType.cs @@ -1,4 +1,4 @@ -namespace OmniSharp.Models.FilesChanged +namespace OmniSharp.FileWatching { public enum FileChangeType { diff --git a/src/OmniSharp.Abstractions/Models/v1/FilesChanged/FilesChangedRequest.cs b/src/OmniSharp.Abstractions/Models/v1/FilesChanged/FilesChangedRequest.cs index e61a2056f6..6b720de559 100644 --- a/src/OmniSharp.Abstractions/Models/v1/FilesChanged/FilesChangedRequest.cs +++ b/src/OmniSharp.Abstractions/Models/v1/FilesChanged/FilesChangedRequest.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using OmniSharp.FileWatching; using OmniSharp.Mef; namespace OmniSharp.Models.FilesChanged diff --git a/src/OmniSharp.MSBuild/MSBuildProjectSystem.cs b/src/OmniSharp.MSBuild/MSBuildProjectSystem.cs deleted file mode 100644 index 7d559e16ed..0000000000 --- a/src/OmniSharp.MSBuild/MSBuildProjectSystem.cs +++ /dev/null @@ -1,637 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Composition; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using OmniSharp.Eventing; -using OmniSharp.FileWatching; -using OmniSharp.Models.Events; -using OmniSharp.Models.FilesChanged; -using OmniSharp.Models.UpdateBuffer; -using OmniSharp.Models.WorkspaceInformation; -using OmniSharp.MSBuild.Discovery; -using OmniSharp.MSBuild.Logging; -using OmniSharp.MSBuild.Models; -using OmniSharp.MSBuild.Models.Events; -using OmniSharp.MSBuild.ProjectFile; -using OmniSharp.MSBuild.Resolution; -using OmniSharp.MSBuild.SolutionParsing; -using OmniSharp.Options; -using OmniSharp.Services; - -namespace OmniSharp.MSBuild -{ - [Export(typeof(IProjectSystem)), Shared] - public class MSBuildProjectSystem : IProjectSystem - { - private readonly IOmniSharpEnvironment _environment; - private readonly OmniSharpWorkspace _workspace; - private readonly ImmutableDictionary _propertyOverrides; - private readonly DotNetCliService _dotNetCli; - private readonly MetadataFileReferenceCache _metadataFileReferenceCache; - private readonly IEventEmitter _eventEmitter; - private readonly IFileSystemWatcher _fileSystemWatcher; - private readonly ILoggerFactory _loggerFactory; - private readonly ILogger _logger; - private readonly PackageDependencyResolver _packageDepedencyResolver; - - private readonly object _gate = new object(); - private readonly Queue _projectsToProcess; - private readonly ProjectFileInfoCollection _projects; - - private ProjectLoader _loader; - private MSBuildOptions _options; - private string _solutionFileOrRootPath; - - public string Key { get; } = "MsBuild"; - public string Language { get; } = LanguageNames.CSharp; - public IEnumerable Extensions { get; } = new[] { ".cs" }; - - [ImportingConstructor] - public MSBuildProjectSystem( - IOmniSharpEnvironment environment, - OmniSharpWorkspace workspace, - IMSBuildLocator msbuildLocator, - DotNetCliService dotNetCliService, - MetadataFileReferenceCache metadataFileReferenceCache, - IEventEmitter eventEmitter, - IFileSystemWatcher fileSystemWatcher, - ILoggerFactory loggerFactory) - { - _environment = environment; - _workspace = workspace; - _propertyOverrides = msbuildLocator.RegisteredInstance.PropertyOverrides; - _dotNetCli = dotNetCliService; - _metadataFileReferenceCache = metadataFileReferenceCache; - _eventEmitter = eventEmitter; - _fileSystemWatcher = fileSystemWatcher; - _loggerFactory = loggerFactory; - - _projects = new ProjectFileInfoCollection(); - _projectsToProcess = new Queue(); - _logger = loggerFactory.CreateLogger(); - _packageDepedencyResolver = new PackageDependencyResolver(loggerFactory); - } - - public void Initalize(IConfiguration configuration) - { - _options = new MSBuildOptions(); - ConfigurationBinder.Bind(configuration, _options); - - if (_environment.LogLevel < LogLevel.Information) - { - var buildEnvironmentInfo = MSBuildHelpers.GetBuildEnvironmentInfo(); - _logger.LogDebug($"MSBuild environment: {Environment.NewLine}{buildEnvironmentInfo}"); - } - - _loader = new ProjectLoader(_options, _environment.TargetDirectory, _propertyOverrides, _loggerFactory); - - var initialProjectPaths = GetInitialProjectPaths(); - - foreach (var projectPath in initialProjectPaths) - { - if (!File.Exists(projectPath)) - { - _logger.LogWarning($"Found project that doesn't exist on disk: {projectPath}"); - continue; - } - - var project = LoadProject(projectPath); - if (project == null) - { - // Diagnostics reported while loading the project have already been logged. - continue; - } - - _projectsToProcess.Enqueue(project); - } - - ProcessProjects(); - } - - private IEnumerable GetInitialProjectPaths() - { - // If a solution was provided, use it. - if (!string.IsNullOrEmpty(_environment.SolutionFilePath)) - { - _solutionFileOrRootPath = _environment.SolutionFilePath; - return GetProjectPathsFromSolution(_environment.SolutionFilePath); - } - - // Otherwise, assume that the path provided is a directory and look for a solution there. - var solutionFilePath = FindSolutionFilePath(_environment.TargetDirectory, _logger); - if (!string.IsNullOrEmpty(solutionFilePath)) - { - _solutionFileOrRootPath = solutionFilePath; - return GetProjectPathsFromSolution(solutionFilePath); - } - - // Finally, if there isn't a single solution immediately available, - // Just process all of the projects beneath the root path. - _solutionFileOrRootPath = _environment.TargetDirectory; - return Directory.GetFiles(_environment.TargetDirectory, "*.csproj", SearchOption.AllDirectories); - } - - private IEnumerable GetProjectPathsFromSolution(string solutionFilePath) - { - _logger.LogInformation($"Detecting projects in '{solutionFilePath}'."); - - var solutionFile = SolutionFile.ParseFile(solutionFilePath); - var processedProjects = new HashSet(StringComparer.OrdinalIgnoreCase); - var result = new List(); - - foreach (var project in solutionFile.Projects) - { - if (project.IsSolutionFolder) - { - continue; - } - - // Solution files are assumed to contain relative paths to project files with Windows-style slashes. - var projectFilePath = project.RelativePath.Replace('\\', Path.DirectorySeparatorChar); - projectFilePath = Path.Combine(_environment.TargetDirectory, projectFilePath); - projectFilePath = Path.GetFullPath(projectFilePath); - - // Have we seen this project? If so, move on. - if (processedProjects.Contains(projectFilePath)) - { - continue; - } - - if (string.Equals(Path.GetExtension(projectFilePath), ".csproj", StringComparison.OrdinalIgnoreCase)) - { - result.Add(projectFilePath); - } - - processedProjects.Add(projectFilePath); - } - - return result; - } - - private void ProcessProjects() - { - while (_projectsToProcess.Count > 0) - { - var newProjects = new List(); - - while (_projectsToProcess.Count > 0) - { - var project = _projectsToProcess.Dequeue(); - - if (!_projects.ContainsKey(project.FilePath)) - { - AddProject(project); - } - else - { - _projects[project.FilePath] = project; - } - - newProjects.Add(project); - } - - // Next, update all projects. - foreach (var project in newProjects) - { - UpdateProject(project); - } - - // Finally, check for any unresolved dependencies in the projects we just processes. - foreach (var project in newProjects) - { - CheckForUnresolvedDependences(project, allowAutoRestore: true); - } - } - } - - private void AddProject(ProjectFileInfo project) - { - _projects.Add(project); - - var compilationOptions = CreateCompilationOptions(project); - - var projectInfo = ProjectInfo.Create( - id: project.Id, - version: VersionStamp.Create(), - name: project.Name, - assemblyName: project.AssemblyName, - language: LanguageNames.CSharp, - filePath: project.FilePath, - outputFilePath: project.TargetPath, - compilationOptions: compilationOptions); - - _workspace.AddProject(projectInfo); - - WatchProject(project); - } - - private void WatchProject(ProjectFileInfo project) - { - // TODO: This needs some improvement. Currently, it tracks both deletions and changes - // as "updates". We should properly remove projects that are deleted. - _fileSystemWatcher.Watch(project.FilePath, (file, changeType) => - { - OnProjectChanged(project.FilePath, allowAutoRestore: true); - }); - - if (!string.IsNullOrEmpty(project.ProjectAssetsFile)) - { - _fileSystemWatcher.Watch(project.ProjectAssetsFile, (file, changeType) => - { - OnProjectChanged(project.FilePath, allowAutoRestore: false); - }); - } - } - - private static CSharpCompilationOptions CreateCompilationOptions(ProjectFileInfo projectFileInfo) - { - var result = new CSharpCompilationOptions(projectFileInfo.OutputKind); - - result = result.WithAssemblyIdentityComparer(DesktopAssemblyIdentityComparer.Default); - - if (projectFileInfo.AllowUnsafeCode) - { - result = result.WithAllowUnsafe(true); - } - - var specificDiagnosticOptions = new Dictionary(projectFileInfo.SuppressedDiagnosticIds.Count) - { - // Ensure that specific warnings about assembly references are always suppressed. - { "CS1701", ReportDiagnostic.Suppress }, - { "CS1702", ReportDiagnostic.Suppress }, - { "CS1705", ReportDiagnostic.Suppress } - }; - - if (projectFileInfo.SuppressedDiagnosticIds.Any()) - { - foreach (var id in projectFileInfo.SuppressedDiagnosticIds) - { - if (!specificDiagnosticOptions.ContainsKey(id)) - { - specificDiagnosticOptions.Add(id, ReportDiagnostic.Suppress); - } - } - } - - result = result.WithSpecificDiagnosticOptions(specificDiagnosticOptions); - - if (projectFileInfo.SignAssembly && !string.IsNullOrEmpty(projectFileInfo.AssemblyOriginatorKeyFile)) - { - var keyFile = Path.Combine(projectFileInfo.Directory, projectFileInfo.AssemblyOriginatorKeyFile); - result = result.WithStrongNameProvider(new DesktopStrongNameProvider()) - .WithCryptoKeyFile(keyFile); - } - - if (!string.IsNullOrWhiteSpace(projectFileInfo.DocumentationFile)) - { - result = result.WithXmlReferenceResolver(XmlFileResolver.Default); - } - - return result; - } - - private static string FindSolutionFilePath(string rootPath, ILogger logger) - { - var solutionsFilePaths = Directory.GetFiles(rootPath, "*.sln"); - var result = SolutionSelector.Pick(solutionsFilePaths, rootPath); - - if (result.Message != null) - { - logger.LogInformation(result.Message); - } - - return result.FilePath; - } - - private ProjectFileInfo LoadProject(string projectFilePath) - { - _logger.LogInformation($"Loading project: {projectFilePath}"); - - ProjectFileInfo project; - ImmutableArray diagnostics; - - try - { - (project, diagnostics) = ProjectFileInfo.Create(projectFilePath, _loader); - - if (project == null) - { - _logger.LogWarning($"Failed to load project file '{projectFilePath}'."); - } - } - catch (Exception ex) - { - _logger.LogWarning($"Failed to load project file '{projectFilePath}'.", ex); - _eventEmitter.Error(ex, fileName: projectFilePath); - project = null; - } - - _eventEmitter.MSBuildProjectDiagnostics(projectFilePath, diagnostics); - - return project; - } - - private void OnProjectChanged(string projectFilePath, bool allowAutoRestore) - { - lock (_gate) - { - if (_projects.TryGetValue(projectFilePath, out var oldProjectFileInfo)) - { - ProjectFileInfo newProjectFileInfo; - ImmutableArray diagnostics; - - (newProjectFileInfo, diagnostics) = oldProjectFileInfo.Reload(_loader); - - if (newProjectFileInfo != null) - { - _projects[projectFilePath] = newProjectFileInfo; - - UpdateProject(newProjectFileInfo); - CheckForUnresolvedDependences(newProjectFileInfo, allowAutoRestore); - } - } - - ProcessProjects(); - } - } - - private void UpdateProject(ProjectFileInfo projectFileInfo) - { - var project = _workspace.CurrentSolution.GetProject(projectFileInfo.Id); - if (project == null) - { - _logger.LogError($"Could not locate project in workspace: {projectFileInfo.FilePath}"); - return; - } - - UpdateSourceFiles(project, projectFileInfo.SourceFiles); - UpdateParseOptions(project, projectFileInfo.LanguageVersion, projectFileInfo.PreprocessorSymbolNames, !string.IsNullOrWhiteSpace(projectFileInfo.DocumentationFile)); - UpdateProjectReferences(project, projectFileInfo.ProjectReferences); - UpdateReferences(project, projectFileInfo.References); - } - - private void UpdateSourceFiles(Project project, IList sourceFiles) - { - var currentDocuments = project.Documents.ToDictionary(d => d.FilePath, d => d.Id); - - // Add source files to the project. - foreach (var sourceFile in sourceFiles) - { - WatchDirectoryContainingFile(sourceFile); - - // If a document for this source file already exists in the project, carry on. - if (currentDocuments.Remove(sourceFile)) - { - continue; - } - - // If the source file doesn't exist on disk, don't try to add it. - if (!File.Exists(sourceFile)) - { - continue; - } - - _workspace.AddDocument(project.Id, sourceFile); - } - - // Removing any remaining documents from the project. - foreach (var currentDocument in currentDocuments) - { - _workspace.RemoveDocument(currentDocument.Value); - } - } - - private void WatchDirectoryContainingFile(string sourceFile) - => _fileSystemWatcher.Watch(Path.GetDirectoryName(sourceFile), OnDirectoryFileChanged); - - private void OnDirectoryFileChanged(string path, FileChangeType changeType) - { - // Hosts may not have passed through a file change type - if (changeType == FileChangeType.Unspecified && !File.Exists(path) || changeType == FileChangeType.Delete) - { - foreach (var documentId in _workspace.CurrentSolution.GetDocumentIdsWithFilePath(path)) - { - _workspace.RemoveDocument(documentId); - } - } - - if (changeType == FileChangeType.Unspecified || changeType == FileChangeType.Create) - { - // Only add cs files. Also, make sure the path is a file, and not a directory name that happens to end in ".cs" - if (string.Equals(Path.GetExtension(path), ".cs", StringComparison.CurrentCultureIgnoreCase) && File.Exists(path)) - { - // Use the buffer manager to add the new file to the appropriate projects - // Hosts that don't pass the FileChangeType may wind up updating the buffer twice - _workspace.BufferManager.UpdateBufferAsync(new UpdateBufferRequest() { FileName = path, FromDisk = true }).Wait(); - } - } - } - - private void UpdateParseOptions(Project project, LanguageVersion languageVersion, IEnumerable preprocessorSymbolNames, bool generateXmlDocumentation) - { - var existingParseOptions = (CSharpParseOptions)project.ParseOptions; - - if (existingParseOptions.LanguageVersion == languageVersion && - Enumerable.SequenceEqual(existingParseOptions.PreprocessorSymbolNames, preprocessorSymbolNames) && - (existingParseOptions.DocumentationMode == DocumentationMode.Diagnose) == generateXmlDocumentation) - { - // No changes to make. Moving on. - return; - } - - var parseOptions = new CSharpParseOptions(languageVersion); - - if (preprocessorSymbolNames.Any()) - { - parseOptions = parseOptions.WithPreprocessorSymbols(preprocessorSymbolNames); - } - - if (generateXmlDocumentation) - { - parseOptions = parseOptions.WithDocumentationMode(DocumentationMode.Diagnose); - } - - _workspace.SetParseOptions(project.Id, parseOptions); - } - - private void UpdateProjectReferences(Project project, ImmutableArray projectReferencePaths) - { - _logger.LogInformation($"Update project: {project.Name}"); - - var existingProjectReferences = new HashSet(project.ProjectReferences); - var addedProjectReferences = new HashSet(); - - foreach (var projectReferencePath in projectReferencePaths) - { - if (!_projects.TryGetValue(projectReferencePath, out var referencedProject)) - { - if (File.Exists(projectReferencePath)) - { - _logger.LogInformation($"Found referenced project outside root directory: {projectReferencePath}"); - - // We've found a project reference that we didn't know about already, but it exists on disk. - // This is likely a project that is outside of OmniSharp's TargetDirectory. - referencedProject = LoadProject(projectReferencePath); - - if (referencedProject != null) - { - AddProject(referencedProject); - - // Ensure this project is queued to be updated later. - _projectsToProcess.Enqueue(referencedProject); - } - } - } - - if (referencedProject == null) - { - _logger.LogWarning($"Unable to resolve project reference '{projectReferencePath}' for '{project.Name}'."); - continue; - } - - var projectReference = new ProjectReference(referencedProject.Id); - - if (existingProjectReferences.Remove(projectReference)) - { - // This reference already exists - continue; - } - - if (!addedProjectReferences.Contains(projectReference)) - { - _workspace.AddProjectReference(project.Id, projectReference); - addedProjectReferences.Add(projectReference); - } - } - - foreach (var existingProjectReference in existingProjectReferences) - { - _workspace.RemoveProjectReference(project.Id, existingProjectReference); - } - } - - private class MetadataReferenceComparer : IEqualityComparer - { - public static MetadataReferenceComparer Instance { get; } = new MetadataReferenceComparer(); - - public bool Equals(MetadataReference x, MetadataReference y) - => x is PortableExecutableReference pe1 && y is PortableExecutableReference pe2 - ? StringComparer.OrdinalIgnoreCase.Equals(pe1.FilePath, pe2.FilePath) - : EqualityComparer.Default.Equals(x, y); - - public int GetHashCode(MetadataReference obj) - => obj is PortableExecutableReference pe - ? StringComparer.OrdinalIgnoreCase.GetHashCode(pe.FilePath) - : EqualityComparer.Default.GetHashCode(obj); - } - - private void UpdateReferences(Project project, ImmutableArray referencePaths) - { - var referencesToRemove = new HashSet(project.MetadataReferences, MetadataReferenceComparer.Instance); - var referencesToAdd = new HashSet(MetadataReferenceComparer.Instance); - - foreach (var referencePath in referencePaths) - { - if (!File.Exists(referencePath)) - { - _logger.LogWarning($"Unable to resolve assembly '{referencePath}'"); - } - else - { - var reference = _metadataFileReferenceCache.GetMetadataReference(referencePath); - - if (referencesToRemove.Remove(reference)) - { - continue; - } - - if (!referencesToAdd.Contains(reference)) - { - _logger.LogDebug($"Adding reference '{referencePath}' to '{project.Name}'."); - _workspace.AddMetadataReference(project.Id, reference); - referencesToAdd.Add(reference); - } - } - } - - foreach (var reference in referencesToRemove) - { - _workspace.RemoveMetadataReference(project.Id, reference); - } - } - - private void CheckForUnresolvedDependences(ProjectFileInfo projectFile, bool allowAutoRestore) - { - var unresolvedPackageReferences = _packageDepedencyResolver.FindUnresolvedPackageReferences(projectFile); - if (unresolvedPackageReferences.IsEmpty) - { - return; - } - - var unresolvedDependencies = unresolvedPackageReferences.Select(packageReference => - new PackageDependency - { - Name = packageReference.Dependency.Id, - Version = packageReference.Dependency.VersionRange.ToNormalizedString() - }); - - if (allowAutoRestore && _options.EnablePackageAutoRestore) - { - _dotNetCli.RestoreAsync(projectFile.Directory, onFailure: () => - { - _eventEmitter.UnresolvedDepdendencies(projectFile.FilePath, unresolvedDependencies); - }); - } - else - { - _eventEmitter.UnresolvedDepdendencies(projectFile.FilePath, unresolvedDependencies); - } - } - - private ProjectFileInfo GetProjectFileInfo(string path) - { - if (!_projects.TryGetValue(path, out var projectFileInfo)) - { - return null; - } - - return projectFileInfo; - } - - Task IProjectSystem.GetWorkspaceModelAsync(WorkspaceInformationRequest request) - { - var info = new MSBuildWorkspaceInfo( - _solutionFileOrRootPath, _projects, - excludeSourceFiles: request?.ExcludeSourceFiles ?? false); - - return Task.FromResult(info); - } - - Task IProjectSystem.GetProjectModelAsync(string filePath) - { - var document = _workspace.GetDocument(filePath); - - var projectFilePath = document != null - ? document.Project.FilePath - : filePath; - - var projectFileInfo = GetProjectFileInfo(projectFilePath); - if (projectFileInfo == null) - { - _logger.LogDebug($"Could not locate project for '{projectFilePath}'"); - return Task.FromResult(null); - } - - var info = new MSBuildProjectInfo(projectFileInfo); - - return Task.FromResult(info); - } - } -} diff --git a/src/OmniSharp.MSBuild/Resolution/PackageDependencyResolver.cs b/src/OmniSharp.MSBuild/PackageDependencyChecker.cs similarity index 69% rename from src/OmniSharp.MSBuild/Resolution/PackageDependencyResolver.cs rename to src/OmniSharp.MSBuild/PackageDependencyChecker.cs index 01cbc37cf3..17d7881929 100644 --- a/src/OmniSharp.MSBuild/Resolution/PackageDependencyResolver.cs +++ b/src/OmniSharp.MSBuild/PackageDependencyChecker.cs @@ -5,17 +5,55 @@ using System.Linq; using Microsoft.Extensions.Logging; using NuGet.ProjectModel; +using OmniSharp.Eventing; +using OmniSharp.Models.Events; using OmniSharp.MSBuild.ProjectFile; +using OmniSharp.Options; +using OmniSharp.Services; -namespace OmniSharp.MSBuild.Resolution +namespace OmniSharp.MSBuild { - internal class PackageDependencyResolver + internal class PackageDependencyChecker { private readonly ILogger _logger; + private readonly IEventEmitter _eventEmitter; + private readonly DotNetCliService _dotNetCli; + private readonly MSBuildOptions _options; - public PackageDependencyResolver(ILoggerFactory loggerFactory) + public PackageDependencyChecker(ILoggerFactory loggerFactory, IEventEmitter eventEmitter, DotNetCliService dotNetCli, MSBuildOptions options) { - _logger = loggerFactory.CreateLogger(); + _logger = loggerFactory.CreateLogger(); + _eventEmitter = eventEmitter; + _dotNetCli = dotNetCli; + _options = options; + } + + public void CheckForUnresolvedDependences(ProjectFileInfo projectFile, bool allowAutoRestore) + { + var unresolvedPackageReferences = FindUnresolvedPackageReferences(projectFile); + if (unresolvedPackageReferences.IsEmpty) + { + return; + } + + var unresolvedDependencies = unresolvedPackageReferences.Select(packageReference => + new PackageDependency + { + Name = packageReference.Dependency.Id, + Version = packageReference.Dependency.VersionRange.ToNormalizedString() + }); + + if (allowAutoRestore && _options.EnablePackageAutoRestore) + { + _dotNetCli.RestoreAsync(projectFile.Directory, onFailure: () => + { + _eventEmitter.UnresolvedDepdendencies(projectFile.FilePath, unresolvedDependencies); + }); + } + else + { + _eventEmitter.UnresolvedDepdendencies(projectFile.FilePath, unresolvedDependencies); + } } public ImmutableArray FindUnresolvedPackageReferences(ProjectFileInfo projectFile) diff --git a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.ProjectData.cs b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.ProjectData.cs index 61f3dae3c5..7117155e99 100644 --- a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.ProjectData.cs +++ b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.ProjectData.cs @@ -4,11 +4,12 @@ using System.IO; using System.Linq; using System.Runtime.Versioning; -using Microsoft.Build.Execution; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using NuGet.Packaging.Core; +using MSB = Microsoft.Build; + namespace OmniSharp.MSBuild.ProjectFile { internal partial class ProjectFileInfo @@ -54,12 +55,7 @@ private ProjectData( ImmutableArray preprocessorSymbolNames, ImmutableArray suppressedDiagnosticIds, bool signAssembly, - string assemblyOriginatorKeyFile, - ImmutableArray sourceFiles, - ImmutableArray projectReferences, - ImmutableArray references, - ImmutableArray packageReferences, - ImmutableArray analyzers) + string assemblyOriginatorKeyFile) { Guid = guid; Name = name; @@ -81,7 +77,30 @@ private ProjectData( SignAssembly = signAssembly; AssemblyOriginatorKeyFile = assemblyOriginatorKeyFile; + } + private ProjectData( + Guid guid, string name, + string assemblyName, string targetPath, string outputPath, string projectAssetsFile, + FrameworkName targetFramework, + ImmutableArray targetFrameworks, + OutputKind outputKind, + LanguageVersion languageVersion, + bool allowUnsafeCode, + string documentationFile, + ImmutableArray preprocessorSymbolNames, + ImmutableArray suppressedDiagnosticIds, + bool signAssembly, + string assemblyOriginatorKeyFile, + ImmutableArray sourceFiles, + ImmutableArray projectReferences, + ImmutableArray references, + ImmutableArray packageReferences, + ImmutableArray analyzers) + : this(guid, name, assemblyName, targetPath, outputPath, projectAssetsFile, + targetFramework, targetFrameworks, outputKind, languageVersion, allowUnsafeCode, + documentationFile, preprocessorSymbolNames, suppressedDiagnosticIds, signAssembly, assemblyOriginatorKeyFile) + { SourceFiles = sourceFiles; ProjectReferences = projectReferences; References = references; @@ -89,7 +108,41 @@ private ProjectData( Analyzers = analyzers; } - public static ProjectData Create(ProjectInstance projectInstance) + public static ProjectData Create(MSB.Evaluation.Project project) + { + var guid = PropertyConverter.ToGuid(project.GetPropertyValue(PropertyNames.ProjectGuid)); + var name = project.GetPropertyValue(PropertyNames.ProjectName); + var assemblyName = project.GetPropertyValue(PropertyNames.AssemblyName); + var targetPath = project.GetPropertyValue(PropertyNames.TargetPath); + var outputPath = project.GetPropertyValue(PropertyNames.OutputPath); + var projectAssetsFile = project.GetPropertyValue(PropertyNames.ProjectAssetsFile); + + var targetFramework = new FrameworkName(project.GetPropertyValue(PropertyNames.TargetFrameworkMoniker)); + + var targetFrameworkValue = project.GetPropertyValue(PropertyNames.TargetFramework); + var targetFrameworks = PropertyConverter.SplitList(project.GetPropertyValue(PropertyNames.TargetFrameworks), ';'); + + if (!string.IsNullOrWhiteSpace(targetFrameworkValue) && targetFrameworks.Length == 0) + { + targetFrameworks = ImmutableArray.Create(targetFrameworkValue); + } + + var languageVersion = PropertyConverter.ToLanguageVersion(project.GetPropertyValue(PropertyNames.LangVersion)); + var allowUnsafeCode = PropertyConverter.ToBoolean(project.GetPropertyValue(PropertyNames.AllowUnsafeBlocks), defaultValue: false); + var outputKind = PropertyConverter.ToOutputKind(project.GetPropertyValue(PropertyNames.OutputType)); + var documentationFile = project.GetPropertyValue(PropertyNames.DocumentationFile); + var preprocessorSymbolNames = PropertyConverter.ToPreprocessorSymbolNames(project.GetPropertyValue(PropertyNames.DefineConstants)); + var suppressedDiagnosticIds = PropertyConverter.ToSuppressedDiagnosticIds(project.GetPropertyValue(PropertyNames.NoWarn)); + var signAssembly = PropertyConverter.ToBoolean(project.GetPropertyValue(PropertyNames.SignAssembly), defaultValue: false); + var assemblyOriginatorKeyFile = project.GetPropertyValue(PropertyNames.AssemblyOriginatorKeyFile); + + return new ProjectData( + guid, name, assemblyName, targetPath, outputPath, projectAssetsFile, + targetFramework, targetFrameworks, outputKind, languageVersion, allowUnsafeCode, + documentationFile, preprocessorSymbolNames, suppressedDiagnosticIds, signAssembly, assemblyOriginatorKeyFile); + } + + public static ProjectData Create(MSB.Execution.ProjectInstance projectInstance) { var guid = PropertyConverter.ToGuid(projectInstance.GetPropertyValue(PropertyNames.ProjectGuid)); var name = projectInstance.GetPropertyValue(PropertyNames.ProjectName); @@ -113,7 +166,7 @@ public static ProjectData Create(ProjectInstance projectInstance) var outputKind = PropertyConverter.ToOutputKind(projectInstance.GetPropertyValue(PropertyNames.OutputType)); var documentationFile = projectInstance.GetPropertyValue(PropertyNames.DocumentationFile); var preprocessorSymbolNames = PropertyConverter.ToPreprocessorSymbolNames(projectInstance.GetPropertyValue(PropertyNames.DefineConstants)); - var suppressDiagnosticIds = PropertyConverter.ToSuppressDiagnosticIds(projectInstance.GetPropertyValue(PropertyNames.NoWarn)); + var suppressedDiagnosticIds = PropertyConverter.ToSuppressedDiagnosticIds(projectInstance.GetPropertyValue(PropertyNames.NoWarn)); var signAssembly = PropertyConverter.ToBoolean(projectInstance.GetPropertyValue(PropertyNames.SignAssembly), defaultValue: false); var assemblyOriginatorKeyFile = projectInstance.GetPropertyValue(PropertyNames.AssemblyOriginatorKeyFile); @@ -128,18 +181,18 @@ public static ProjectData Create(ProjectInstance projectInstance) return new ProjectData(guid, name, assemblyName, targetPath, outputPath, projectAssetsFile, targetFramework, targetFrameworks, - outputKind, languageVersion, allowUnsafeCode, documentationFile, preprocessorSymbolNames, suppressDiagnosticIds, + outputKind, languageVersion, allowUnsafeCode, documentationFile, preprocessorSymbolNames, suppressedDiagnosticIds, signAssembly, assemblyOriginatorKeyFile, sourceFiles, projectReferences, references, packageReferences, analyzers); } - private static bool ReferenceSourceTargetIsNotProjectReference(ProjectItemInstance item) + private static bool ReferenceSourceTargetIsNotProjectReference(MSB.Execution.ProjectItemInstance item) => item.GetMetadataValue(MetadataNames.ReferenceSourceTarget) != ItemNames.ProjectReference; private static bool FileNameIsNotGenerated(string filePath) => !Path.GetFileName(filePath).StartsWith("TemporaryGeneratedFile_"); - private static ImmutableArray GetFullPaths(IEnumerable items, Func filter = null) + private static ImmutableArray GetFullPaths(IEnumerable items, Func filter = null) { var builder = ImmutableArray.CreateBuilder(); var addedSet = new HashSet(); @@ -159,7 +212,7 @@ private static ImmutableArray GetFullPaths(IEnumerable GetPackageReferences(ICollection items) + private static ImmutableArray GetPackageReferences(ICollection items) { var builder = ImmutableArray.CreateBuilder(items.Count); var addedSet = new HashSet(); diff --git a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.cs b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.cs index 13265a9c7a..bf30719a01 100644 --- a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.cs +++ b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.cs @@ -46,11 +46,6 @@ internal partial class ProjectFileInfo public ImmutableArray PackageReferences => _data.PackageReferences; public ImmutableArray Analyzers => _data.Analyzers; - internal ProjectFileInfo(string filePath) - { - this.FilePath = filePath; - } - private ProjectFileInfo( ProjectId id, string filePath, @@ -63,7 +58,23 @@ private ProjectFileInfo( _data = data; } - public static (ProjectFileInfo projectFileInfo, ImmutableArray diagnostics) Create(string filePath, ProjectLoader loader) + internal static ProjectFileInfo CreateEmpty(string filePath) + { + var id = ProjectId.CreateNewId(debugName: filePath); + + return new ProjectFileInfo(id, filePath, data: null); + } + + internal static ProjectFileInfo CreateNoBuild(string filePath, ProjectLoader loader) + { + var id = ProjectId.CreateNewId(debugName: filePath); + var project = loader.EvaluateProjectFile(filePath); + var data = ProjectData.Create(project); + + return new ProjectFileInfo(id, filePath, data); + } + + public static (ProjectFileInfo projectFileInfo, ImmutableArray diagnostics) Load(string filePath, ProjectLoader loader) { if (!File.Exists(filePath)) { diff --git a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfoCollection.cs b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfoCollection.cs index 36db94057e..ff186a0029 100644 --- a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfoCollection.cs +++ b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfoCollection.cs @@ -1,10 +1,9 @@ using System; -using System.Collections; using System.Collections.Generic; namespace OmniSharp.MSBuild.ProjectFile { - internal class ProjectFileInfoCollection : IEnumerable + internal class ProjectFileInfoCollection { private readonly List _items; private readonly Dictionary _itemMap; @@ -15,18 +14,7 @@ public ProjectFileInfoCollection() _itemMap = new Dictionary(StringComparer.OrdinalIgnoreCase); } - public IEnumerator GetEnumerator() - { - foreach (var item in _items) - { - yield return item; - } - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } + public IEnumerable GetItems() => _items.ToArray(); public void Add(ProjectFileInfo fileInfo) { @@ -49,6 +37,18 @@ public bool ContainsKey(string filePath) return _itemMap.ContainsKey(filePath); } + public bool Remove(string filePath) + { + if (_itemMap.TryGetValue(filePath, out var fileInfo)) + { + _items.Remove(fileInfo); + _itemMap.Remove(filePath); + return true; + } + + return false; + } + public bool TryGetValue(string filePath, out ProjectFileInfo fileInfo) { return _itemMap.TryGetValue(filePath, out fileInfo); diff --git a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfoExtensions.cs b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfoExtensions.cs new file mode 100644 index 0000000000..30aca47135 --- /dev/null +++ b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfoExtensions.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace OmniSharp.MSBuild.ProjectFile +{ + internal static class ProjectFileInfoExtensions + { + public static CSharpCompilationOptions CreateCompilationOptions(this ProjectFileInfo projectFileInfo) + { + var result = new CSharpCompilationOptions(projectFileInfo.OutputKind); + + result = result.WithAssemblyIdentityComparer(DesktopAssemblyIdentityComparer.Default); + + if (projectFileInfo.AllowUnsafeCode) + { + result = result.WithAllowUnsafe(true); + } + + var specificDiagnosticOptions = new Dictionary(projectFileInfo.SuppressedDiagnosticIds.Count) + { + // Ensure that specific warnings about assembly references are always suppressed. + { "CS1701", ReportDiagnostic.Suppress }, + { "CS1702", ReportDiagnostic.Suppress }, + { "CS1705", ReportDiagnostic.Suppress } + }; + + if (projectFileInfo.SuppressedDiagnosticIds.Any()) + { + foreach (var id in projectFileInfo.SuppressedDiagnosticIds) + { + if (!specificDiagnosticOptions.ContainsKey(id)) + { + specificDiagnosticOptions.Add(id, ReportDiagnostic.Suppress); + } + } + } + + result = result.WithSpecificDiagnosticOptions(specificDiagnosticOptions); + + if (projectFileInfo.SignAssembly && !string.IsNullOrEmpty(projectFileInfo.AssemblyOriginatorKeyFile)) + { + var keyFile = Path.Combine(projectFileInfo.Directory, projectFileInfo.AssemblyOriginatorKeyFile); + result = result.WithStrongNameProvider(new DesktopStrongNameProvider()) + .WithCryptoKeyFile(keyFile); + } + + if (!string.IsNullOrWhiteSpace(projectFileInfo.DocumentationFile)) + { + result = result.WithXmlReferenceResolver(XmlFileResolver.Default); + } + + return result; + } + + public static ProjectInfo CreateProjectInfo(this ProjectFileInfo projectFileInfo) + { + return ProjectInfo.Create( + id: projectFileInfo.Id, + version: VersionStamp.Create(), + name: projectFileInfo.Name, + assemblyName: projectFileInfo.AssemblyName, + language: LanguageNames.CSharp, + filePath: projectFileInfo.FilePath, + outputFilePath: projectFileInfo.TargetPath, + compilationOptions: projectFileInfo.CreateCompilationOptions()); + } + } +} diff --git a/src/OmniSharp.MSBuild/ProjectFile/PropertyConverter.cs b/src/OmniSharp.MSBuild/ProjectFile/PropertyConverter.cs index f856e4dddc..0fa9165154 100644 --- a/src/OmniSharp.MSBuild/ProjectFile/PropertyConverter.cs +++ b/src/OmniSharp.MSBuild/ProjectFile/PropertyConverter.cs @@ -80,7 +80,7 @@ public static ImmutableArray ToPreprocessorSymbolNames(string propertyVa return ImmutableArray.CreateRange(values); } - public static ImmutableArray ToSuppressDiagnosticIds(string propertyValue) + public static ImmutableArray ToSuppressedDiagnosticIds(string propertyValue) { if (string.IsNullOrWhiteSpace(propertyValue)) { diff --git a/src/OmniSharp.MSBuild/ProjectLoader.cs b/src/OmniSharp.MSBuild/ProjectLoader.cs index 87995f3f89..121a554929 100644 --- a/src/OmniSharp.MSBuild/ProjectLoader.cs +++ b/src/OmniSharp.MSBuild/ProjectLoader.cs @@ -15,24 +15,14 @@ internal class ProjectLoader { private readonly ILogger _logger; private readonly Dictionary _globalProperties; - private readonly MSB.Evaluation.ProjectCollection _projectCollection; - private readonly string _toolsVersion; + private readonly MSBuildOptions _options; public ProjectLoader(MSBuildOptions options, string solutionDirectory, ImmutableDictionary propertyOverrides, ILoggerFactory loggerFactory) { _logger = loggerFactory.CreateLogger(); - options = options ?? new MSBuildOptions(); + _options = options ?? new MSBuildOptions(); - _globalProperties = CreateGlobalProperties(options, solutionDirectory, propertyOverrides, _logger); - _projectCollection = new MSB.Evaluation.ProjectCollection(_globalProperties); - - var toolsVersion = options.ToolsVersion; - if (string.IsNullOrEmpty(toolsVersion) || Version.TryParse(toolsVersion, out _)) - { - toolsVersion = _projectCollection.DefaultToolsVersion; - } - - _toolsVersion = GetLegalToolsetVersion(toolsVersion, _projectCollection.Toolsets); + _globalProperties = CreateGlobalProperties(_options, solutionDirectory, propertyOverrides, _logger); } private static Dictionary CreateGlobalProperties( @@ -66,8 +56,7 @@ private static Dictionary CreateGlobalProperties( public (MSB.Execution.ProjectInstance projectInstance, ImmutableArray diagnostics) BuildProject(string filePath) { - // Evaluate the MSBuild project - var evaluatedProject = _projectCollection.LoadProject(filePath, _toolsVersion); + var evaluatedProject = EvaluateProjectFile(filePath); SetTargetFrameworkIfNeeded(evaluatedProject); @@ -84,6 +73,22 @@ private static Dictionary CreateGlobalProperties( : (null, diagnostics); } + public MSB.Evaluation.Project EvaluateProjectFile(string filePath) + { + // Evaluate the MSBuild project + var projectCollection = new MSB.Evaluation.ProjectCollection(_globalProperties); + + var toolsVersion = _options.ToolsVersion; + if (string.IsNullOrEmpty(toolsVersion) || Version.TryParse(toolsVersion, out _)) + { + toolsVersion = projectCollection.DefaultToolsVersion; + } + + toolsVersion = GetLegalToolsetVersion(toolsVersion, projectCollection.Toolsets); + + return projectCollection.LoadProject(filePath, toolsVersion); + } + private static void SetTargetFrameworkIfNeeded(MSB.Evaluation.Project evaluatedProject) { var targetFramework = evaluatedProject.GetPropertyValue(PropertyNames.TargetFramework); diff --git a/src/OmniSharp.MSBuild/ProjectManager.cs b/src/OmniSharp.MSBuild/ProjectManager.cs new file mode 100644 index 0000000000..766a42729e --- /dev/null +++ b/src/OmniSharp.MSBuild/ProjectManager.cs @@ -0,0 +1,469 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Threading.Tasks.Dataflow; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.Extensions.Logging; +using OmniSharp.Eventing; +using OmniSharp.FileWatching; +using OmniSharp.Models.UpdateBuffer; +using OmniSharp.MSBuild.Logging; +using OmniSharp.MSBuild.Models.Events; +using OmniSharp.MSBuild.ProjectFile; +using OmniSharp.Services; +using OmniSharp.Utilities; + +namespace OmniSharp.MSBuild +{ + internal class ProjectManager : DisposableObject + { + private readonly ILogger _logger; + private readonly IEventEmitter _eventEmitter; + private readonly IFileSystemWatcher _fileSystemWatcher; + private readonly MetadataFileReferenceCache _metadataFileReferenceCache; + private readonly PackageDependencyChecker _packageDependencyChecker; + private readonly ProjectFileInfoCollection _projectFiles; + private readonly ProjectLoader _projectLoader; + private readonly OmniSharpWorkspace _workspace; + + private const int LoopDelay = 100; // milliseconds + private readonly BufferBlock _queue; + private readonly CancellationTokenSource _processLoopCancellation; + private readonly Task _processLoopTask; + private bool _processingQueue; + + public ProjectManager(ILoggerFactory loggerFactory, IEventEmitter eventEmitter, IFileSystemWatcher fileSystemWatcher, MetadataFileReferenceCache metadataFileReferenceCache, PackageDependencyChecker packageDependencyChecker, ProjectLoader projectLoader, OmniSharpWorkspace workspace) + { + _logger = loggerFactory.CreateLogger(); + _eventEmitter = eventEmitter; + _fileSystemWatcher = fileSystemWatcher; + _metadataFileReferenceCache = metadataFileReferenceCache; + _packageDependencyChecker = packageDependencyChecker; + _projectFiles = new ProjectFileInfoCollection(); + _projectLoader = projectLoader; + _workspace = workspace; + + _queue = new BufferBlock(); + _processLoopCancellation = new CancellationTokenSource(); + _processLoopTask = Task.Run(() => ProcessLoopAsync(_processLoopCancellation.Token)); + } + + protected override void DisposeCore(bool disposing) + { + if (IsDisposed) + { + return; + } + + _processLoopCancellation.Cancel(); + _processLoopCancellation.Dispose(); + } + + public IEnumerable GetAllProjects() => _projectFiles.GetItems(); + public bool TryGetProject(string projectFilePath, out ProjectFileInfo projectFileInfo) => _projectFiles.TryGetValue(projectFilePath, out projectFileInfo); + + public void QueueProjectUpdate(string projectFilePath) + { + _logger.LogInformation($"Queue project update for '{projectFilePath}'"); + _queue.Post(projectFilePath); + } + + public async Task WaitForQueueEmptyAsync() + { + while (_queue.Count > 0 || _processingQueue) + { + await Task.Delay(LoopDelay); + } + } + + private async Task ProcessLoopAsync(CancellationToken cancellationToken) + { + while (true) + { + await Task.Delay(LoopDelay, cancellationToken); + ProcessQueue(cancellationToken); + } + } + + private void ProcessQueue(CancellationToken cancellationToken) + { + _processingQueue = true; + try + { + HashSet processedSet = null; + + while (_queue.TryReceive(out var projectFilePath)) + { + if (cancellationToken.IsCancellationRequested) + { + break; + } + + if (processedSet == null) + { + processedSet = new HashSet(StringComparer.OrdinalIgnoreCase); + } + + // Ensure that we don't process the same project twice. + if (!processedSet.Add(projectFilePath)) + { + continue; + } + + // TODO: Handle removing project + + // update or add project + if (_projectFiles.TryGetValue(projectFilePath, out var projectFileInfo)) + { + projectFileInfo = ReloadProject(projectFileInfo); + _projectFiles[projectFilePath] = projectFileInfo; + } + else + { + projectFileInfo = LoadProject(projectFilePath); + AddProject(projectFileInfo); + } + } + + if (processedSet != null) + { + foreach (var projectFilePath in processedSet) + { + UpdateProject(projectFilePath); + } + + foreach (var projectFilePath in processedSet) + { + if (_projectFiles.TryGetValue(projectFilePath, out var projectFileInfo)) + { + _packageDependencyChecker.CheckForUnresolvedDependences(projectFileInfo, allowAutoRestore: true); + } + } + } + } + finally + { + _processingQueue = false; + } + } + + private ProjectFileInfo LoadProject(string projectFilePath) + => LoadOrReloadProject(projectFilePath, () => ProjectFileInfo.Load(projectFilePath, _projectLoader)); + + private ProjectFileInfo ReloadProject(ProjectFileInfo projectFileInfo) + => LoadOrReloadProject(projectFileInfo.FilePath, () => projectFileInfo.Reload(_projectLoader)); + + private ProjectFileInfo LoadOrReloadProject(string projectFilePath, Func<(ProjectFileInfo, ImmutableArray)> loadFunc) + { + _logger.LogInformation($"Loading project: {projectFilePath}"); + + ProjectFileInfo projectFileInfo; + ImmutableArray diagnostics; + + try + { + (projectFileInfo, diagnostics) = loadFunc(); + + if (projectFileInfo == null) + { + _logger.LogWarning($"Failed to load project file '{projectFilePath}'."); + } + } + catch (Exception ex) + { + _logger.LogWarning($"Failed to load project file '{projectFilePath}'.", ex); + _eventEmitter.Error(ex, fileName: projectFilePath); + projectFileInfo = null; + } + + _eventEmitter.MSBuildProjectDiagnostics(projectFilePath, diagnostics); + + return projectFileInfo; + } + + private bool RemoveProject(string projectFilePath) + { + if (!_projectFiles.TryGetValue(projectFilePath, out var projectFileInfo)) + { + return false; + } + + _projectFiles.Remove(projectFilePath); + + var newSolution = _workspace.CurrentSolution.RemoveProject(projectFileInfo.Id); + + if (!_workspace.TryApplyChanges(newSolution)) + { + _logger.LogError($"Failed to remove project from workspace: '{projectFileInfo.FilePath}'"); + } + + // TODO: Stop watching project files + + return true; + } + + private void AddProject(ProjectFileInfo projectFileInfo) + { + _logger.LogInformation($"Adding project '{projectFileInfo.FilePath}'"); + + _projectFiles.Add(projectFileInfo); + + var projectInfo = projectFileInfo.CreateProjectInfo(); + var newSolution = _workspace.CurrentSolution.AddProject(projectInfo); + + if (!_workspace.TryApplyChanges(newSolution)) + { + _logger.LogError($"Failed to add project to workspace: '{projectFileInfo.FilePath}'"); + } + + WatchProjectFiles(projectFileInfo); + } + + private void WatchProjectFiles(ProjectFileInfo projectFileInfo) + { + // TODO: This needs some improvement. Currently, it tracks both deletions and changes + // as "updates". We should properly remove projects that are deleted. + _fileSystemWatcher.Watch(projectFileInfo.FilePath, (file, changeType) => + { + QueueProjectUpdate(projectFileInfo.FilePath); + }); + + if (!string.IsNullOrEmpty(projectFileInfo.ProjectAssetsFile)) + { + _fileSystemWatcher.Watch(projectFileInfo.ProjectAssetsFile, (file, changeType) => + { + QueueProjectUpdate(projectFileInfo.FilePath); + }); + + var restoreDirectory = Path.GetDirectoryName(projectFileInfo.ProjectAssetsFile); + var nugetFileBase = Path.Combine(restoreDirectory, Path.GetFileName(projectFileInfo.FilePath) + ".nuget"); + var nugetCacheFile = nugetFileBase + ".cache"; + var nugetPropsFile = nugetFileBase + ".g.props"; + var nugetTargetsFile = nugetFileBase + ".g.targets"; + + _fileSystemWatcher.Watch(nugetCacheFile, (file, changeType) => + { + QueueProjectUpdate(projectFileInfo.FilePath); + }); + + _fileSystemWatcher.Watch(nugetPropsFile, (file, changeType) => + { + QueueProjectUpdate(projectFileInfo.FilePath); + }); + + _fileSystemWatcher.Watch(nugetTargetsFile, (file, changeType) => + { + QueueProjectUpdate(projectFileInfo.FilePath); + }); + } + } + + private void UpdateProject(string projectFilePath) + { + if (!_projectFiles.TryGetValue(projectFilePath, out var projectFileInfo)) + { + _logger.LogError($"Attemped to update project that is not loaded: {projectFilePath}"); + return; + } + + var project = _workspace.CurrentSolution.GetProject(projectFileInfo.Id); + if (project == null) + { + _logger.LogError($"Could not locate project in workspace: {projectFileInfo.FilePath}"); + return; + } + + UpdateSourceFiles(project, projectFileInfo.SourceFiles); + UpdateParseOptions(project, projectFileInfo.LanguageVersion, projectFileInfo.PreprocessorSymbolNames, !string.IsNullOrWhiteSpace(projectFileInfo.DocumentationFile)); + UpdateProjectReferences(project, projectFileInfo.ProjectReferences); + UpdateReferences(project, projectFileInfo.References); + } + + private void UpdateSourceFiles(Project project, IList sourceFiles) + { + var currentDocuments = project.Documents.ToDictionary(d => d.FilePath, d => d.Id); + + // Add source files to the project. + foreach (var sourceFile in sourceFiles) + { + _fileSystemWatcher.Watch(Path.GetDirectoryName(sourceFile), OnDirectoryFileChanged); + + // If a document for this source file already exists in the project, carry on. + if (currentDocuments.Remove(sourceFile)) + { + continue; + } + + // If the source file doesn't exist on disk, don't try to add it. + if (!File.Exists(sourceFile)) + { + continue; + } + + _workspace.AddDocument(project.Id, sourceFile); + } + + // Removing any remaining documents from the project. + foreach (var currentDocument in currentDocuments) + { + _workspace.RemoveDocument(currentDocument.Value); + } + } + + private void OnDirectoryFileChanged(string path, FileChangeType changeType) + { + // Hosts may not have passed through a file change type + if (changeType == FileChangeType.Unspecified && !File.Exists(path) || changeType == FileChangeType.Delete) + { + foreach (var documentId in _workspace.CurrentSolution.GetDocumentIdsWithFilePath(path)) + { + _workspace.RemoveDocument(documentId); + } + } + + if (changeType == FileChangeType.Unspecified || changeType == FileChangeType.Create) + { + // Only add cs files. Also, make sure the path is a file, and not a directory name that happens to end in ".cs" + if (string.Equals(Path.GetExtension(path), ".cs", StringComparison.CurrentCultureIgnoreCase) && File.Exists(path)) + { + // Use the buffer manager to add the new file to the appropriate projects + // Hosts that don't pass the FileChangeType may wind up updating the buffer twice + _workspace.BufferManager.UpdateBufferAsync(new UpdateBufferRequest() { FileName = path, FromDisk = true }).Wait(); + } + } + } + + private void UpdateParseOptions(Project project, LanguageVersion languageVersion, IEnumerable preprocessorSymbolNames, bool generateXmlDocumentation) + { + var existingParseOptions = (CSharpParseOptions)project.ParseOptions; + + if (existingParseOptions.LanguageVersion == languageVersion && + Enumerable.SequenceEqual(existingParseOptions.PreprocessorSymbolNames, preprocessorSymbolNames) && + (existingParseOptions.DocumentationMode == DocumentationMode.Diagnose) == generateXmlDocumentation) + { + // No changes to make. Moving on. + return; + } + + var parseOptions = new CSharpParseOptions(languageVersion); + + if (preprocessorSymbolNames.Any()) + { + parseOptions = parseOptions.WithPreprocessorSymbols(preprocessorSymbolNames); + } + + if (generateXmlDocumentation) + { + parseOptions = parseOptions.WithDocumentationMode(DocumentationMode.Diagnose); + } + + _workspace.SetParseOptions(project.Id, parseOptions); + } + + private void UpdateProjectReferences(Project project, ImmutableArray projectReferencePaths) + { + _logger.LogInformation($"Update project: {project.Name}"); + + var existingProjectReferences = new HashSet(project.ProjectReferences); + var addedProjectReferences = new HashSet(); + + foreach (var projectReferencePath in projectReferencePaths) + { + if (!_projectFiles.TryGetValue(projectReferencePath, out var referencedProject)) + { + if (File.Exists(projectReferencePath)) + { + _logger.LogInformation($"Found referenced project outside root directory: {projectReferencePath}"); + + // We've found a project reference that we didn't know about already, but it exists on disk. + // This is likely a project that is outside of OmniSharp's TargetDirectory. + referencedProject = ProjectFileInfo.CreateNoBuild(projectReferencePath, _projectLoader); + AddProject(referencedProject); + + QueueProjectUpdate(projectReferencePath); + } + } + + if (referencedProject == null) + { + _logger.LogWarning($"Unable to resolve project reference '{projectReferencePath}' for '{project.Name}'."); + continue; + } + + var projectReference = new ProjectReference(referencedProject.Id); + + if (existingProjectReferences.Remove(projectReference)) + { + // This reference already exists + continue; + } + + if (!addedProjectReferences.Contains(projectReference)) + { + _workspace.AddProjectReference(project.Id, projectReference); + addedProjectReferences.Add(projectReference); + } + } + + foreach (var existingProjectReference in existingProjectReferences) + { + _workspace.RemoveProjectReference(project.Id, existingProjectReference); + } + } + + private class MetadataReferenceComparer : IEqualityComparer + { + public static MetadataReferenceComparer Instance { get; } = new MetadataReferenceComparer(); + + public bool Equals(MetadataReference x, MetadataReference y) + => x is PortableExecutableReference pe1 && y is PortableExecutableReference pe2 + ? StringComparer.OrdinalIgnoreCase.Equals(pe1.FilePath, pe2.FilePath) + : EqualityComparer.Default.Equals(x, y); + + public int GetHashCode(MetadataReference obj) + => obj is PortableExecutableReference pe + ? StringComparer.OrdinalIgnoreCase.GetHashCode(pe.FilePath) + : EqualityComparer.Default.GetHashCode(obj); + } + + private void UpdateReferences(Project project, ImmutableArray referencePaths) + { + var referencesToRemove = new HashSet(project.MetadataReferences, MetadataReferenceComparer.Instance); + var referencesToAdd = new HashSet(MetadataReferenceComparer.Instance); + + foreach (var referencePath in referencePaths) + { + if (!File.Exists(referencePath)) + { + _logger.LogWarning($"Unable to resolve assembly '{referencePath}'"); + } + else + { + var reference = _metadataFileReferenceCache.GetMetadataReference(referencePath); + + if (referencesToRemove.Remove(reference)) + { + continue; + } + + if (!referencesToAdd.Contains(reference)) + { + _logger.LogDebug($"Adding reference '{referencePath}' to '{project.Name}'."); + _workspace.AddMetadataReference(project.Id, reference); + referencesToAdd.Add(reference); + } + } + } + + foreach (var reference in referencesToRemove) + { + _workspace.RemoveMetadataReference(project.Id, reference); + } + } + } +} diff --git a/src/OmniSharp.MSBuild/ProjectSystem.cs b/src/OmniSharp.MSBuild/ProjectSystem.cs new file mode 100644 index 0000000000..28b85770cb --- /dev/null +++ b/src/OmniSharp.MSBuild/ProjectSystem.cs @@ -0,0 +1,202 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; +using System.IO; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using OmniSharp.Eventing; +using OmniSharp.FileWatching; +using OmniSharp.Models.WorkspaceInformation; +using OmniSharp.MSBuild.Discovery; +using OmniSharp.MSBuild.Models; +using OmniSharp.MSBuild.ProjectFile; +using OmniSharp.MSBuild.SolutionParsing; +using OmniSharp.Options; +using OmniSharp.Services; + +namespace OmniSharp.MSBuild +{ + [Export(typeof(IProjectSystem)), Shared] + public class ProjectSystem : IProjectSystem + { + private readonly IOmniSharpEnvironment _environment; + private readonly OmniSharpWorkspace _workspace; + private readonly ImmutableDictionary _propertyOverrides; + private readonly DotNetCliService _dotNetCli; + private readonly MetadataFileReferenceCache _metadataFileReferenceCache; + private readonly IEventEmitter _eventEmitter; + private readonly IFileSystemWatcher _fileSystemWatcher; + private readonly ILoggerFactory _loggerFactory; + private readonly ILogger _logger; + + private readonly object _gate = new object(); + private readonly Queue _projectsToProcess; + + private PackageDependencyChecker _packageDependencyChecker; + private ProjectManager _manager; + private ProjectLoader _loader; + private MSBuildOptions _options; + private string _solutionFileOrRootPath; + + public string Key { get; } = "MsBuild"; + public string Language { get; } = LanguageNames.CSharp; + public IEnumerable Extensions { get; } = new[] { ".cs" }; + + [ImportingConstructor] + public ProjectSystem( + IOmniSharpEnvironment environment, + OmniSharpWorkspace workspace, + IMSBuildLocator msbuildLocator, + DotNetCliService dotNetCliService, + MetadataFileReferenceCache metadataFileReferenceCache, + IEventEmitter eventEmitter, + IFileSystemWatcher fileSystemWatcher, + ILoggerFactory loggerFactory) + { + _environment = environment; + _workspace = workspace; + _propertyOverrides = msbuildLocator.RegisteredInstance.PropertyOverrides; + _dotNetCli = dotNetCliService; + _metadataFileReferenceCache = metadataFileReferenceCache; + _eventEmitter = eventEmitter; + _fileSystemWatcher = fileSystemWatcher; + _loggerFactory = loggerFactory; + + _projectsToProcess = new Queue(); + _logger = loggerFactory.CreateLogger(); + } + + public void Initalize(IConfiguration configuration) + { + _options = new MSBuildOptions(); + ConfigurationBinder.Bind(configuration, _options); + + if (_environment.LogLevel < LogLevel.Information) + { + var buildEnvironmentInfo = MSBuildHelpers.GetBuildEnvironmentInfo(); + _logger.LogDebug($"MSBuild environment: {Environment.NewLine}{buildEnvironmentInfo}"); + } + + _packageDependencyChecker = new PackageDependencyChecker(_loggerFactory, _eventEmitter, _dotNetCli, _options); + _loader = new ProjectLoader(_options, _environment.TargetDirectory, _propertyOverrides, _loggerFactory); + _manager = new ProjectManager(_loggerFactory, _eventEmitter, _fileSystemWatcher, _metadataFileReferenceCache, _packageDependencyChecker, _loader, _workspace); + + var initialProjectPaths = GetInitialProjectPaths(); + + foreach (var projectFilePath in initialProjectPaths) + { + if (!File.Exists(projectFilePath)) + { + _logger.LogWarning($"Found project that doesn't exist on disk: {projectFilePath}"); + continue; + } + + _manager.QueueProjectUpdate(projectFilePath); + } + } + + private IEnumerable GetInitialProjectPaths() + { + // If a solution was provided, use it. + if (!string.IsNullOrEmpty(_environment.SolutionFilePath)) + { + _solutionFileOrRootPath = _environment.SolutionFilePath; + return GetProjectPathsFromSolution(_environment.SolutionFilePath); + } + + // Otherwise, assume that the path provided is a directory and look for a solution there. + var solutionFilePath = FindSolutionFilePath(_environment.TargetDirectory, _logger); + if (!string.IsNullOrEmpty(solutionFilePath)) + { + _solutionFileOrRootPath = solutionFilePath; + return GetProjectPathsFromSolution(solutionFilePath); + } + + // Finally, if there isn't a single solution immediately available, + // Just process all of the projects beneath the root path. + _solutionFileOrRootPath = _environment.TargetDirectory; + return Directory.GetFiles(_environment.TargetDirectory, "*.csproj", SearchOption.AllDirectories); + } + + private IEnumerable GetProjectPathsFromSolution(string solutionFilePath) + { + _logger.LogInformation($"Detecting projects in '{solutionFilePath}'."); + + var solutionFile = SolutionFile.ParseFile(solutionFilePath); + var processedProjects = new HashSet(StringComparer.OrdinalIgnoreCase); + var result = new List(); + + foreach (var project in solutionFile.Projects) + { + if (project.IsSolutionFolder) + { + continue; + } + + // Solution files are assumed to contain relative paths to project files with Windows-style slashes. + var projectFilePath = project.RelativePath.Replace('\\', Path.DirectorySeparatorChar); + projectFilePath = Path.Combine(_environment.TargetDirectory, projectFilePath); + projectFilePath = Path.GetFullPath(projectFilePath); + + // Have we seen this project? If so, move on. + if (processedProjects.Contains(projectFilePath)) + { + continue; + } + + if (string.Equals(Path.GetExtension(projectFilePath), ".csproj", StringComparison.OrdinalIgnoreCase)) + { + result.Add(projectFilePath); + } + + processedProjects.Add(projectFilePath); + } + + return result; + } + + private static string FindSolutionFilePath(string rootPath, ILogger logger) + { + var solutionsFilePaths = Directory.GetFiles(rootPath, "*.sln"); + var result = SolutionSelector.Pick(solutionsFilePaths, rootPath); + + if (result.Message != null) + { + logger.LogInformation(result.Message); + } + + return result.FilePath; + } + + async Task IProjectSystem.GetWorkspaceModelAsync(WorkspaceInformationRequest request) + { + await _manager.WaitForQueueEmptyAsync(); + + return new MSBuildWorkspaceInfo( + _solutionFileOrRootPath, _manager.GetAllProjects(), + excludeSourceFiles: request?.ExcludeSourceFiles ?? false); + } + + async Task IProjectSystem.GetProjectModelAsync(string filePath) + { + await _manager.WaitForQueueEmptyAsync(); + + var document = _workspace.GetDocument(filePath); + + var projectFilePath = document != null + ? document.Project.FilePath + : filePath; + + if (!_manager.TryGetProject(projectFilePath, out var projectFileInfo)) + { + _logger.LogDebug($"Could not locate project for '{projectFilePath}'"); + return Task.FromResult(null); + } + + return new MSBuildProjectInfo(projectFileInfo); + } + } +} diff --git a/tests/OmniSharp.MSBuild.Tests/MSBuildProjectSystemTests.cs b/tests/OmniSharp.MSBuild.Tests/MSBuildProjectSystemTests.cs index 523390886c..b2326929af 100644 --- a/tests/OmniSharp.MSBuild.Tests/MSBuildProjectSystemTests.cs +++ b/tests/OmniSharp.MSBuild.Tests/MSBuildProjectSystemTests.cs @@ -11,13 +11,11 @@ public void Project_path_is_case_insensitive() var projectPath = @"c:\projects\project1\project.csproj"; var searchProjectPath = @"c:\Projects\Project1\Project.csproj"; - var collection = new ProjectFileInfoCollection - { - new ProjectFileInfo(projectPath) - }; + var collection = new ProjectFileInfoCollection(); + collection.Add(ProjectFileInfo.CreateEmpty(projectPath)); Assert.True(collection.TryGetValue(searchProjectPath, out var outInfo)); Assert.NotNull(outInfo); } } -} \ No newline at end of file +} diff --git a/tests/OmniSharp.MSBuild.Tests/ProjectFileInfoTests.cs b/tests/OmniSharp.MSBuild.Tests/ProjectFileInfoTests.cs index 3e991bbf74..cb0710e77b 100644 --- a/tests/OmniSharp.MSBuild.Tests/ProjectFileInfoTests.cs +++ b/tests/OmniSharp.MSBuild.Tests/ProjectFileInfoTests.cs @@ -29,7 +29,7 @@ private ProjectFileInfo CreateProjectFileInfo(OmniSharpTestHost host, ITestProje propertyOverrides: msbuildLocator.RegisteredInstance.PropertyOverrides, loggerFactory: LoggerFactory); - var (projectFileInfo, _) = ProjectFileInfo.Create(projectFilePath, loader); + var (projectFileInfo, _) = ProjectFileInfo.Load(projectFilePath, loader); return projectFileInfo; } diff --git a/tests/TestUtility/OmniSharpTestHost.cs b/tests/TestUtility/OmniSharpTestHost.cs index 582a83091b..e2a17a8d89 100644 --- a/tests/TestUtility/OmniSharpTestHost.cs +++ b/tests/TestUtility/OmniSharpTestHost.cs @@ -13,6 +13,7 @@ using OmniSharp.DotNetTest.Models; using OmniSharp.Eventing; using OmniSharp.Mef; +using OmniSharp.Models.WorkspaceInformation; using OmniSharp.MSBuild; using OmniSharp.Roslyn.CSharp.Services; using OmniSharp.Services; @@ -32,7 +33,7 @@ public class OmniSharpTestHost : DisposableObject typeof(HostHelpers).GetTypeInfo().Assembly, // OmniSharp.Host typeof(DotNetProjectSystem).GetTypeInfo().Assembly, // OmniSharp.DotNet typeof(RunTestRequest).GetTypeInfo().Assembly, // OmniSharp.DotNetTest - typeof(MSBuildProjectSystem).GetTypeInfo().Assembly, // OmniSharp.MSBuild + typeof(ProjectSystem).GetTypeInfo().Assembly, // OmniSharp.MSBuild typeof(OmniSharpWorkspace).GetTypeInfo().Assembly, // OmniSharp.Roslyn typeof(RoslynFeaturesHostServicesProvider).GetTypeInfo().Assembly, // OmniSharp.Roslyn.CSharp typeof(CakeProjectSystem).GetTypeInfo().Assembly, // OmniSharp.Cake @@ -133,7 +134,13 @@ public static OmniSharpTestHost Create(string path = null, ITestOutputHelper tes WorkspaceInitializer.Initialize(serviceProvider, compositionHost, configuration, logger); - return new OmniSharpTestHost(serviceProvider, loggerFactory, workspace, compositionHost, oldMSBuildSdksPath); + var host = new OmniSharpTestHost(serviceProvider, loggerFactory, workspace, compositionHost, oldMSBuildSdksPath); + + // Force workspace to be updated + var service = host.GetWorkspaceInformationService(); + service.Handle(new WorkspaceInformationRequest()).Wait(); + + return host; } private static string SetMSBuildSdksPath(DotNetCliService dotNetCli) From 18a88549f6ca61e9615c1bd58c5d1304c57dafde Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 3 Nov 2017 09:46:44 -0700 Subject: [PATCH 07/10] Thread 'auto restore' through MSBuild project manager Previously, we tracked whether a project update *could* invoke an automatic 'dotnet restore' and this PR adds that back. Essentially, we allow auto restore in all cases that a project is updated *except* when the update comes via a restore. --- src/OmniSharp.MSBuild/ProjectManager.cs | 71 ++++++++++++++++--------- src/OmniSharp.MSBuild/ProjectSystem.cs | 2 +- 2 files changed, 47 insertions(+), 26 deletions(-) diff --git a/src/OmniSharp.MSBuild/ProjectManager.cs b/src/OmniSharp.MSBuild/ProjectManager.cs index 766a42729e..bf4136a558 100644 --- a/src/OmniSharp.MSBuild/ProjectManager.cs +++ b/src/OmniSharp.MSBuild/ProjectManager.cs @@ -22,6 +22,18 @@ namespace OmniSharp.MSBuild { internal class ProjectManager : DisposableObject { + private class ProjectToUpdate + { + public string FilePath { get; } + public bool AllowAutoRestore { get; set; } + + public ProjectToUpdate(string filePath, bool allowAutoRestore) + { + FilePath = filePath ?? throw new ArgumentNullException(nameof(filePath)); + AllowAutoRestore = allowAutoRestore; + } + } + private readonly ILogger _logger; private readonly IEventEmitter _eventEmitter; private readonly IFileSystemWatcher _fileSystemWatcher; @@ -32,7 +44,7 @@ internal class ProjectManager : DisposableObject private readonly OmniSharpWorkspace _workspace; private const int LoopDelay = 100; // milliseconds - private readonly BufferBlock _queue; + private readonly BufferBlock _queue; private readonly CancellationTokenSource _processLoopCancellation; private readonly Task _processLoopTask; private bool _processingQueue; @@ -48,7 +60,7 @@ public ProjectManager(ILoggerFactory loggerFactory, IEventEmitter eventEmitter, _projectLoader = projectLoader; _workspace = workspace; - _queue = new BufferBlock(); + _queue = new BufferBlock(); _processLoopCancellation = new CancellationTokenSource(); _processLoopTask = Task.Run(() => ProcessLoopAsync(_processLoopCancellation.Token)); } @@ -67,10 +79,10 @@ protected override void DisposeCore(bool disposing) public IEnumerable GetAllProjects() => _projectFiles.GetItems(); public bool TryGetProject(string projectFilePath, out ProjectFileInfo projectFileInfo) => _projectFiles.TryGetValue(projectFilePath, out projectFileInfo); - public void QueueProjectUpdate(string projectFilePath) + public void QueueProjectUpdate(string projectFilePath, bool allowAutoRestore) { _logger.LogInformation($"Queue project update for '{projectFilePath}'"); - _queue.Post(projectFilePath); + _queue.Post(new ProjectToUpdate(projectFilePath, allowAutoRestore)); } public async Task WaitForQueueEmptyAsync() @@ -95,53 +107,62 @@ private void ProcessQueue(CancellationToken cancellationToken) _processingQueue = true; try { - HashSet processedSet = null; + Dictionary projectByFilePathMap = null; + List projectList = null; - while (_queue.TryReceive(out var projectFilePath)) + while (_queue.TryReceive(out var currentProject)) { if (cancellationToken.IsCancellationRequested) { break; } - if (processedSet == null) + if (projectByFilePathMap == null) { - processedSet = new HashSet(StringComparer.OrdinalIgnoreCase); + projectByFilePathMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + projectList = new List(); } - // Ensure that we don't process the same project twice. - if (!processedSet.Add(projectFilePath)) + // Ensure that we don't process the same project twice. However, if a project *does* + // appear more than once in the update queue, ensure that AllowAutoRestore is set to true + // if any of the updates requires it. + if (projectByFilePathMap.TryGetValue(currentProject.FilePath, out var trackedProject)) { + if (currentProject.AllowAutoRestore && !trackedProject.AllowAutoRestore) + { + trackedProject.AllowAutoRestore = true; + } + continue; } // TODO: Handle removing project // update or add project - if (_projectFiles.TryGetValue(projectFilePath, out var projectFileInfo)) + if (_projectFiles.TryGetValue(currentProject.FilePath, out var projectFileInfo)) { projectFileInfo = ReloadProject(projectFileInfo); - _projectFiles[projectFilePath] = projectFileInfo; + _projectFiles[currentProject.FilePath] = projectFileInfo; } else { - projectFileInfo = LoadProject(projectFilePath); + projectFileInfo = LoadProject(currentProject.FilePath); AddProject(projectFileInfo); } } - if (processedSet != null) + if (projectByFilePathMap != null) { - foreach (var projectFilePath in processedSet) + foreach (var project in projectList) { - UpdateProject(projectFilePath); + UpdateProject(project.FilePath); } - foreach (var projectFilePath in processedSet) + foreach (var project in projectList) { - if (_projectFiles.TryGetValue(projectFilePath, out var projectFileInfo)) + if (_projectFiles.TryGetValue(project.FilePath, out var projectFileInfo)) { - _packageDependencyChecker.CheckForUnresolvedDependences(projectFileInfo, allowAutoRestore: true); + _packageDependencyChecker.CheckForUnresolvedDependences(projectFileInfo, project.AllowAutoRestore); } } } @@ -230,14 +251,14 @@ private void WatchProjectFiles(ProjectFileInfo projectFileInfo) // as "updates". We should properly remove projects that are deleted. _fileSystemWatcher.Watch(projectFileInfo.FilePath, (file, changeType) => { - QueueProjectUpdate(projectFileInfo.FilePath); + QueueProjectUpdate(projectFileInfo.FilePath, allowAutoRestore: true); }); if (!string.IsNullOrEmpty(projectFileInfo.ProjectAssetsFile)) { _fileSystemWatcher.Watch(projectFileInfo.ProjectAssetsFile, (file, changeType) => { - QueueProjectUpdate(projectFileInfo.FilePath); + QueueProjectUpdate(projectFileInfo.FilePath, allowAutoRestore: false); }); var restoreDirectory = Path.GetDirectoryName(projectFileInfo.ProjectAssetsFile); @@ -248,17 +269,17 @@ private void WatchProjectFiles(ProjectFileInfo projectFileInfo) _fileSystemWatcher.Watch(nugetCacheFile, (file, changeType) => { - QueueProjectUpdate(projectFileInfo.FilePath); + QueueProjectUpdate(projectFileInfo.FilePath, allowAutoRestore: false); }); _fileSystemWatcher.Watch(nugetPropsFile, (file, changeType) => { - QueueProjectUpdate(projectFileInfo.FilePath); + QueueProjectUpdate(projectFileInfo.FilePath, allowAutoRestore: false); }); _fileSystemWatcher.Watch(nugetTargetsFile, (file, changeType) => { - QueueProjectUpdate(projectFileInfo.FilePath); + QueueProjectUpdate(projectFileInfo.FilePath, allowAutoRestore: false); }); } } @@ -385,7 +406,7 @@ private void UpdateProjectReferences(Project project, ImmutableArray pro referencedProject = ProjectFileInfo.CreateNoBuild(projectReferencePath, _projectLoader); AddProject(referencedProject); - QueueProjectUpdate(projectReferencePath); + QueueProjectUpdate(projectReferencePath, allowAutoRestore: true); } } diff --git a/src/OmniSharp.MSBuild/ProjectSystem.cs b/src/OmniSharp.MSBuild/ProjectSystem.cs index 28b85770cb..1c4f2d648c 100644 --- a/src/OmniSharp.MSBuild/ProjectSystem.cs +++ b/src/OmniSharp.MSBuild/ProjectSystem.cs @@ -94,7 +94,7 @@ public void Initalize(IConfiguration configuration) continue; } - _manager.QueueProjectUpdate(projectFilePath); + _manager.QueueProjectUpdate(projectFilePath, allowAutoRestore: true); } } From e79151c8ba50ec2ff974869305bc5411a276fb5a Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 3 Nov 2017 09:50:57 -0700 Subject: [PATCH 08/10] Move MetdataReferenceEqualityComparer to OmniSharp.Roslyn/Utilities --- src/OmniSharp.MSBuild/ProjectManager.cs | 20 +++--------------- .../MetadataReferenceEqualityComparer.cs | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+), 17 deletions(-) create mode 100644 src/OmniSharp.Roslyn/Utilities/MetadataReferenceEqualityComparer.cs diff --git a/src/OmniSharp.MSBuild/ProjectManager.cs b/src/OmniSharp.MSBuild/ProjectManager.cs index bf4136a558..dee8a78995 100644 --- a/src/OmniSharp.MSBuild/ProjectManager.cs +++ b/src/OmniSharp.MSBuild/ProjectManager.cs @@ -15,6 +15,7 @@ using OmniSharp.MSBuild.Logging; using OmniSharp.MSBuild.Models.Events; using OmniSharp.MSBuild.ProjectFile; +using OmniSharp.Roslyn.Utilities; using OmniSharp.Services; using OmniSharp.Utilities; @@ -437,25 +438,10 @@ private void UpdateProjectReferences(Project project, ImmutableArray pro } } - private class MetadataReferenceComparer : IEqualityComparer - { - public static MetadataReferenceComparer Instance { get; } = new MetadataReferenceComparer(); - - public bool Equals(MetadataReference x, MetadataReference y) - => x is PortableExecutableReference pe1 && y is PortableExecutableReference pe2 - ? StringComparer.OrdinalIgnoreCase.Equals(pe1.FilePath, pe2.FilePath) - : EqualityComparer.Default.Equals(x, y); - - public int GetHashCode(MetadataReference obj) - => obj is PortableExecutableReference pe - ? StringComparer.OrdinalIgnoreCase.GetHashCode(pe.FilePath) - : EqualityComparer.Default.GetHashCode(obj); - } - private void UpdateReferences(Project project, ImmutableArray referencePaths) { - var referencesToRemove = new HashSet(project.MetadataReferences, MetadataReferenceComparer.Instance); - var referencesToAdd = new HashSet(MetadataReferenceComparer.Instance); + var referencesToRemove = new HashSet(project.MetadataReferences, MetadataReferenceEqualityComparer.Instance); + var referencesToAdd = new HashSet(MetadataReferenceEqualityComparer.Instance); foreach (var referencePath in referencePaths) { diff --git a/src/OmniSharp.Roslyn/Utilities/MetadataReferenceEqualityComparer.cs b/src/OmniSharp.Roslyn/Utilities/MetadataReferenceEqualityComparer.cs new file mode 100644 index 0000000000..93539abdd7 --- /dev/null +++ b/src/OmniSharp.Roslyn/Utilities/MetadataReferenceEqualityComparer.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using Microsoft.CodeAnalysis; + +namespace OmniSharp.Roslyn.Utilities +{ + public class MetadataReferenceEqualityComparer : IEqualityComparer + { + public static MetadataReferenceEqualityComparer Instance { get; } = new MetadataReferenceEqualityComparer(); + + public bool Equals(MetadataReference x, MetadataReference y) + => x is PortableExecutableReference pe1 && y is PortableExecutableReference pe2 + ? StringComparer.OrdinalIgnoreCase.Equals(pe1.FilePath, pe2.FilePath) + : EqualityComparer.Default.Equals(x, y); + + public int GetHashCode(MetadataReference obj) + => obj is PortableExecutableReference pe + ? StringComparer.OrdinalIgnoreCase.GetHashCode(pe.FilePath) + : EqualityComparer.Default.GetHashCode(obj); + } +} From b4f8071eb9338b8d79f822e78d57ac443ef7359e Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Fri, 3 Nov 2017 10:27:54 -0700 Subject: [PATCH 09/10] Fix bug introduced in 18a88549f6ca61e9615c1bd58c5d1304c57dafde --- src/OmniSharp.MSBuild/ProjectManager.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/OmniSharp.MSBuild/ProjectManager.cs b/src/OmniSharp.MSBuild/ProjectManager.cs index dee8a78995..f4da63eaa0 100644 --- a/src/OmniSharp.MSBuild/ProjectManager.cs +++ b/src/OmniSharp.MSBuild/ProjectManager.cs @@ -139,6 +139,9 @@ private void ProcessQueue(CancellationToken cancellationToken) // TODO: Handle removing project + projectByFilePathMap.Add(currentProject.FilePath, currentProject); + projectList.Add(currentProject); + // update or add project if (_projectFiles.TryGetValue(currentProject.FilePath, out var projectFileInfo)) { From 7315995791b7d10890d5bf662d8bdc724c64e4f6 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Sat, 4 Nov 2017 10:29:34 -0700 Subject: [PATCH 10/10] Port 6d8813ffe608e974e90d7d93b0262bb18e4d624f to fix C# projects referencing F# projects --- build.json | 3 +- .../ProjectFile/MetadataNames.cs | 1 + .../ProjectFileInfo.ProjectData.cs | 43 ++++++++++++++--- src/OmniSharp.MSBuild/ProjectManager.cs | 3 +- .../CSharpAndFSharp/CSharpAndFSharp.sln | 48 +++++++++++++++++++ .../CSharpAndFSharp/csharp-console/Program.cs | 12 +++++ .../csharp-console/csharp-console.csproj | 12 +++++ .../CSharpAndFSharp/fsharp-lib/Library.fs | 5 ++ .../fsharp-lib/fsharp-lib.fsproj | 11 +++++ .../WorkspaceInformationTests.cs | 17 +++++++ 10 files changed, 146 insertions(+), 9 deletions(-) create mode 100644 test-assets/test-projects/CSharpAndFSharp/CSharpAndFSharp.sln create mode 100644 test-assets/test-projects/CSharpAndFSharp/csharp-console/Program.cs create mode 100644 test-assets/test-projects/CSharpAndFSharp/csharp-console/csharp-console.csproj create mode 100644 test-assets/test-projects/CSharpAndFSharp/fsharp-lib/Library.fs create mode 100644 test-assets/test-projects/CSharpAndFSharp/fsharp-lib/fsharp-lib.fsproj diff --git a/build.json b/build.json index 0fcd698eb2..a1d6ee9d54 100644 --- a/build.json +++ b/build.json @@ -35,7 +35,8 @@ "ProjectAndSolution", "ProjectAndSolutionWithProjectSection", "TwoProjectsWithSolution", - "ProjectWithGeneratedFile" + "ProjectWithGeneratedFile", + "CSharpAndFSharp" ], "LegacyTestAssets": [ "LegacyNUnitTestProject", diff --git a/src/OmniSharp.MSBuild/ProjectFile/MetadataNames.cs b/src/OmniSharp.MSBuild/ProjectFile/MetadataNames.cs index a81bb110c4..74c5835fd8 100644 --- a/src/OmniSharp.MSBuild/ProjectFile/MetadataNames.cs +++ b/src/OmniSharp.MSBuild/ProjectFile/MetadataNames.cs @@ -5,6 +5,7 @@ internal static class MetadataNames public const string FullPath = nameof(FullPath); public const string IsImplicitlyDefined = nameof(IsImplicitlyDefined); public const string Project = nameof(Project); + public const string OriginalItemSpec = nameof(OriginalItemSpec); public const string ReferenceSourceTarget = nameof(ReferenceSourceTarget); public const string Version = nameof(Version); } diff --git a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.ProjectData.cs b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.ProjectData.cs index 7117155e99..b07b2f3675 100644 --- a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.ProjectData.cs +++ b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.ProjectData.cs @@ -172,9 +172,38 @@ public static ProjectData Create(MSB.Execution.ProjectInstance projectInstance) var sourceFiles = GetFullPaths( projectInstance.GetItems(ItemNames.Compile), filter: FileNameIsNotGenerated); - var projectReferences = GetFullPaths(projectInstance.GetItems(ItemNames.ProjectReference)); - var references = GetFullPaths( - projectInstance.GetItems(ItemNames.ReferencePath).Where(ReferenceSourceTargetIsNotProjectReference)); + + var projectReferences = GetFullPaths( + projectInstance.GetItems(ItemNames.ProjectReference), filter: IsCSharpProject); + + var references = ImmutableArray.CreateBuilder(); + foreach (var referencePathItem in projectInstance.GetItems(ItemNames.ReferencePath)) + { + var referenceSourceTarget = referencePathItem.GetMetadataValue(MetadataNames.ReferenceSourceTarget); + + if (StringComparer.OrdinalIgnoreCase.Equals(referenceSourceTarget, ItemNames.ProjectReference)) + { + // If the reference was sourced from a project reference, we have two choices: + // + // 1. If the reference is a C# project reference, we shouldn't add it because it'll just duplicate + // the project reference. + // 2. If the reference is *not* a C# project reference, we should keep this reference because the + // project reference was already removed. + + var originalItemSpec = referencePathItem.GetMetadataValue(MetadataNames.OriginalItemSpec); + if (originalItemSpec.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + } + + var fullPath = referencePathItem.GetMetadataValue(MetadataNames.FullPath); + if (!string.IsNullOrEmpty(fullPath)) + { + references.Add(fullPath); + } + } + var packageReferences = GetPackageReferences(projectInstance.GetItems(ItemNames.PackageReference)); var analyzers = GetFullPaths(projectInstance.GetItems(ItemNames.Analyzer)); @@ -183,14 +212,14 @@ public static ProjectData Create(MSB.Execution.ProjectInstance projectInstance) targetFramework, targetFrameworks, outputKind, languageVersion, allowUnsafeCode, documentationFile, preprocessorSymbolNames, suppressedDiagnosticIds, signAssembly, assemblyOriginatorKeyFile, - sourceFiles, projectReferences, references, packageReferences, analyzers); + sourceFiles, projectReferences, references.ToImmutable(), packageReferences, analyzers); } - private static bool ReferenceSourceTargetIsNotProjectReference(MSB.Execution.ProjectItemInstance item) - => item.GetMetadataValue(MetadataNames.ReferenceSourceTarget) != ItemNames.ProjectReference; + private static bool IsCSharpProject(string filePath) + => filePath.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase); private static bool FileNameIsNotGenerated(string filePath) - => !Path.GetFileName(filePath).StartsWith("TemporaryGeneratedFile_"); + => !Path.GetFileName(filePath).StartsWith("TemporaryGeneratedFile_", StringComparison.OrdinalIgnoreCase); private static ImmutableArray GetFullPaths(IEnumerable items, Func filter = null) { diff --git a/src/OmniSharp.MSBuild/ProjectManager.cs b/src/OmniSharp.MSBuild/ProjectManager.cs index f4da63eaa0..6996da4efa 100644 --- a/src/OmniSharp.MSBuild/ProjectManager.cs +++ b/src/OmniSharp.MSBuild/ProjectManager.cs @@ -401,7 +401,8 @@ private void UpdateProjectReferences(Project project, ImmutableArray pro { if (!_projectFiles.TryGetValue(projectReferencePath, out var referencedProject)) { - if (File.Exists(projectReferencePath)) + if (File.Exists(projectReferencePath) && + projectReferencePath.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase)) { _logger.LogInformation($"Found referenced project outside root directory: {projectReferencePath}"); diff --git a/test-assets/test-projects/CSharpAndFSharp/CSharpAndFSharp.sln b/test-assets/test-projects/CSharpAndFSharp/CSharpAndFSharp.sln new file mode 100644 index 0000000000..d4cf949602 --- /dev/null +++ b/test-assets/test-projects/CSharpAndFSharp/CSharpAndFSharp.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "csharp-console", "csharp-console\csharp-console.csproj", "{ED247A90-AFBE-4717-8F32-AA8BFC2C8627}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "fsharp-lib", "fsharp-lib\fsharp-lib.fsproj", "{4FCFD6A3-2860-42C4-B98E-ADAEC268B928}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {ED247A90-AFBE-4717-8F32-AA8BFC2C8627}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ED247A90-AFBE-4717-8F32-AA8BFC2C8627}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ED247A90-AFBE-4717-8F32-AA8BFC2C8627}.Debug|x64.ActiveCfg = Debug|x64 + {ED247A90-AFBE-4717-8F32-AA8BFC2C8627}.Debug|x64.Build.0 = Debug|x64 + {ED247A90-AFBE-4717-8F32-AA8BFC2C8627}.Debug|x86.ActiveCfg = Debug|x86 + {ED247A90-AFBE-4717-8F32-AA8BFC2C8627}.Debug|x86.Build.0 = Debug|x86 + {ED247A90-AFBE-4717-8F32-AA8BFC2C8627}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ED247A90-AFBE-4717-8F32-AA8BFC2C8627}.Release|Any CPU.Build.0 = Release|Any CPU + {ED247A90-AFBE-4717-8F32-AA8BFC2C8627}.Release|x64.ActiveCfg = Release|x64 + {ED247A90-AFBE-4717-8F32-AA8BFC2C8627}.Release|x64.Build.0 = Release|x64 + {ED247A90-AFBE-4717-8F32-AA8BFC2C8627}.Release|x86.ActiveCfg = Release|x86 + {ED247A90-AFBE-4717-8F32-AA8BFC2C8627}.Release|x86.Build.0 = Release|x86 + {4FCFD6A3-2860-42C4-B98E-ADAEC268B928}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4FCFD6A3-2860-42C4-B98E-ADAEC268B928}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4FCFD6A3-2860-42C4-B98E-ADAEC268B928}.Debug|x64.ActiveCfg = Debug|x64 + {4FCFD6A3-2860-42C4-B98E-ADAEC268B928}.Debug|x64.Build.0 = Debug|x64 + {4FCFD6A3-2860-42C4-B98E-ADAEC268B928}.Debug|x86.ActiveCfg = Debug|x86 + {4FCFD6A3-2860-42C4-B98E-ADAEC268B928}.Debug|x86.Build.0 = Debug|x86 + {4FCFD6A3-2860-42C4-B98E-ADAEC268B928}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4FCFD6A3-2860-42C4-B98E-ADAEC268B928}.Release|Any CPU.Build.0 = Release|Any CPU + {4FCFD6A3-2860-42C4-B98E-ADAEC268B928}.Release|x64.ActiveCfg = Release|x64 + {4FCFD6A3-2860-42C4-B98E-ADAEC268B928}.Release|x64.Build.0 = Release|x64 + {4FCFD6A3-2860-42C4-B98E-ADAEC268B928}.Release|x86.ActiveCfg = Release|x86 + {4FCFD6A3-2860-42C4-B98E-ADAEC268B928}.Release|x86.Build.0 = Release|x86 + EndGlobalSection +EndGlobal diff --git a/test-assets/test-projects/CSharpAndFSharp/csharp-console/Program.cs b/test-assets/test-projects/CSharpAndFSharp/csharp-console/Program.cs new file mode 100644 index 0000000000..1ebeafa81a --- /dev/null +++ b/test-assets/test-projects/CSharpAndFSharp/csharp-console/Program.cs @@ -0,0 +1,12 @@ +using System; + +namespace csharp_console +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + } + } +} diff --git a/test-assets/test-projects/CSharpAndFSharp/csharp-console/csharp-console.csproj b/test-assets/test-projects/CSharpAndFSharp/csharp-console/csharp-console.csproj new file mode 100644 index 0000000000..af64f07386 --- /dev/null +++ b/test-assets/test-projects/CSharpAndFSharp/csharp-console/csharp-console.csproj @@ -0,0 +1,12 @@ + + + + + + + + Exe + netcoreapp2.0 + + + diff --git a/test-assets/test-projects/CSharpAndFSharp/fsharp-lib/Library.fs b/test-assets/test-projects/CSharpAndFSharp/fsharp-lib/Library.fs new file mode 100644 index 0000000000..f5fa3f0d9f --- /dev/null +++ b/test-assets/test-projects/CSharpAndFSharp/fsharp-lib/Library.fs @@ -0,0 +1,5 @@ +namespace lib + +module Say = + let hello name = + printfn "Hello %s" name diff --git a/test-assets/test-projects/CSharpAndFSharp/fsharp-lib/fsharp-lib.fsproj b/test-assets/test-projects/CSharpAndFSharp/fsharp-lib/fsharp-lib.fsproj new file mode 100644 index 0000000000..df4543225f --- /dev/null +++ b/test-assets/test-projects/CSharpAndFSharp/fsharp-lib/fsharp-lib.fsproj @@ -0,0 +1,11 @@ + + + + netstandard2.0 + + + + + + + diff --git a/tests/OmniSharp.MSBuild.Tests/WorkspaceInformationTests.cs b/tests/OmniSharp.MSBuild.Tests/WorkspaceInformationTests.cs index 0be40d54ac..8b906655cb 100644 --- a/tests/OmniSharp.MSBuild.Tests/WorkspaceInformationTests.cs +++ b/tests/OmniSharp.MSBuild.Tests/WorkspaceInformationTests.cs @@ -105,6 +105,23 @@ public async Task ProjectWithSdkProperty() } } + [Fact] + public async Task CSharpAndFSharp() + { + using (var testProject = await TestAssets.Instance.GetTestProjectAsync("CSharpAndFSharp")) + using (var host = CreateOmniSharpHost(testProject.Directory)) + { + var workspaceInfo = await GetWorkspaceInfoAsync(host); + + Assert.NotNull(workspaceInfo.Projects); + Assert.Equal(1, workspaceInfo.Projects.Count); + + var project = workspaceInfo.Projects[0]; + Assert.Equal("csharp-console.csproj", Path.GetFileName(project.Path)); + Assert.Equal(3, project.SourceFiles.Count); + } + } + [ConditionalFact(typeof(WindowsOnly))] public async Task AntlrGeneratedFiles() {