File: ProjectSystem\RuleSets\RuleSetEventHandler.cs
Web Access
Project: src\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices_pxr0p0dn_wpftmp.csproj (Microsoft.VisualStudio.LanguageServices)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
#nullable disable
 
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Roslyn.Utilities;
using VSLangProj;
 
namespace Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem.RuleSets;
 
[Export(typeof(RuleSetEventHandler))]
internal sealed class RuleSetEventHandler : IVsTrackProjectDocumentsEvents2, IVsTrackProjectDocumentsEvents3, IVsTrackProjectDocumentsEvents4
{
    private readonly IThreadingContext _threadingContext;
    private readonly IServiceProvider _serviceProvider;
    private bool _eventsHookedUp = false;
    private uint _cookie = 0;
 
    [ImportingConstructor]
    [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
    public RuleSetEventHandler(
        IThreadingContext threadingContext,
        [Import(typeof(SVsServiceProvider))] IServiceProvider serviceProvider)
    {
        _threadingContext = threadingContext;
        _serviceProvider = serviceProvider;
    }
 
    public async Task RegisterAsync(IAsyncServiceProvider serviceProvider, CancellationToken cancellationToken)
    {
        if (!_eventsHookedUp)
        {
            var trackProjectDocuments = await serviceProvider.GetServiceAsync<SVsTrackProjectDocuments, IVsTrackProjectDocuments2>(_threadingContext.JoinableTaskFactory).ConfigureAwait(false);
            await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
 
            if (!_eventsHookedUp)
            {
                if (ErrorHandler.Succeeded(trackProjectDocuments.AdviseTrackProjectDocumentsEvents(this, out _cookie)))
                    _eventsHookedUp = true;
            }
        }
    }
 
    public void Unregister()
    {
        if (_eventsHookedUp)
        {
            var trackProjectDocuments = (IVsTrackProjectDocuments2)_serviceProvider.GetService(typeof(SVsTrackProjectDocuments));
 
            // Null check, because sometimes during shutdown the IVsTrackProjectDocuments2 is cleaned up before we get told to unregister
            if (trackProjectDocuments is null || ErrorHandler.Succeeded(trackProjectDocuments.UnadviseTrackProjectDocumentsEvents(_cookie)))
            {
                _eventsHookedUp = false;
                _cookie = 0;
            }
        }
    }
 
    int IVsTrackProjectDocumentsEvents2.OnAfterAddDirectoriesEx(int cProjects, int cDirectories, IVsProject[] rgpProjects, int[] rgFirstIndices, string[] rgpszMkDocuments, VSADDDIRECTORYFLAGS[] rgFlags)
        => VSConstants.S_OK;
 
    int IVsTrackProjectDocumentsEvents2.OnAfterAddFilesEx(int cProjects, int cFiles, IVsProject[] rgpProjects, int[] rgFirstIndices, string[] rgpszMkDocuments, VSADDFILEFLAGS[] rgFlags)
        => VSConstants.S_OK;
 
    int IVsTrackProjectDocumentsEvents2.OnAfterRemoveDirectories(int cProjects, int cDirectories, IVsProject[] rgpProjects, int[] rgFirstIndices, string[] rgpszMkDocuments, VSREMOVEDIRECTORYFLAGS[] rgFlags)
        => VSConstants.S_OK;
 
    int IVsTrackProjectDocumentsEvents2.OnAfterRemoveFiles(int cProjects, int cFiles, IVsProject[] rgpProjects, int[] rgFirstIndices, string[] rgpszMkDocuments, VSREMOVEFILEFLAGS[] rgFlags)
        => VSConstants.S_OK;
 
    int IVsTrackProjectDocumentsEvents2.OnAfterRenameDirectories(int cProjects, int cDirs, IVsProject[] rgpProjects, int[] rgFirstIndices, string[] rgszMkOldNames, string[] rgszMkNewNames, VSRENAMEDIRECTORYFLAGS[] rgFlags)
        => VSConstants.S_OK;
 
    int IVsTrackProjectDocumentsEvents2.OnAfterRenameFiles(int cProjects, int cFiles, IVsProject[] rgpProjects, int[] rgFirstIndices, string[] rgszMkOldNames, string[] rgszMkNewNames, VSRENAMEFILEFLAGS[] rgFlags)
    {
        var ruleSetRenames = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 
        for (var i = 0; i < rgszMkOldNames.Length; i++)
        {
            var oldFileFullPath = rgszMkOldNames[i];
            if (Path.GetExtension(oldFileFullPath).Equals(".ruleset", StringComparison.OrdinalIgnoreCase))
            {
                var newFileFullPath = rgszMkNewNames[i];
                ruleSetRenames[oldFileFullPath] = newFileFullPath;
            }
        }
 
        foreach (var path in ruleSetRenames.Values)
        {
            UpdateCodeAnalysisRuleSetPropertiesInAllProjects(path);
        }
 
        return VSConstants.S_OK;
    }
 
    int IVsTrackProjectDocumentsEvents2.OnAfterSccStatusChanged(int cProjects, int cFiles, IVsProject[] rgpProjects, int[] rgFirstIndices, string[] rgpszMkDocuments, uint[] rgdwSccStatus)
        => VSConstants.S_OK;
 
    int IVsTrackProjectDocumentsEvents2.OnQueryAddDirectories(IVsProject pProject, int cDirectories, string[] rgpszMkDocuments, VSQUERYADDDIRECTORYFLAGS[] rgFlags, VSQUERYADDDIRECTORYRESULTS[] pSummaryResult, VSQUERYADDDIRECTORYRESULTS[] rgResults)
        => VSConstants.S_OK;
 
    int IVsTrackProjectDocumentsEvents2.OnQueryAddFiles(IVsProject pProject, int cFiles, string[] rgpszMkDocuments, VSQUERYADDFILEFLAGS[] rgFlags, VSQUERYADDFILERESULTS[] pSummaryResult, VSQUERYADDFILERESULTS[] rgResults)
        => VSConstants.S_OK;
 
    int IVsTrackProjectDocumentsEvents2.OnQueryRemoveDirectories(IVsProject pProject, int cDirectories, string[] rgpszMkDocuments, VSQUERYREMOVEDIRECTORYFLAGS[] rgFlags, VSQUERYREMOVEDIRECTORYRESULTS[] pSummaryResult, VSQUERYREMOVEDIRECTORYRESULTS[] rgResults)
        => VSConstants.S_OK;
 
    int IVsTrackProjectDocumentsEvents2.OnQueryRemoveFiles(IVsProject pProject, int cFiles, string[] rgpszMkDocuments, VSQUERYREMOVEFILEFLAGS[] rgFlags, VSQUERYREMOVEFILERESULTS[] pSummaryResult, VSQUERYREMOVEFILERESULTS[] rgResults)
        => VSConstants.S_OK;
 
    int IVsTrackProjectDocumentsEvents2.OnQueryRenameDirectories(IVsProject pProject, int cDirs, string[] rgszMkOldNames, string[] rgszMkNewNames, VSQUERYRENAMEDIRECTORYFLAGS[] rgFlags, VSQUERYRENAMEDIRECTORYRESULTS[] pSummaryResult, VSQUERYRENAMEDIRECTORYRESULTS[] rgResults)
        => VSConstants.S_OK;
 
    int IVsTrackProjectDocumentsEvents2.OnQueryRenameFiles(IVsProject pProject, int cFiles, string[] rgszMkOldNames, string[] rgszMkNewNames, VSQUERYRENAMEFILEFLAGS[] rgFlags, VSQUERYRENAMEFILERESULTS[] pSummaryResult, VSQUERYRENAMEFILERESULTS[] rgResults)
        => VSConstants.S_OK;
 
    int IVsTrackProjectDocumentsEvents3.OnBeginQueryBatch()
        => VSConstants.S_OK;
 
    int IVsTrackProjectDocumentsEvents3.OnEndQueryBatch(out int pfActionOK)
    {
        pfActionOK = 1;
        return VSConstants.S_OK;
    }
 
    int IVsTrackProjectDocumentsEvents3.OnCancelQueryBatch()
        => VSConstants.S_OK;
 
    int IVsTrackProjectDocumentsEvents3.OnQueryAddFilesEx(IVsProject pProject, int cFiles, string[] rgpszNewMkDocuments, string[] rgpszSrcMkDocuments, VSQUERYADDFILEFLAGS[] rgFlags, VSQUERYADDFILERESULTS[] pSummaryResult, VSQUERYADDFILERESULTS[] rgResults)
        => VSConstants.S_OK;
 
    int IVsTrackProjectDocumentsEvents3.HandsOffFiles(uint grfRequiredAccess, int cFiles, string[] rgpszMkDocuments)
        => VSConstants.S_OK;
 
    int IVsTrackProjectDocumentsEvents3.HandsOnFiles(int cFiles, string[] rgpszMkDocuments)
        => VSConstants.S_OK;
 
    void IVsTrackProjectDocumentsEvents4.OnQueryRemoveFilesEx(IVsProject pProject, int cFiles, string[] rgpszMkDocuments, uint[] rgFlags, VSQUERYREMOVEFILERESULTS[] pSummaryResult, VSQUERYREMOVEFILERESULTS[] rgResults)
    {
    }
 
    void IVsTrackProjectDocumentsEvents4.OnQueryRemoveDirectoriesEx(IVsProject pProject, int cDirectories, string[] rgpszMkDocuments, uint[] rgFlags, VSQUERYREMOVEDIRECTORYRESULTS[] pSummaryResult, VSQUERYREMOVEDIRECTORYRESULTS[] rgResults)
    {
    }
 
    void IVsTrackProjectDocumentsEvents4.OnAfterRemoveFilesEx(int cProjects, int cFiles, IVsProject[] rgpProjects, int[] rgFirstIndices, string[] rgpszMkDocuments, uint[] rgFlags)
    {
        // First, handle the files that have been removed from projects (rather than deleted).
        // Here we only want to update the projects from which the file was removed.
 
        for (var i = 0; i < rgpProjects.Length; i++)
        {
            var indexOfFirstDocumentInProject = IndexOfFirstDocumentInProject(i, rgFirstIndices);
            var indexOfFirstDocumentInNextProject = IndexOfFirstDocumentInProject(i + 1, rgFirstIndices);
            for (var j = indexOfFirstDocumentInProject; j < indexOfFirstDocumentInNextProject; j++)
            {
                var fileFullPath = rgpszMkDocuments[j];
                var removed = (rgFlags[j] & (uint)__VSREMOVEFILEFLAGS2.VSREMOVEFILEFLAGS_IsRemovedFromProjectOnly) != 0;
                if (removed &&
                    Path.GetExtension(fileFullPath).Equals(".ruleset", StringComparison.OrdinalIgnoreCase))
                {
                    if (rgpProjects[i] is IVsHierarchy hierarchy &&
                        hierarchy.TryGetProject(out var project))
                    {
                        UpdateCodeAnalysisRuleSetPropertiesInProject(project, string.Empty);
                    }
                }
            }
        }
 
        // Second, handle the files that have been deleted. In this case we need to update
        // every project that was using this file in some way.
        var ruleSetDeletions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 
        for (var i = 0; i < rgpszMkDocuments.Length; i++)
        {
            var fileFullPath = rgpszMkDocuments[i];
            var deleted = (rgFlags[i] & (uint)__VSREMOVEFILEFLAGS2.VSREMOVEFILEFLAGS_IsRemovedFromProjectOnly) == 0;
            if (deleted &&
                Path.GetExtension(fileFullPath).Equals(".ruleset", StringComparison.OrdinalIgnoreCase))
            {
                ruleSetDeletions.Add(fileFullPath);
            }
        }
 
#pragma warning disable IDE0059 // Unnecessary assignment of a value - https://github.com/dotnet/roslyn/issues/46168
        foreach (var fileFullPath in ruleSetDeletions)
#pragma warning restore IDE0059 // Unnecessary assignment of a value
        {
            UpdateCodeAnalysisRuleSetPropertiesInAllProjects(string.Empty);
        }
    }
 
    void IVsTrackProjectDocumentsEvents4.OnAfterRemoveDirectoriesEx(int cProjects, int cDirectories, IVsProject[] rgpProjects, int[] rgFirstIndices, string[] rgpszMkDocuments, uint[] rgFlags)
    {
    }
 
    private void UpdateCodeAnalysisRuleSetPropertiesInAllProjects(string newFileFullPath)
    {
        var dte = (EnvDTE.DTE)_serviceProvider.GetService(typeof(SDTE));
        foreach (EnvDTE.Project project in dte.Solution.Projects)
        {
            UpdateCodeAnalysisRuleSetPropertiesInProject(project, newFileFullPath);
        }
    }
 
    private static void UpdateCodeAnalysisRuleSetPropertiesInProject(EnvDTE.Project project, string newRuleSetFilePath)
    {
        if (project.Kind is PrjKind.prjKindCSharpProject or
            PrjKind.prjKindVBProject)
        {
            var projectFullName = project.FullName;
            if (!string.IsNullOrWhiteSpace(projectFullName))
            {
                var projectDirectoryFullPath = Path.GetDirectoryName(project.FullName);
                foreach (EnvDTE.Configuration config in project.ConfigurationManager)
                {
                    UpdateCodeAnalysisRuleSetPropertyInConfiguration(config, newRuleSetFilePath, projectDirectoryFullPath);
                }
            }
        }
    }
 
    private static void UpdateCodeAnalysisRuleSetPropertyInConfiguration(EnvDTE.Configuration config, string newRuleSetFilePath, string projectDirectoryFullPath)
    {
        var properties = config.Properties;
        try
        {
            var codeAnalysisRuleSetFileProperty = properties?.Item("CodeAnalysisRuleSet");
 
            if (codeAnalysisRuleSetFileProperty != null)
            {
                var codeAnalysisRuleSetFileName = codeAnalysisRuleSetFileProperty.Value as string;
                if (!string.IsNullOrWhiteSpace(codeAnalysisRuleSetFileName))
                {
                    var codeAnalysisRuleSetFullPath = FileUtilities.ResolveRelativePath(codeAnalysisRuleSetFileName, projectDirectoryFullPath);
                    codeAnalysisRuleSetFullPath = FileUtilities.NormalizeAbsolutePath(codeAnalysisRuleSetFullPath);
                    var oldRuleSetFilePath = FileUtilities.NormalizeAbsolutePath(codeAnalysisRuleSetFullPath);
 
                    if (codeAnalysisRuleSetFullPath.Equals(oldRuleSetFilePath, StringComparison.OrdinalIgnoreCase))
                    {
                        var newRuleSetRelativePath = PathUtilities.GetRelativePath(projectDirectoryFullPath, newRuleSetFilePath);
                        codeAnalysisRuleSetFileProperty.Value = newRuleSetRelativePath;
                    }
                }
            }
        }
        catch (ArgumentException)
        {
            // Unfortunately the properties collection sometimes throws an ArgumentException
            // instead of returning null if the current configuration doesn't support CodeAnalysisRuleSet.
            // Ignore it and move on.
        }
    }
 
    private static int IndexOfFirstDocumentInProject(int projectIndex, int[] firstDocumentIndices)
    {
        if (projectIndex >= firstDocumentIndices.Length)
        {
            return firstDocumentIndices.Length;
        }
        else
        {
            return firstDocumentIndices[projectIndex];
        }
    }
}