diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a634ebe77..6f978a0b94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,9 @@ All changes to the project will be documented in this file. ## [1.35.1] - not yet released * Fixed not supported exception when trying to decompile a BCL assembly on Mono. For now we do not try to resolve implementation assembly from a ref assembly (PR: [#1767](https://github.com/OmniSharp/omnisharp-roslyn/pull/1767)) -* Added support for generic classes in test runner ([omnisharp-vscode#3722](https://github.com/OmniSharp/omnisharp-vscode/issues/3722), PR: [#1768](https://github.com/OmniSharp/omnisharp-roslyn/pull/1768)) +* Added support for generic classes in test runner ([omnisharp-vscode#3722](https://github.com/OmniSharp/omnisharp-vscode/issues/3722), PR: [#1768](https://github.com/OmniSharp/omnisharp-roslyn/pull/1768)) * Improved autocompletion performance (PR: [#1761](https://github.com/OmniSharp/omnisharp-roslyn/pull/1761)) +* Move to Roslyn's .editorconfig support ([#1657](https://github.com/OmniSharp/omnisharp-roslyn/issues/1657), PR: [#1771](https://github.com/OmniSharp/omnisharp-roslyn/pull/1771)) ## [1.35.0] - 2020-04-10 * Support for `` and `` (PR: [#1739](https://github.com/OmniSharp/omnisharp-roslyn/pull/1739)) diff --git a/build/Packages.props b/build/Packages.props index ebb70cb9a8..8c1bbcf8f1 100644 --- a/build/Packages.props +++ b/build/Packages.props @@ -52,7 +52,6 @@ - diff --git a/src/OmniSharp.Cake/CakeProjectSystem.cs b/src/OmniSharp.Cake/CakeProjectSystem.cs index 667e21eeb1..d547d77e41 100644 --- a/src/OmniSharp.Cake/CakeProjectSystem.cs +++ b/src/OmniSharp.Cake/CakeProjectSystem.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.Immutable; using System.Composition; using System.IO; using System.Linq; using System.Reflection; +using System.Text; using System.Threading.Tasks; using Cake.Scripting.Abstractions.Models; using Microsoft.CodeAnalysis; @@ -18,6 +20,7 @@ using OmniSharp.Helpers; using OmniSharp.Mef; using OmniSharp.Models.WorkspaceInformation; +using OmniSharp.Roslyn.EditorConfig; using OmniSharp.Roslyn.Utilities; using OmniSharp.Services; @@ -280,8 +283,21 @@ private ProjectInfo GetProject(CakeScript cakeScript, string filePath) throw new InvalidOperationException($"Could not get host object type: {cakeScript.Host.TypeName}."); } + var projectId = ProjectId.CreateNewId(Guid.NewGuid().ToString()); + var analyzerConfigDocuments = _workspace.EditorConfigEnabled + ? EditorConfigFinder + .GetEditorConfigPaths(filePath) + .Select(path => + DocumentInfo.Create( + DocumentId.CreateNewId(projectId), + name: ".editorconfig", + loader: new FileTextLoader(path, Encoding.UTF8), + filePath: path)) + .ToImmutableArray() + : ImmutableArray.Empty; + return ProjectInfo.Create( - id: ProjectId.CreateNewId(Guid.NewGuid().ToString()), + id: projectId, version: VersionStamp.Create(), name: name, filePath: filePath, @@ -292,7 +308,8 @@ private ProjectInfo GetProject(CakeScript cakeScript, string filePath) metadataReferences: GetMetadataReferences(cakeScript.References), // TODO: projectReferences? isSubmission: true, - hostObjectType: hostObjectType); + hostObjectType: hostObjectType) + .WithAnalyzerConfigDocuments(analyzerConfigDocuments); } private IEnumerable GetMetadataReferences(IEnumerable references) diff --git a/src/OmniSharp.Host/WorkspaceInitializer.cs b/src/OmniSharp.Host/WorkspaceInitializer.cs index 2a98ae407d..476e7cd78c 100644 --- a/src/OmniSharp.Host/WorkspaceInitializer.cs +++ b/src/OmniSharp.Host/WorkspaceInitializer.cs @@ -29,6 +29,8 @@ public static void Initialize(IServiceProvider serviceProvider, CompositionHost projectEventForwarder.Initialize(); var projectSystems = compositionHost.GetExports(); + workspace.EditorConfigEnabled = options.CurrentValue.FormattingOptions.EnableEditorConfigSupport; + foreach (var projectSystem in projectSystems) { try diff --git a/src/OmniSharp.MSBuild/ProjectFile/ItemNames.cs b/src/OmniSharp.MSBuild/ProjectFile/ItemNames.cs index 47ad967f30..785f9df460 100644 --- a/src/OmniSharp.MSBuild/ProjectFile/ItemNames.cs +++ b/src/OmniSharp.MSBuild/ProjectFile/ItemNames.cs @@ -5,6 +5,7 @@ internal static class ItemNames public const string Analyzer = nameof(Analyzer); public const string AdditionalFiles = nameof(AdditionalFiles); public const string Compile = nameof(Compile); + public const string EditorConfigFiles = nameof(EditorConfigFiles); 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.ProjectData.cs b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.ProjectData.cs index 6602d98f64..643967befc 100644 --- a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.ProjectData.cs +++ b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.ProjectData.cs @@ -49,6 +49,8 @@ private class ProjectData public ImmutableArray PackageReferences { get; } public ImmutableArray Analyzers { get; } public ImmutableArray AdditionalFiles { get; } + public ImmutableArray AnalyzerConfigFiles { get; } + public RuleSet RuleSet { get; } public ImmutableDictionary ReferenceAliases { get; } public ImmutableDictionary ProjectReferenceAliases { get; } @@ -70,6 +72,7 @@ private ProjectData() PackageReferences = ImmutableArray.Empty; Analyzers = ImmutableArray.Empty; AdditionalFiles = ImmutableArray.Empty; + AnalyzerConfigFiles = ImmutableArray.Empty; ReferenceAliases = ImmutableDictionary.Empty; ProjectReferenceAliases = ImmutableDictionary.Empty; } @@ -154,6 +157,7 @@ private ProjectData( ImmutableArray packageReferences, ImmutableArray analyzers, ImmutableArray additionalFiles, + ImmutableArray analyzerConfigFiles, bool treatWarningsAsErrors, string defaultNamespace, bool runAnalyzers, @@ -171,6 +175,7 @@ private ProjectData( PackageReferences = packageReferences.EmptyIfDefault(); Analyzers = analyzers.EmptyIfDefault(); AdditionalFiles = additionalFiles.EmptyIfDefault(); + AnalyzerConfigFiles = analyzerConfigFiles.EmptyIfDefault(); ReferenceAliases = referenceAliases; ProjectReferenceAliases = projectReferenceAliases; } @@ -317,13 +322,14 @@ public static ProjectData Create(MSB.Execution.ProjectInstance projectInstance) var packageReferences = GetPackageReferences(projectInstance.GetItems(ItemNames.PackageReference)); var analyzers = GetFullPaths(projectInstance.GetItems(ItemNames.Analyzer)); var additionalFiles = GetFullPaths(projectInstance.GetItems(ItemNames.AdditionalFiles)); + var editorConfigFiles = GetFullPaths(projectInstance.GetItems(ItemNames.EditorConfigFiles)); return new ProjectData(guid, name, assemblyName, targetPath, outputPath, intermediateOutputPath, projectAssetsFile, configuration, platform, targetFramework, targetFrameworks, outputKind, languageVersion, nullableContextOptions, allowUnsafeCode, checkForOverflowUnderflow, documentationFile, preprocessorSymbolNames, suppressedDiagnosticIds, signAssembly, assemblyOriginatorKeyFile, - sourceFiles, projectReferences.ToImmutable(), references.ToImmutable(), packageReferences, analyzers, additionalFiles, treatWarningsAsErrors, defaultNamespace, runAnalyzers, runAnalyzersDuringLiveAnalysis, ruleset, + sourceFiles, projectReferences.ToImmutable(), references.ToImmutable(), packageReferences, analyzers, additionalFiles, editorConfigFiles, treatWarningsAsErrors, defaultNamespace, runAnalyzers, runAnalyzersDuringLiveAnalysis, ruleset, referenceAliases.ToImmutableDictionary(), projectReferenceAliases.ToImmutable()); } diff --git a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.cs b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.cs index 2d9651af76..743ff242ce 100644 --- a/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.cs +++ b/src/OmniSharp.MSBuild/ProjectFile/ProjectFileInfo.cs @@ -52,6 +52,7 @@ internal partial class ProjectFileInfo public ImmutableArray PackageReferences => _data.PackageReferences; public ImmutableArray Analyzers => _data.Analyzers; public ImmutableArray AdditionalFiles => _data.AdditionalFiles; + public ImmutableArray AnalyzerConfigFiles => _data.AnalyzerConfigFiles; public ImmutableDictionary ReferenceAliases => _data.ReferenceAliases; public ImmutableDictionary ProjectReferenceAliases => _data.ProjectReferenceAliases; public bool TreatWarningsAsErrors => _data.TreatWarningsAsErrors; @@ -77,7 +78,7 @@ internal static ProjectFileInfo CreateEmpty(string filePath) { var id = ProjectId.CreateNewId(debugName: filePath); - return new ProjectFileInfo(new ProjectIdInfo(id, isDefinedInSolution:false), filePath, data: null); + return new ProjectFileInfo(new ProjectIdInfo(id, isDefinedInSolution: false), filePath, data: null); } internal static ProjectFileInfo CreateNoBuild(string filePath, ProjectLoader loader) @@ -85,7 +86,7 @@ internal static ProjectFileInfo CreateNoBuild(string filePath, ProjectLoader loa var id = ProjectId.CreateNewId(debugName: filePath); var project = loader.EvaluateProjectFile(filePath); var data = ProjectData.Create(project); - //we are not reading the solution here + //we are not reading the solution here var projectIdInfo = new ProjectIdInfo(id, isDefinedInSolution: false); return new ProjectFileInfo(projectIdInfo, filePath, data); @@ -127,7 +128,7 @@ public static (ProjectFileInfo, ImmutableArray, ProjectLoaded var data = ProjectData.Create(projectInstance); var projectFileInfo = new ProjectFileInfo(ProjectIdInfo, FilePath, data); - var eventArgs = new ProjectLoadedEventArgs(Id, projectInstance, diagnostics, isReload: true, ProjectIdInfo.IsDefinedInSolution,data.References); + var eventArgs = new ProjectLoadedEventArgs(Id, projectInstance, diagnostics, isReload: true, ProjectIdInfo.IsDefinedInSolution, data.References); return (projectFileInfo, diagnostics, eventArgs); } diff --git a/src/OmniSharp.MSBuild/ProjectManager.cs b/src/OmniSharp.MSBuild/ProjectManager.cs index d14b8bd723..c90c564ae4 100644 --- a/src/OmniSharp.MSBuild/ProjectManager.cs +++ b/src/OmniSharp.MSBuild/ProjectManager.cs @@ -176,7 +176,7 @@ private async Task ProcessLoopAsync(CancellationToken cancellationToken) await Task.Delay(LoopDelay, cancellationToken); ProcessQueue(cancellationToken); } - catch(Exception ex) + catch (Exception ex) { _logger.LogError($"Error occurred while processing project updates: {ex}"); } @@ -377,6 +377,14 @@ private void WatchProjectFiles(ProjectFileInfo projectFileInfo) QueueProjectUpdate(projectFileInfo.FilePath, allowAutoRestore: true, projectFileInfo.ProjectIdInfo); }); + if (_workspace.EditorConfigEnabled) + { + _fileSystemWatcher.Watch(".editorconfig", (file, changeType) => + { + QueueProjectUpdate(projectFileInfo.FilePath, allowAutoRestore: false, projectFileInfo.ProjectIdInfo); + }); + } + if (projectFileInfo.RuleSet?.FilePath != null) { _fileSystemWatcher.Watch(projectFileInfo.RuleSet.FilePath, (file, changeType) => @@ -436,6 +444,7 @@ private void UpdateProject(string projectFilePath) UpdateReferences(project, projectFileInfo.ProjectReferences, projectFileInfo.References); UpdateAnalyzerReferences(project, projectFileInfo); UpdateAdditionalFiles(project, projectFileInfo.AdditionalFiles); + UpdateAnalyzerConfigFiles(project, projectFileInfo.AnalyzerConfigFiles); UpdateProjectProperties(project, projectFileInfo); _workspace.TryPromoteMiscellaneousDocumentsToProject(project); @@ -483,10 +492,32 @@ private void UpdateAdditionalFiles(Project project, IList additionalFile foreach (var file in additionalFiles) { - if (File.Exists(file)) - { + if (File.Exists(file)) + { _workspace.AddAdditionalDocument(project.Id, file); - } + } + } + } + + private void UpdateAnalyzerConfigFiles(Project project, IList analyzerConfigFiles) + { + if (!_workspace.EditorConfigEnabled) + { + return; + } + + var currentAnalyzerConfigDocuments = project.AnalyzerConfigDocuments; + foreach (var document in currentAnalyzerConfigDocuments) + { + _workspace.RemoveAnalyzerConfigDocument(document.Id); + } + + foreach (var file in analyzerConfigFiles) + { + if (File.Exists(file)) + { + _workspace.AddAnalyzerConfigDocument(project.Id, file); + } } } diff --git a/src/OmniSharp.Roslyn.CSharp/OmniSharp.Roslyn.CSharp.csproj b/src/OmniSharp.Roslyn.CSharp/OmniSharp.Roslyn.CSharp.csproj index 2f38e3b5ce..e7787704e8 100644 --- a/src/OmniSharp.Roslyn.CSharp/OmniSharp.Roslyn.CSharp.csproj +++ b/src/OmniSharp.Roslyn.CSharp/OmniSharp.Roslyn.CSharp.csproj @@ -20,6 +20,5 @@ - diff --git a/src/OmniSharp.Roslyn.CSharp/Services/EditorConfigWorkspaceOptionsProvider.cs b/src/OmniSharp.Roslyn.CSharp/Services/EditorConfigWorkspaceOptionsProvider.cs deleted file mode 100644 index 94a33a7906..0000000000 --- a/src/OmniSharp.Roslyn.CSharp/Services/EditorConfigWorkspaceOptionsProvider.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Composition; -using System.IO; -using Microsoft.CodeAnalysis.Options; -using Microsoft.Extensions.Logging; -using OmniSharp.Options; -using OmniSharp.Roslyn.CSharp.Services.Formatting.EditorConfig; -using OmniSharp.Roslyn.Options; - -namespace OmniSharp.Roslyn.CSharp.Services -{ - [Export(typeof(IWorkspaceOptionsProvider)), Shared] - public class EditorConfigWorkspaceOptionsProvider : IWorkspaceOptionsProvider - { - private readonly ILoggerFactory _loggerFactory; - - public int Order => 200; - - [ImportingConstructor] - public EditorConfigWorkspaceOptionsProvider(ILoggerFactory loggerFactory) - { - _loggerFactory = loggerFactory; - } - - public OptionSet Process(OptionSet currentOptionSet, OmniSharpOptions omnisharpOptions, IOmniSharpEnvironment omnisharpEnvironment) - { - if (!omnisharpOptions.FormattingOptions.EnableEditorConfigSupport) return currentOptionSet; - - // this is a dummy file that doesn't exist, but we simply want to tell .editorconfig to load *.cs specific settings - var filePath = Path.Combine(omnisharpEnvironment.TargetDirectory, "omnisharp.cs"); - var changedOptionSet = currentOptionSet.WithEditorConfigOptions(filePath, _loggerFactory).GetAwaiter().GetResult(); - return changedOptionSet; - } - } -} diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Formatting/EditorConfig/EditorConfigOptionExtensions.cs b/src/OmniSharp.Roslyn.CSharp/Services/Formatting/EditorConfig/EditorConfigOptionExtensions.cs deleted file mode 100644 index fbb8de4d76..0000000000 --- a/src/OmniSharp.Roslyn.CSharp/Services/Formatting/EditorConfig/EditorConfigOptionExtensions.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Options; -using Microsoft.Extensions.Logging; -using Microsoft.VisualStudio.CodingConventions; - -namespace OmniSharp.Roslyn.CSharp.Services.Formatting.EditorConfig -{ - internal static class EditorConfigOptionExtensions - { - public static async Task WithEditorConfigOptions(this OptionSet optionSet, string path, ILoggerFactory loggerFactory) - { - if (!Path.IsPathRooted(path)) - { - path = Directory.GetCurrentDirectory(); - } - - var codingConventionsManager = CodingConventionsManagerFactory.CreateCodingConventionsManager(); - var optionsApplier = new EditorConfigOptionsApplier(loggerFactory); - var context = await codingConventionsManager.GetConventionContextAsync(path, CancellationToken.None); - - if (context != null && context.CurrentConventions != null) - { - return optionsApplier.ApplyConventions(optionSet, context.CurrentConventions, LanguageNames.CSharp); - } - - - return optionSet; - } - } -} diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Formatting/EditorConfig/EditorConfigOptionsApplier.cs b/src/OmniSharp.Roslyn.CSharp/Services/Formatting/EditorConfig/EditorConfigOptionsApplier.cs deleted file mode 100644 index 7e1fe5c721..0000000000 --- a/src/OmniSharp.Roslyn.CSharp/Services/Formatting/EditorConfig/EditorConfigOptionsApplier.cs +++ /dev/null @@ -1,111 +0,0 @@ -// adapted from https://github.com/dotnet/format/blob/master/src/Utilities/EditorConfigOptionsApplier.cs -// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Microsoft.CodeAnalysis.CodeStyle; -using Microsoft.CodeAnalysis.CSharp.Formatting; -using Microsoft.CodeAnalysis.Formatting; -using Microsoft.CodeAnalysis.Options; -using Microsoft.CodeAnalysis.Simplification; -using Microsoft.Extensions.Logging; -using Microsoft.VisualStudio.CodingConventions; - -namespace OmniSharp.Roslyn.CSharp.Services.Formatting.EditorConfig -{ - internal class EditorConfigOptionsApplier - { - private static readonly List<(IOption, OptionStorageLocation, MethodInfo)> _optionsWithStorage; - private readonly ILogger _logger; - - static EditorConfigOptionsApplier() - { - _optionsWithStorage = new List<(IOption, OptionStorageLocation, MethodInfo)>(); - _optionsWithStorage.AddRange(GetPropertyBasedOptionsWithStorageFromTypes(typeof(FormattingOptions), typeof(CSharpFormattingOptions), typeof(SimplificationOptions), typeof(SimplificationOptions).Assembly.GetType("Microsoft.CodeAnalysis.Simplification.NamingStyleOptions"))); - _optionsWithStorage.AddRange(GetFieldBasedOptionsWithStorageFromTypes(typeof(CodeStyleOptions), typeof(CSharpFormattingOptions).Assembly.GetType("Microsoft.CodeAnalysis.CSharp.CodeStyle.CSharpCodeStyleOptions"))); - } - - public EditorConfigOptionsApplier(ILoggerFactory loggerFactory) - { - _logger = loggerFactory.CreateLogger(); - } - - public OptionSet ApplyConventions(OptionSet optionSet, ICodingConventionsSnapshot codingConventions, string languageName) - { - try - { - var adjustedConventions = codingConventions.AllRawConventions.ToDictionary(kvp => kvp.Key, kvp => (string)kvp.Value); - _logger.LogDebug($"All raw discovered .editorconfig options: {string.Join(Environment.NewLine, adjustedConventions.Select(kvp => $"{kvp.Key}={kvp.Value}"))}"); - - foreach (var optionWithStorage in _optionsWithStorage) - { - if (TryGetConventionValue(optionWithStorage, adjustedConventions, out var value)) - { - var option = optionWithStorage.Item1; - _logger.LogTrace($"Applying .editorconfig option {option.Name}"); - var optionKey = new OptionKey(option, option.IsPerLanguage ? languageName : null); - optionSet = optionSet.WithChangedOption(optionKey, value); - } - } - } - catch (Exception e) - { - _logger.LogError(e, "There was an error when applying .editorconfig options."); - } - - return optionSet; - } - - internal static IEnumerable<(IOption, OptionStorageLocation, MethodInfo)> GetPropertyBasedOptionsWithStorageFromTypes(params Type[] types) - => types - .SelectMany(t => t.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.GetProperty)) - .Where(p => typeof(IOption).IsAssignableFrom(p.PropertyType)).Select(p => (IOption)p.GetValue(null)) - .Select(GetOptionWithStorage).Where(ows => ows.Item2 != null); - - internal static IEnumerable<(IOption, OptionStorageLocation, MethodInfo)> GetFieldBasedOptionsWithStorageFromTypes(params Type[] types) - => types - .SelectMany(t => t.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)) - .Where(p => typeof(IOption).IsAssignableFrom(p.FieldType)).Select(p => (IOption)p.GetValue(null)) - .Select(GetOptionWithStorage).Where(ows => ows.Item2 != null); - - internal static (IOption, OptionStorageLocation, MethodInfo) GetOptionWithStorage(IOption option) - { - var editorConfigStorage = !option.StorageLocations.IsDefaultOrEmpty - ? option.StorageLocations.FirstOrDefault(IsEditorConfigStorage) - : null; - - var tryGetOptionMethod = editorConfigStorage?.GetType().GetMethod("TryGetOption", new[] { typeof(IReadOnlyDictionary), typeof(Type), typeof(object).MakeByRefType() }); - return (option, editorConfigStorage, tryGetOptionMethod); - } - - internal static bool IsEditorConfigStorage(OptionStorageLocation storageLocation) - { - if (storageLocation.GetType().FullName.StartsWith("Microsoft.CodeAnalysis.Options.EditorConfigStorageLocation")) - { - return true; - } - - if (storageLocation.GetType().FullName.StartsWith("Microsoft.CodeAnalysis.Options.NamingStylePreferenceEditorConfigStorageLocation")) - { - return true; - } - - return false; - } - - internal static bool TryGetConventionValue((IOption, OptionStorageLocation, MethodInfo) optionWithStorage, Dictionary adjustedConventions, out object value) - { - var (option, editorConfigStorage, tryGetOptionMethod) = optionWithStorage; - value = null; - - var args = new object[] { adjustedConventions, option.Type, value }; - - var isOptionPresent = (bool)tryGetOptionMethod.Invoke(editorConfigStorage, args); - value = args[2]; - - return isOptionPresent; - } - } -} diff --git a/src/OmniSharp.Roslyn.CSharp/Workers/Formatting/FormattingWorker.cs b/src/OmniSharp.Roslyn.CSharp/Workers/Formatting/FormattingWorker.cs index 3b32dfd18b..0c7d4e9b02 100644 --- a/src/OmniSharp.Roslyn.CSharp/Workers/Formatting/FormattingWorker.cs +++ b/src/OmniSharp.Roslyn.CSharp/Workers/Formatting/FormattingWorker.cs @@ -1,17 +1,13 @@ using System.Collections.Generic; using System.Linq; -using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Formatting; -using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Text; using Microsoft.Extensions.Logging; -using Microsoft.VisualStudio.CodingConventions; using OmniSharp.Models; using OmniSharp.Options; -using OmniSharp.Roslyn.CSharp.Services.Formatting.EditorConfig; using OmniSharp.Roslyn.Utilities; namespace OmniSharp.Roslyn.CSharp.Workers.Formatting @@ -103,9 +99,10 @@ public static async Task> GetFormattedTe private static async Task FormatDocument(Document document, OmniSharpOptions omnisharpOptions, ILoggerFactory loggerFactory, TextSpan? textSpan = null) { + // If we are not using .editorconfig for formatting options then we can avoid any overhead of calculating document options. var optionSet = omnisharpOptions.FormattingOptions.EnableEditorConfigSupport - ? await document.Project.Solution.Workspace.Options.WithEditorConfigOptions(document.FilePath, loggerFactory) - : document.Project.Solution.Workspace.Options; + ? await document.GetOptionsAsync() + : document.Project.Solution.Options; var newDocument = textSpan != null ? await Formatter.FormatAsync(document, textSpan.Value, optionSet) : await Formatter.FormatAsync(document, optionSet); if (omnisharpOptions.FormattingOptions.OrganizeImports) diff --git a/src/OmniSharp.Roslyn/EditorConfig/EditorConfigFinder.cs b/src/OmniSharp.Roslyn/EditorConfig/EditorConfigFinder.cs new file mode 100644 index 0000000000..ddb66c4439 --- /dev/null +++ b/src/OmniSharp.Roslyn/EditorConfig/EditorConfigFinder.cs @@ -0,0 +1,61 @@ +// adapted from https://github.com/dotnet/format/blob/d8a66bbcc6b6b9e769eb168cb384b44328786f7b/src/Utilities/EditorConfigFinder.cs +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. + +using System.IO; +using System.Collections.Immutable; +using System.Linq; + +namespace OmniSharp.Roslyn.EditorConfig +{ + public static class EditorConfigFinder + { + public static ImmutableArray GetEditorConfigPaths(string path) + { + // If we are passed a filename then try to parse out the path + if (!Directory.Exists(path) && + !TryGetDirectoryPath(path, out path)) + { + return ImmutableArray.Empty; + } + + if (!Directory.Exists(path)) + { + return ImmutableArray.Empty; + } + + var directory = new DirectoryInfo(path); + + var editorConfigPaths = directory.GetFiles(".editorconfig", SearchOption.AllDirectories) + .Select(file => file.FullName) + .ToList(); + + try + { + while (directory.Parent is object) + { + directory = directory.Parent; + editorConfigPaths.AddRange( + directory.GetFiles(".editorconfig", SearchOption.TopDirectoryOnly) + .Select(file => file.FullName)); + } + } + catch { } + + return editorConfigPaths.ToImmutableArray(); + } + + private static bool TryGetDirectoryPath(string path, out string directoryPath) + { + try + { + directoryPath = Path.GetDirectoryName(path); + return true; + } + catch + { + directoryPath = default; + return false; + } + } + } +} diff --git a/src/OmniSharp.Roslyn/OmniSharpWorkspace.cs b/src/OmniSharp.Roslyn/OmniSharpWorkspace.cs index d8213e92a6..a6d423b453 100644 --- a/src/OmniSharp.Roslyn/OmniSharpWorkspace.cs +++ b/src/OmniSharp.Roslyn/OmniSharpWorkspace.cs @@ -16,6 +16,7 @@ using OmniSharp.FileSystem; using OmniSharp.FileWatching; using OmniSharp.Roslyn; +using OmniSharp.Roslyn.EditorConfig; using OmniSharp.Roslyn.Utilities; using OmniSharp.Utilities; @@ -25,6 +26,7 @@ namespace OmniSharp public class OmniSharpWorkspace : Workspace { public bool Initialized { get; set; } + public bool EditorConfigEnabled { get; set; } public BufferManager BufferManager { get; private set; } private readonly ILogger _logger; @@ -103,6 +105,21 @@ public DocumentId TryAddMiscellaneousDocument(string filePath, string language) var projectInfo = miscDocumentsProjectInfos.GetOrAdd(language, (lang) => CreateMiscFilesProject(lang)); var documentId = AddDocument(projectInfo.Id, filePath); _logger.LogInformation($"Miscellaneous file: {filePath} added to workspace"); + + if (!EditorConfigEnabled) + { + return documentId; + } + + var analyzerConfigFiles = projectInfo.AnalyzerConfigDocuments.Select(document => document.FilePath); + var newAnalyzerConfigFiles = EditorConfigFinder + .GetEditorConfigPaths(filePath) + .Except(analyzerConfigFiles); + foreach (var analyzerConfigFile in newAnalyzerConfigFiles) + { + AddAnalyzerConfigDocument(projectInfo.Id, analyzerConfigFile); + } + return documentId; } @@ -153,7 +170,7 @@ public void TryPromoteMiscellaneousDocumentsToProject(Project project) public void UpdateDiagnosticOptionsForProject(ProjectId projectId, ImmutableDictionary rules) { var project = this.CurrentSolution.GetProject(projectId); - OnCompilationOptionsChanged(projectId, project.CompilationOptions.WithSpecificDiagnosticOptions(rules)); + OnCompilationOptionsChanged(projectId, project.CompilationOptions.WithSpecificDiagnosticOptions(rules)); } private ProjectInfo CreateMiscFilesProject(string language) @@ -222,7 +239,7 @@ internal DocumentId AddDocument(DocumentId documentId, Project project, string f // folder computation is best effort. in case of exceptions, we back out because it's not essential for core features try { - // find the relative path from project file to our document + // find the relative path from project file to our document var relativeDocumentPath = FileSystemHelper.GetRelativePath(fullPath, basePath); // only set document's folders if @@ -449,13 +466,13 @@ public void SetAnalyzerReferences(ProjectId id, ImmutableArray project.AnalyzerReferences.All(oldRef => oldRef.Display != newRef.Display)); var refsToRemove = project.AnalyzerReferences.Where(newRef => analyzerReferences.All(oldRef => oldRef.Display != newRef.Display)); - foreach(var toAdd in refsToAdd) + foreach (var toAdd in refsToAdd) { _logger.LogInformation($"Adding analyzer reference: {toAdd.FullPath}"); base.OnAnalyzerReferenceAdded(id, toAdd); } - foreach(var toRemove in refsToRemove) + foreach (var toRemove in refsToRemove) { _logger.LogInformation($"Removing analyzer reference: {toRemove.FullPath}"); base.OnAnalyzerReferenceRemoved(id, toRemove); @@ -470,11 +487,24 @@ public void AddAdditionalDocument(ProjectId projectId, string filePath) OnAdditionalDocumentAdded(documentInfo); } + public void AddAnalyzerConfigDocument(ProjectId projectId, string filePath) + { + var documentId = DocumentId.CreateNewId(projectId); + var loader = new OmniSharpTextLoader(filePath); + var documentInfo = DocumentInfo.Create(documentId, Path.GetFileName(filePath), filePath: filePath, loader: loader); + OnAnalyzerConfigDocumentAdded(documentInfo); + } + public void RemoveAdditionalDocument(DocumentId documentId) { OnAdditionalDocumentRemoved(documentId); } + public void RemoveAnalyzerConfigDocument(DocumentId documentId) + { + OnAnalyzerConfigDocumentRemoved(documentId); + } + protected override void ApplyProjectChanges(ProjectChanges projectChanges) { // since Roslyn currently doesn't handle DefaultNamespace changes via ApplyProjectChanges diff --git a/src/OmniSharp.Script/ScriptContextProvider.cs b/src/OmniSharp.Script/ScriptContextProvider.cs index 830af001a4..f4ae9e61b6 100644 --- a/src/OmniSharp.Script/ScriptContextProvider.cs +++ b/src/OmniSharp.Script/ScriptContextProvider.cs @@ -70,7 +70,7 @@ public ScriptContextProvider(ILoggerFactory loggerFactory, IOmniSharpEnvironment }); } - public ScriptContext CreateScriptContext(ScriptOptions scriptOptions, string[] allCsxFiles) + public ScriptContext CreateScriptContext(ScriptOptions scriptOptions, string[] allCsxFiles, bool editorConfigEnabled) { var currentDomainAssemblies = AppDomain.CurrentDomain.GetAssemblies(); @@ -134,7 +134,7 @@ public ScriptContext CreateScriptContext(ScriptOptions scriptOptions, string[] a AddMetadataReference(metadataReferences, inheritedCompileLib.Location); } - var scriptProjectProvider = new ScriptProjectProvider(scriptOptions, _env, _loggerFactory, isDesktopClr); + var scriptProjectProvider = new ScriptProjectProvider(scriptOptions, _env, _loggerFactory, isDesktopClr, editorConfigEnabled); return new ScriptContext(scriptProjectProvider, metadataReferences, compilationDependencies, _defaultGlobalsType); } diff --git a/src/OmniSharp.Script/ScriptProjectProvider.cs b/src/OmniSharp.Script/ScriptProjectProvider.cs index 5f8afc0d73..6109b0d461 100644 --- a/src/OmniSharp.Script/ScriptProjectProvider.cs +++ b/src/OmniSharp.Script/ScriptProjectProvider.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.IO; using System.Linq; using System.Reflection; +using System.Text; using Dotnet.Script.DependencyModel.NuGet; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -10,6 +12,7 @@ using Microsoft.CodeAnalysis.Scripting.Hosting; using Microsoft.Extensions.Logging; using OmniSharp.Helpers; +using OmniSharp.Roslyn.EditorConfig; using OmniSharp.Roslyn.Utilities; namespace OmniSharp.Script @@ -48,8 +51,9 @@ public class ScriptProjectProvider private readonly IOmniSharpEnvironment _env; private readonly ILogger _logger; private readonly bool _isDesktopClr; + private readonly bool _editorConfigEnabled; - public ScriptProjectProvider(ScriptOptions scriptOptions, IOmniSharpEnvironment env, ILoggerFactory loggerFactory, bool isDesktopClr) + public ScriptProjectProvider(ScriptOptions scriptOptions, IOmniSharpEnvironment env, ILoggerFactory loggerFactory, bool isDesktopClr, bool editorConfigEnabled) { _scriptOptions = scriptOptions ?? throw new ArgumentNullException(nameof(scriptOptions)); _env = env ?? throw new ArgumentNullException(nameof(env)); @@ -58,6 +62,7 @@ public ScriptProjectProvider(ScriptOptions scriptOptions, IOmniSharpEnvironment _compilationOptions = new Lazy(CreateCompilationOptions); _commandLineArgs = new Lazy(CreateCommandLineArguments); _isDesktopClr = isDesktopClr; + _editorConfigEnabled = editorConfigEnabled; } private CSharpCommandLineArguments CreateCommandLineArguments() @@ -158,9 +163,22 @@ public ProjectInfo CreateProject(string csxFileName, IEnumerable + DocumentInfo.Create( + DocumentId.CreateNewId(projectId), + name: ".editorconfig", + loader: new FileTextLoader(path, Encoding.UTF8), + filePath: path)) + .ToImmutableArray() + : ImmutableArray.Empty; + var project = ProjectInfo.Create( filePath: csxFilePath, - id: ProjectId.CreateNewId(), + id: projectId, version: VersionStamp.Create(), name: csxFileName, assemblyName: $"{csxFileName}.dll", @@ -171,7 +189,8 @@ public ProjectInfo CreateProject(string csxFileName, IEnumerable(() => _scriptContextProvider.CreateScriptContext(_scriptOptions, allCsxFiles)); + _scriptContext = new Lazy(() => _scriptContextProvider.CreateScriptContext(_scriptOptions, allCsxFiles, _workspace.EditorConfigEnabled)); if (allCsxFiles.Length == 0) { diff --git a/test-assets/test-projects/ProjectWithAnalyzersAndEditorConfig/.editorconfig b/test-assets/test-projects/ProjectWithAnalyzersAndEditorConfig/.editorconfig new file mode 100644 index 0000000000..9bf71d115a --- /dev/null +++ b/test-assets/test-projects/ProjectWithAnalyzersAndEditorConfig/.editorconfig @@ -0,0 +1,5 @@ +root = true + +[*.cs] +# IDE0005: Unnecessary using +dotnet_diagnostic.IDE0005.severity = error diff --git a/test-assets/test-projects/ProjectWithAnalyzersAndEditorConfig/Program.cs b/test-assets/test-projects/ProjectWithAnalyzersAndEditorConfig/Program.cs new file mode 100644 index 0000000000..20f32262ed --- /dev/null +++ b/test-assets/test-projects/ProjectWithAnalyzersAndEditorConfig/Program.cs @@ -0,0 +1,12 @@ +using System; + +namespace HelloWorld +{ + class Program + { + static void Main(string[] args) + { + + } + } +} diff --git a/test-assets/test-projects/ProjectWithAnalyzersAndEditorConfig/ProjectWithAnalyzersAndEditorConfig.csproj b/test-assets/test-projects/ProjectWithAnalyzersAndEditorConfig/ProjectWithAnalyzersAndEditorConfig.csproj new file mode 100644 index 0000000000..d6555490c9 --- /dev/null +++ b/test-assets/test-projects/ProjectWithAnalyzersAndEditorConfig/ProjectWithAnalyzersAndEditorConfig.csproj @@ -0,0 +1,7 @@ + + + Exe + netcoreapp2.1 + 7.1 + + diff --git a/tests/OmniSharp.MSBuild.Tests/ProjectWithAnalyzersTests.cs b/tests/OmniSharp.MSBuild.Tests/ProjectWithAnalyzersTests.cs index 9b1d9219f3..1e8e63704a 100644 --- a/tests/OmniSharp.MSBuild.Tests/ProjectWithAnalyzersTests.cs +++ b/tests/OmniSharp.MSBuild.Tests/ProjectWithAnalyzersTests.cs @@ -75,6 +75,54 @@ public async Task WhenProjectIsLoadedThenItContainsCustomRulesetsFromCsproj() } } + [Fact] + public async Task WhenProjectIsLoadedThenItContainsAnalyzerConfigurationFromEditorConfig() + { + using (var testProject = await TestAssets.Instance.GetTestProjectAsync("ProjectWithAnalyzersAndEditorConfig")) + using (var host = CreateMSBuildTestHost(testProject.Directory, configurationData: TestHelpers.GetConfigurationDataWithAnalyzerConfig(roslynAnalyzersEnabled: true, editorConfigEnabled: true))) + { + var diagnostics = await host.RequestCodeCheckAsync(Path.Combine(testProject.Directory, "Program.cs")); + + Assert.NotEmpty(diagnostics.QuickFixes); + + var quickFix = diagnostics.QuickFixes.OfType().Single(x => x.Id == "IDE0005"); + Assert.Equal("Error", quickFix.LogLevel); + } + } + + [Fact] + public async Task WhenProjectEditorConfigIsChangedThenAnalyzerConfigurationUpdates() + { + var emitter = new ProjectLoadTestEventEmitter(); + + using (var testProject = await TestAssets.Instance.GetTestProjectAsync("ProjectWithAnalyzersAndEditorConfig")) + using (var host = CreateMSBuildTestHost( + testProject.Directory, + emitter.AsExportDescriptionProvider(LoggerFactory), + TestHelpers.GetConfigurationDataWithAnalyzerConfig(roslynAnalyzersEnabled: true, editorConfigEnabled: true))) + { + var initialProject = host.Workspace.CurrentSolution.Projects.Single(); + var analyzerConfigDocument = initialProject.AnalyzerConfigDocuments.Single(); + + File.WriteAllText(analyzerConfigDocument.FilePath, @" +root = true + +[*.cs] +# IDE0005: Unnecessary using +dotnet_diagnostic.IDE0005.severity = none +"); + + await NotifyFileChanged(host, analyzerConfigDocument.FilePath); + + emitter.WaitForProjectUpdate(); + + var diagnostics = await host.RequestCodeCheckAsync(Path.Combine(testProject.Directory, "Program.cs")); + + Assert.NotEmpty(diagnostics.QuickFixes); + Assert.DoesNotContain(diagnostics.QuickFixes.OfType(), x => x.Id == "IDE0005"); + } + } + [Theory] [InlineData("ProjectWithDisabledAnalyzers")] [InlineData("ProjectWithDisabledAnalyzers2")] diff --git a/tests/TestUtility/TestHelpers.cs b/tests/TestUtility/TestHelpers.cs index 4217ff5203..139a7b8bbd 100644 --- a/tests/TestUtility/TestHelpers.cs +++ b/tests/TestUtility/TestHelpers.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Text; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; @@ -13,6 +14,7 @@ using OmniSharp; using OmniSharp.FileWatching; using OmniSharp.MSBuild.Discovery; +using OmniSharp.Roslyn.EditorConfig; using OmniSharp.Script; using OmniSharp.Services; @@ -30,7 +32,7 @@ public static OmniSharpWorkspace CreateCsxWorkspace(TestFile testFile) public static void AddCsxProjectToWorkspace(OmniSharpWorkspace workspace, TestFile testFile) { var references = GetReferences(); - var scriptHelper = new ScriptProjectProvider(new ScriptOptions(), new OmniSharpEnvironment(), new LoggerFactory(), true); + var scriptHelper = new ScriptProjectProvider(new ScriptOptions(), new OmniSharpEnvironment(), new LoggerFactory(), isDesktopClr: true, editorConfigEnabled: true); var project = scriptHelper.CreateProject(testFile.FileName, references.Union(new[] { MetadataReference.CreateFromFile(typeof(CommandLineScriptGlobals).GetTypeInfo().Assembly.Location) }), testFile.FileName, typeof(CommandLineScriptGlobals), Enumerable.Empty()); workspace.AddProject(project); @@ -49,18 +51,30 @@ public static IEnumerable AddProjectToWorkspace(OmniSharpWorkspace wo var references = GetReferences(); frameworks = frameworks ?? new[] { string.Empty }; var projectsIds = new List(); + var editorConfigPaths = EditorConfigFinder.GetEditorConfigPaths(filePath); foreach (var framework in frameworks) { + var projectId = ProjectId.CreateNewId(); + var analyzerConfigDocuments = editorConfigPaths.Select(path => + DocumentInfo.Create( + DocumentId.CreateNewId(projectId), + name: ".editorconfig", + loader: new FileTextLoader(path, Encoding.UTF8), + filePath: path)) + .ToImmutableArray(); + var projectInfo = ProjectInfo.Create( - id: ProjectId.CreateNewId(), + id: projectId, version: versionStamp, name: "OmniSharp+" + framework, assemblyName: "AssemblyName", language: LanguageNames.CSharp, filePath: filePath, metadataReferences: references, - analyzerReferences: analyzerRefs).WithDefaultNamespace("OmniSharpTest"); + analyzerReferences: analyzerRefs) + .WithDefaultNamespace("OmniSharpTest") + .WithAnalyzerConfigDocuments(analyzerConfigDocuments); workspace.AddProject(projectInfo); @@ -114,15 +128,23 @@ public static MSBuildInstance AddDotNetCoreToFakeInstance(this MSBuildInstance i return instance; } - public static Dictionary GetConfigurationDataWithAnalyzerConfig(bool roslynAnalyzersEnabled = false, Dictionary existingConfiguration = null) + public static Dictionary GetConfigurationDataWithAnalyzerConfig( + bool roslynAnalyzersEnabled = false, + bool editorConfigEnabled = false, + Dictionary existingConfiguration = null) { if (existingConfiguration == null) { - return new Dictionary() { { "RoslynExtensionsOptions:EnableAnalyzersSupport", roslynAnalyzersEnabled.ToString() } }; + return new Dictionary() + { + { "RoslynExtensionsOptions:EnableAnalyzersSupport", roslynAnalyzersEnabled.ToString() }, + { "FormattingOptions:EnableEditorConfigSupport", editorConfigEnabled.ToString() } + }; } var copyOfExistingConfigs = existingConfiguration.ToDictionary(x => x.Key, x => x.Value); copyOfExistingConfigs.Add("RoslynExtensionsOptions:EnableAnalyzersSupport", roslynAnalyzersEnabled.ToString()); + copyOfExistingConfigs.Add("FormattingOptions:EnableEditorConfigSupport", editorConfigEnabled.ToString()); return copyOfExistingConfigs; }