mrlacey/ConstVisualizer

VB.NET Support: Take Two

Closed this issue · 6 comments

Hi again. I tried to implement support for VB.NET, and please take into account that I'm not very experienced with C# nor with developing extensions (I developed very basic extensions but not at your level), but I think that I was very closer to achieve it... but I finally surrendered trying to figure out how to cross an obstacle...

I'll tell you all the steps I did to partially implement VB.NET support, and what makes me surrender with the hope that maybe you can finish this implementation:

  1. Install these NuGet packages in this order:
    https://www.nuget.org/packages/Microsoft.CodeAnalysis.VisualBasic/
    https://www.nuget.org/packages/Microsoft.CodeAnalysis.Common/

( The package description for Microsoft.CodeAnalysis.Common says to not install it manually, but I got some warnings in the IDE requesting me to install it. )

  1. Update all the Nuget Packages in the solution. (this will get rid of many warnings about old package references)

FROM NOW ON, ALL THE CHANGES ARE MADE IN THE ConstFinder.cs CLASS.

  1. Replace this line:
    if (formattedValue.StartsWith("nameof(")
    For this else:
    if (formattedValue.StartsWith("nameof(", StringComparison.OrdinalIgnoreCase)

  2. Replace this:

// Avoid parsing generated code.
// Reduces overhead (as there may be lots)
// Avoids assets included with Android projects.
if (filePath.ToLowerInvariant().EndsWith(".designer.cs")
 || filePath.ToLowerInvariant().EndsWith(".g.cs")
 || filePath.ToLowerInvariant().EndsWith(".g.i.cs"))
{
    return;
}

For this else:

// Avoid parsing generated code.
// Reduces overhead (as there may be lots)
// Avoids assets included with Android projects.
if (filePath.ToLowerInvariant().EndsWith(".designer.cs")
 || filePath.ToLowerInvariant().EndsWith(".designer.vb")
 || filePath.ToLowerInvariant().EndsWith(".g.cs")
 || filePath.ToLowerInvariant().EndsWith(".g.i.cs"))
{
    return;
}
  1. Replace IsConst(SyntaxNode node) method for this else:
public static bool IsConst(SyntaxNode node)
{
    bool isCSharpConstant = node.ChildTokens().Any(t => t.IsKind(Microsoft.CodeAnalysis.CSharp.SyntaxKind.ConstKeyword));
    bool isVBasicConstant = node.ChildTokens().Any(t => t.IsKind(Microsoft.CodeAnalysis.VisualBasic.SyntaxKind.ConstKeyword));
    return isCSharpConstant || isVBasicConstant;
}

( Something to improve, I know it can be simplified into a single LINQ query but I'm not sure about the operators usage in C#. )

  1. Replace SafeGetActiveDocumentAsync(EnvDTE.DTE dte) method for this else:

        internal static async Task<EnvDTE.Document> SafeGetActiveDocumentAsync(EnvDTE.DTE dte)
        {
            await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
            try
            {
                // Some document types (inc. .csproj) throw an error when try and get the ActiveDocument
                // "The parameter is incorrect. (Exception from HRESULT: 0x80070057 (E_INVALIDARG))"
                EnvDTE.Document doc = await Task.FromResult(dte?.ActiveDocument);
                return doc != null && (doc.Language == "CSharp" || doc.Language == "Basic") ? doc : null;
            }
            catch (Exception exc)
            {
                // Don't call ExceptionHelper.Log as this is really common--see above
                ////await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
                ////ExceptionHelper.Log(exc);

                System.Diagnostics.Debug.WriteLine(exc);

#if DEBUG
                System.Diagnostics.Debugger.Break();
#endif
            }

            return null;
        }

Now, the problem that I had and for which I spent many hours with no luck, it is at the ReloadConstsAsync() method, specifically in this line:

var documentId = workspace.CurrentSolution.GetDocumentIdsWithFilePath(activeDocument.FullName).FirstOrDefault();

I don't know why but the thing is that when the activedocument is a vb source-code file (I mean a module or class, a file that ends with .vb extension), the documentId var is null. I don't have this problem for C# source-code files.

I just can't get the document id for the active vb document, so this stops me from being able to test the changed I did and to see whether the extension finally decorates or not the constant member.

I hope you could do something with all this info.


Also another problem that I found, which is not really important but it is a problem that I noticed too (and that made it harder for me to debug things), is that the OutputPane class is not working for VB.NET solutions (the Output window does not contain the output for this extension, and so your sponsor messages are not shown). I need to create/open a C# solution if I want to see the output sent through the OutputPane. On the other hand, your extension 'Error Helper' works in the right way for VB.NET solutions in this meaning, the output and sponsor messages are shown.

Because I know you will have not much time, I could try to continue by myself implementing the VB.NET support and to do the proper pull request if you know what's happening with documentId and can provide me a fix for that; otherwise I can't continue. ( I even asked to ChatGPT for a fix, LOL. )

In any case, thanks for your time.

Cheers.

1.

I discovered that in order to display the output pane of ConstVisualizer extension, inside a VB solution, one needs to go to Tools > Options and expand the ConstVisualizer settings page. After doing that, the output pane is displayed showing the initial sponsor message.

2.

I also discovered that the issue with var documentId = workspace.CurrentSolution.GetDocumentIdsWithFilePath(activeDocument.FullName).FirstOrDefault(); returning null, I still don't know the exact reason but this problem only occurs when parsing a VB class within a C# solution (when adding a VB class through the "Add new item" option in the IDE). It does not occur when parsing a VB class within a VB solution.
(probably this same error would occur when trying to parse a C# class within a VB solution?).

3.

I also discovered that Microsoft.CodeAnalysis.Common package was not required at all, so it is safe to delete / uninstall it.


4.

Now the interesting part:

I finally managed to parse constants in VB classes / modules as demonstrated in this sample log:

LOG | foreach (VBasicSyntax.VariableDeclaratorSyntax vdec)
LOG | foreach (VBasicSyntax.VariableDeclaratorSyntax variable in fds.Declarators)
LOG | foreach (VBasicSyntax.ModifiedIdentifierSyntax name in variable.Names)
LOG | AddToKnownConstants TestConst Program "This is a constant string."
LOG | KnownConsts.Add((identifier, qualifier, formattedValue, filePath));
LOG |                  identifier: TestConst
LOG |                  qualifier: Program
LOG |                  formattedValue: "This is a constant string."
LOG |                  filePath: C:\ConsoleApp1\Program.vb

Test class parsed:

Class Program

    Const TestConst = "This is a constant string."

    Public Sub TestMethod()
        Console.WriteLine(TestConst)
        ' Console.WriteLine(NameOf(TestConst))
        ' Console.WriteLine($"{TestConst}")
    End Sub

End Class

I compared these results (identifier, qualifier, formattedValue, filePath) for what the extension gets when parsing C# classes, and I think it's all ok.


And finally, this is the ConstFinder.cs class with all additions applied :

// <copyright file="ConstFinder.cs" company="Matt Lacey">
// Copyright (c) Matt Lacey. All rights reserved.
// </copyright>

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
// using System.Windows.Controls;
using Microsoft.CodeAnalysis;
using Microsoft.VisualStudio.ComponentModelHost;
using Microsoft.VisualStudio.LanguageServices;
using Microsoft.VisualStudio.Shell;
using CsharpCodeAnalysis = Microsoft.CodeAnalysis.CSharp;
using CsharpSyntax = Microsoft.CodeAnalysis.CSharp.Syntax;
using VBasicCodeAnalysis = Microsoft.CodeAnalysis.VisualBasic;
using VBasicSyntax = Microsoft.CodeAnalysis.VisualBasic.Syntax;
using Task = System.Threading.Tasks.Task;

namespace ConstVisualizer
{
    internal static class ConstFinder
    {
        public static bool HasParsedSolution { get; private set; } = false;

        public static List<(string Key, string Qualification, string Value, string Source)> KnownConsts { get; } = new List<(string Key, string Qualification, string Value, string Source)>();

        public static string[] SearchValues
        {
            get
            {
                return KnownConsts.Select(c => c.Key).ToArray();
            }
        }

        public static async Task TryParseSolutionAsync(IComponentModel componentModel = null)
        {
            await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();

            Stopwatch timer = new Stopwatch();
            timer.Start();

            try
            {
                if (componentModel == null)
                {
                    componentModel = (IComponentModel)Package.GetGlobalService(typeof(SComponentModel));
                }

                ////OutputPane.Instance.WriteLine($"Parse step 1 duration: {timer.Elapsed}");

                Workspace workspace = (Workspace)componentModel.GetService<VisualStudioWorkspace>();

                if (workspace == null)
                {
                    return;
                }

                ////OutputPane.Instance.WriteLine($"Parse step 2 duration: {timer.Elapsed}");

                ProjectDependencyGraph projectGraph = workspace.CurrentSolution?.GetProjectDependencyGraph();

                if (projectGraph == null)
                {
                    return;
                }

                ////OutputPane.Instance.WriteLine($"Parse step 3 duration: {timer.Elapsed}");

                await Task.Yield();

                IEnumerable<ProjectId> projects = projectGraph.GetTopologicallySortedProjects();

                ////OutputPane.Instance.WriteLine($"Parse step 4 duration: {timer.Elapsed}");

                foreach (ProjectId projectId in projects)
                {
                    Compilation projectCompilation = await workspace.CurrentSolution?.GetProject(projectId).GetCompilationAsync();

                    ////OutputPane.Instance.WriteLine($"Parse loop step duration: {timer.Elapsed} ({projectId})");

                    if (projectCompilation != null)
                    {
                        foreach (SyntaxTree compiledTree in projectCompilation.SyntaxTrees)
                        {
                            await Task.Yield();
                            GetConstsFromSyntaxRoot(await compiledTree.GetRootAsync(), compiledTree.FilePath);
                        }
                    }
                }

                HasParsedSolution = true;
            }
            catch (Exception exc)
            {
                // Exceptions can happen in the above when a solution is modified before the package has finished loading :(
                ExceptionHelper.Log(exc);

                // Recovery from the above would be very difficult so easiest to prompt to trigger for reparsing later.
                HasParsedSolution = true;
            }
            finally
            {
                timer.Stop();

                await OutputPane.Instance.WriteAsync($"Parse total duration: {timer.Elapsed}");
            }
        }

        public static async Task ReloadConstsAsync()
        {
            await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();

            try
            {
                IComponentModel componentModel = (IComponentModel)Package.GetGlobalService(typeof(SComponentModel));

                if (ConstFinder.HasParsedSolution)
                {
                    EnvDTE.DTE dte = Package.GetGlobalService(typeof(EnvDTE.DTE)) as EnvDTE.DTE;

                    EnvDTE.Document activeDocument = await SafeGetActiveDocumentAsync(dte);

                    if (activeDocument != null)
                    {
                        Workspace workspace = (Workspace)componentModel.GetService<VisualStudioWorkspace>();
                        DocumentId documentId = workspace.CurrentSolution.GetDocumentIdsWithFilePath(activeDocument.FullName).FirstOrDefault();
                        if (documentId != null)
                        {
                            Document document = workspace.CurrentSolution.GetDocument(documentId);

                            await TrackConstsInDocumentAsync(document);
                        }
                    }
                }
                else
                {
                    await ConstFinder.TryParseSolutionAsync(componentModel);
                }
            }
            catch (Exception exc)
            {
                await OutputPane.Instance?.WriteAsync($"Error in {nameof(ReloadConstsAsync)}");
                ExceptionHelper.Log(exc);
            }
        }

        public static async Task<bool> TrackConstsInDocumentAsync(Document document)
        {
            if (document == null)
            {
                return false;
            }

            await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();

            System.Diagnostics.Debug.WriteLine(document.FilePath);

            if (document.FilePath == null
                || document.FilePath.Contains(".g.")
                || document.FilePath.Contains(".Designer."))
            {
                return false;
            }

            if (document.TryGetSyntaxTree(out SyntaxTree _))
            {
                SyntaxNode root = await document.GetSyntaxRootAsync();

                if (root == null)
                {
                    return false;
                }

                GetConstsFromSyntaxRoot(root, document.FilePath);
            }

            return true;
        }

        public static void GetConstsFromSyntaxRoot(SyntaxNode root, string filePath)
        {
            if (root == null || filePath == null)
            {
                return;
            }

            try
            {
                // Avoid parsing generated code.
                // Reduces overhead (as there may be lots)
                // Avoids assets included with Android projects.
                if (filePath.ToLowerInvariant().EndsWith(".designer.cs")
                 || filePath.ToLowerInvariant().EndsWith(".designer.vb")
                 || filePath.ToLowerInvariant().EndsWith(".g.cs")
                 || filePath.ToLowerInvariant().EndsWith(".g.i.cs"))
                {
                    return;
                }

                List<(string, string, string, string)> toRemove = new List<(string, string, string, string)>();

                foreach ((string Key, string Qualification, string Value, string Source) item in KnownConsts)
                {
                    if (item.Source == filePath)
                    {
                        toRemove.Add(item);
                    }
                }

                foreach ((string, string, string, string) item in toRemove)
                {
                    KnownConsts.Remove(item);
                }

                void AddToKnownConstants(string identifier, string qualifier, string value)
                {
                    if (value == null)
                    {
                        return;
                    }

                    string formattedValue = value.Replace("\\\"", "\"");

                    if (formattedValue.StartsWith("nameof(", StringComparison.OrdinalIgnoreCase)
                    && formattedValue.EndsWith(")"))
                    {
                        formattedValue = formattedValue.Substring(7, formattedValue.Length - 8);
                    }

                    KnownConsts.Add((identifier, qualifier, formattedValue, filePath));
                }

                foreach (CsharpSyntax.VariableDeclarationSyntax vdec in root.DescendantNodes().OfType<CsharpSyntax.VariableDeclarationSyntax>())
                {
                    if (vdec != null)
                    {
                        if (vdec.Parent != null && vdec.Parent is CsharpSyntax.MemberDeclarationSyntax dec)
                        {
                            if (IsConst(dec))
                            {
                                if (dec is CsharpSyntax.FieldDeclarationSyntax fds)
                                {
                                    string qualification = GetQualificationCSharp(fds);

                                    foreach (CsharpSyntax.VariableDeclaratorSyntax variable in fds.Declaration?.Variables)
                                    {
                                        AddToKnownConstants(
                                            variable.Identifier.Text,
                                            qualification,
                                            variable.Initializer?.Value?.ToString());
                                    }
                                }
                            }
                        }
                        else
                        {
                            if (vdec.Parent != null && vdec.Parent is CsharpSyntax.LocalDeclarationStatementSyntax ldec)
                            {
                                if (IsConst(ldec))
                                {
                                    if (vdec is CsharpSyntax.VariableDeclarationSyntax vds)
                                    {
                                        string qualification = GetQualificationCSharp(vds);

                                        foreach (CsharpSyntax.VariableDeclaratorSyntax variable in vds.Variables)
                                        {
                                            AddToKnownConstants(
                                                variable.Identifier.Text,
                                                qualification,
                                                variable.Initializer?.Value?.ToString());
                                        }
                                    }
                                }
                            }
                        }
                    }
                }

                foreach (VBasicSyntax.VariableDeclaratorSyntax vdec in root.DescendantNodes().OfType<VBasicSyntax.VariableDeclaratorSyntax>())
                {
                    if (vdec != null)
                    {
                        if (vdec.Parent != null && vdec.Parent is VBasicSyntax.DeclarationStatementSyntax dec)
                        {
                            if (IsConst(dec))
                            {
                                if (dec is VBasicSyntax.FieldDeclarationSyntax fds)
                                {
                                    string qualification = GetQualificationVisualBasic(fds);

                                    foreach (VBasicSyntax.VariableDeclaratorSyntax variable in fds.Declarators)
                                    {
                                        foreach (VBasicSyntax.ModifiedIdentifierSyntax name in variable.Names)
                                        {
                                            AddToKnownConstants(
                                                name.Identifier.Text,
                                                qualification,
                                                variable.Initializer?.Value?.ToString());
                                        }
                                    }
                                }
                            }
                        }
                        else
                        {
                            if (vdec.Parent != null && vdec.Parent is VBasicSyntax.LocalDeclarationStatementSyntax ldec)
                            {
                                if (IsConst(ldec))
                                {
                                    if (vdec is VBasicSyntax.VariableDeclaratorSyntax vds)
                                    {
                                        string qualification = GetQualificationVisualBasic(vds);
                                        foreach (VBasicSyntax.ModifiedIdentifierSyntax name in vds.Names)
                                        {
                                            AddToKnownConstants(
                                                name.Identifier.Text,
                                                qualification,
                                                vds.Initializer?.Value?.ToString());
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
            catch (Exception exc)
            {
                ThreadHelper.ThrowIfNotOnUIThread();
                ExceptionHelper.Log(exc);
            }
        }

        public static string GetQualificationCSharp(CsharpCodeAnalysis.CSharpSyntaxNode dec)
        {
            string result = string.Empty;
            SyntaxNode parent = dec.Parent;

            while (parent != null)
            {
                if (parent is CsharpSyntax.TypeDeclarationSyntax tds)
                {
                    result = $"{tds.Identifier.ValueText}.{result}";
                    parent = tds.Parent;
                }
                else if (parent is CsharpSyntax.NamespaceDeclarationSyntax nds)
                {
                    result = $"{nds.Name}.{result}";
                    parent = nds.Parent;
                }
                else
                {
                    parent = parent.Parent;
                }
            }

            return result.TrimEnd('.');
        }

        public static string GetQualificationVisualBasic(VBasicCodeAnalysis.VisualBasicSyntaxNode dec)
        {
            string result = string.Empty;
            SyntaxNode parent = dec.Parent;

            while (parent != null)
            {
                if (parent is VBasicSyntax.TypeBlockSyntax tbs)
                {
                    result = $"{tbs.BlockStatement.Identifier.ValueText}.{result}";
                    parent = tbs.Parent;
                }
                else if (parent is VBasicSyntax.NamespaceBlockSyntax nbs)
                {
                    result = $"{nbs.NamespaceStatement.Name}.{result}";
                    parent = nbs.Parent;
                }
                else
                {
                    parent = parent.Parent;
                }
            }

            return result.TrimEnd('.');
        }

        public static bool IsConst(SyntaxNode node)
        {
            return node.ChildTokens().Any(t => t.IsKind(CsharpCodeAnalysis.SyntaxKind.ConstKeyword) ||
                                               t.IsKind(VBasicCodeAnalysis.SyntaxKind.ConstKeyword));
        }

        internal static async Task<EnvDTE.Document> SafeGetActiveDocumentAsync(EnvDTE.DTE dte)
        {
            await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
            try
            {
                // Some document types (inc. .csproj) throw an error when try and get the ActiveDocument
                // "The parameter is incorrect. (Exception from HRESULT: 0x80070057 (E_INVALIDARG))"
                EnvDTE.Document doc = await Task.FromResult(dte?.ActiveDocument);
                return doc != null && (doc.Language == "CSharp" || doc.Language == "Basic") ? doc : null;
            }
            catch (Exception exc)
            {
                // Don't call ExceptionHelper.Log as this is really common--see above
                ////await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
                ////ExceptionHelper.Log(exc);

                System.Diagnostics.Debug.WriteLine(exc);

#if DEBUG
                System.Diagnostics.Debugger.Break();
#endif
            }

            return null;
        }

        internal static void Reset()
        {
            KnownConsts.Clear();
            HasParsedSolution = false;
        }

        internal static string GetDisplayText(string constName, string qualifier, string fileName)
        {
            (string Key, string Qualification, string Value, string Source) constsInThisFile =
                KnownConsts.Where(c => c.Source == fileName
                                    && c.Key == constName
                                    && c.Qualification.EndsWith(qualifier)).FirstOrDefault();

            if (!string.IsNullOrWhiteSpace(constsInThisFile.Value))
            {
                return constsInThisFile.Value;
            }

            (string _, string _, string value, string _) =
                KnownConsts.Where(c => c.Key == constName
                                    && c.Qualification.EndsWith(qualifier)).FirstOrDefault();

            if (!string.IsNullOrWhiteSpace(value))
            {
                return value;
            }

            return string.Empty;
        }
    }
}

5.

However, if I'm writing this and not doing a pull request it is because these additions were not enough to be able add the adornment. I'm not sure what is stopping the parsing logic to add the adornment, and this is getting too much work for my poor C# and Roslyn knowledge.

I've reviewed and tried things on the ResourceAdornmentManager.cs class but I can't figure out what I'm missing.

I definitely give up on this.

But I hope all this can be of help if in the future you consider to properly implement the VB support.

Have a nice day, Matt.

@ElektroStudios do you have a fork or a branch with the above changes in so I don't have to repeat what you've done?

I discovered that in order to display the output pane of ConstVisualizer extension, inside a VB solution, one needs to go to Tools > Options and expand the ConstVisualizer settings page. After doing that, the output pane is displayed showing the initial sponsor message.

The extension is only set to be loaded when used in a project that contains C# files. Opening the options window also forces it to be loaded.

@ElektroStudios do you have a fork or a branch with the above changes in so I don't have to repeat what you've done?

Here it is, I did the fork now for you: https://github.com/ElektroStudios/ConstVisualizer

I forgot to mention two - possibly irrelevant - aesthetic changes (refactors) that I made in the code of ConstFinder.cs class:

  1. Changed var to explicit types. I did this just for my convenience when working with C# code, it can be rolled back to var with two clicks as you know.
  2. Fixed name casing for tuple members. (eg. x.key -> x.Key)

Version 2.0.0 is now in the marketplace and includes VB.Net support