dotnet/roslyn

SemanticModel.GetSymbolInfo(GenericName) returns null (no candidates, NoneOperation)

Open

#82191 opened on Jan 28, 2026

View on GitHub
 (6 comments) (0 reactions) (0 assignees)C# (20,414 stars) (4,257 forks)batch import
Area-Compilershelp wanted

Description

SemanticModel.GetSymbolInfo(GenericName) returns null (no candidates, NoneOperation) when references are missing — regression/behavior difference between Roslyn 4.13.0 and 5.0.0 for method-group → delegate scenarios Summary When analyzing code where a generic method group is passed to a parameter of delegate type (e.g., propertyChanged: OnChanged<CircleEffect,bool> in a BindableProperty.CreateAttached call), Roslyn 4.13.0 used to provide an incomplete but usable candidate symbol via GetSymbolInfo. After upgrading to Roslyn 5.0.0, in the frequent case where some references are initially missing (first pass of static analysis, before the analyzer completes configuration), GetSymbolInfo returns neither symbol nor candidate and GetOperation yields NoneOperation, leaving no symbol or candidates for the method-group expression. This makes analyzers brittle in early passes: the same source code yields less information in 5.0.0 than it did in 4.13.0 when references are not yet fully available. Environment

Analyzer host: Console app / static analysis tool (no Visual Studio integration), first pass often missing external references (by design), then completed later. OS / .NET: .NET 8.0 (repros also on .NET 7), Windows and Linux. Roslyn packages compared:

Microsoft.CodeAnalysis / Microsoft.CodeAnalysis.CSharp 4.13.0 (works “better” in incomplete ref scenario) Microsoft.CodeAnalysis / Microsoft.CodeAnalysis.CSharp 5.0.0 (returns null/NoneOperation)

LanguageVersion: latest / preview (same behavior with default).

Repro steps Create a console project and paste the code below into Program.cs

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Reflection;

static class Program
{
    /*
     * commands to run with different Roslyn versions:
        # Roslyn 4.13.0
            dotnet run -c Release -p:RoslynVersion=4.13.0

        # Roslyn 5.0.0
        dotnet run -c Release -p:RoslynVersion=5.0.0
    */
    static int Main(string[] args)
    {
        Console.WriteLine($">>> Microsoft.CodeAnalysis: {typeof(Compilation).Assembly.GetName().Version}");
        Console.WriteLine($">>> Microsoft.CodeAnalysis.CSharp: {typeof(CSharpCompilation).Assembly.GetName().Version}");
        Console.WriteLine();

        // ---------------------------------------------------------------------
        // 1) Code sous test : stubs + ThemeEffects + un usage List<int> dédié
        // ---------------------------------------------------------------------
        var code = @"
using System;
using System.Collections.Generic;
using System.Linq;
/*
*/

namespace eShopOnContainers.Core.Effects
{
    using Xamarin.Forms;

    public static class ThemeEffects
    {
        public static readonly BindableProperty CircleProperty =
            BindableProperty.CreateAttached(
                ""Circle"",
                typeof(bool),
                typeof(ThemeEffects),
                false,
                propertyChanged: OnChanged<CircleEffect, bool>);

        public static bool GetCircle(BindableObject view)
            => false;

        public static void SetCircle(BindableObject view, bool circle)
        { }

        private static void OnChanged<TEffect, TProp>(BindableObject bindable, object oldValue, object newValue)
            where TEffect : Effect, new()
        {
            var view = bindable as View;
            if (view == null)
                return;

            if (EqualityComparer<TProp>.Equals(newValue, default(TProp)))
            {
                var toRemove = view.Effects.FirstOrDefault(e => e is TEffect);
                if (toRemove != null)
                    view.Effects.Remove(toRemove);
            }
            else
            {
                view.Effects.Add(new TEffect());
            }
        }

        private class CircleEffect : RoutingEffect
        {
            public CircleEffect() : base(""eShopOnContainers.CircleEffect"") { }
        }

        private static List<int> _probeList;
    }
}
";


        // ---------------------------------------------------------------------
        // 2) Compilation ad-hoc (références de base uniquement)
        // ---------------------------------------------------------------------
        var parseOptions = new CSharpParseOptions(languageVersion: LanguageVersion.Preview);
        var tree = CSharpSyntaxTree.ParseText(code, parseOptions);

        var refs = GetBasicReferences();

        var compilation = CSharpCompilation.Create(
            "SymbolProbeDemo",
            new[] { tree },
            refs,
            new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

        var model = compilation.GetSemanticModel(tree, ignoreAccessibility: true);
        var root = tree.GetCompilationUnitRoot();

        // ---------------------------------------------------------------------
        // 3) Ciblage du OnChanged<CircleEffect,bool> dans l'argument nommé
        // ---------------------------------------------------------------------
        var field = root.DescendantNodes().OfType<FieldDeclarationSyntax>()
            .First(f => f.Declaration.Variables.Any(v => v.Identifier.ValueText == "CircleProperty"));

        var variable = field.Declaration.Variables.First(v => v.Identifier.ValueText == "CircleProperty");
        var invocation = (InvocationExpressionSyntax)variable.Initializer!.Value;

        var propertyChangedArg = invocation.ArgumentList.Arguments
            .First(a => a.NameColon?.Name.Identifier.ValueText == "propertyChanged");

        var expr = propertyChangedArg.Expression;
        var genericName = expr as GenericNameSyntax;

        // ---------------------------------------------------------------------
        // 4) On dump d'abord le cas OnChanged<CircleEffect,bool>
        // ---------------------------------------------------------------------
        Console.WriteLine("=== CAS #1 : OnChanged<CircleEffect,bool> (method group -> delegate) ===");
        DumpAll(model, expr, genericName);

        // ---------------------------------------------------------------------
        // 5) On recherche aussi d'autres GenericNameSyntax (ex : List<int>)
        // ---------------------------------------------------------------------
        var allGenericNames = root.DescendantNodes().OfType<GenericNameSyntax>()
                                  .Distinct()
                                  .ToList();

        // On prend un 'List<int>' (type générique) si présent
        var listGeneric = allGenericNames.FirstOrDefault(n => n.Identifier.ValueText == "List");
        if (listGeneric != null)
        {
            Console.WriteLine();
            Console.WriteLine("=== CAS #2 : List<int> (type générique) ===");
            DumpAll(model, listGeneric, listGeneric);
        }


        // ---------------------------------------------------------------------
        // 6) Diagnostics
        // ---------------------------------------------------------------------
        Console.WriteLine("=== COMPILATION DIAGNOSTICS ===");
        foreach (var d in compilation.GetDiagnostics().OrderBy(d => d.Severity))
            Console.WriteLine(d);
        Console.WriteLine();

        // NOTE: Always return success; you compare outputs across Roslyn versions.
        return 0;
    }

    private static IEnumerable<MetadataReference> GetBasicReferences()
    {
        // Use a pragmatic set of core references from the current runtime.
        var assemblies = new[]
        {
            typeof(object).Assembly,                 // System.Private.CoreLib
            typeof(Console).Assembly,                // System.Console
            typeof(Enumerable).Assembly,             // System.Linq
            typeof(List<>).Assembly,                 // System.Collections
            Assembly.Load("System.Runtime"),         // System.Runtime (usually already loaded)
        };

        var distinct = assemblies
            .Where(a => !string.IsNullOrEmpty(a.Location))
            .Distinct()
            .Select(a => MetadataReference.CreateFromFile(a.Location));

        return distinct;
    }

    private static void DumpSymbolInfo(string title, SymbolInfo info)
    {
        Console.WriteLine($"=== {title} ===");
        if (info.Symbol is null)
        {
            Console.WriteLine("Symbol: <null>");
        }
        else
        {
            Console.WriteLine($"Symbol: {info.Symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)}  ({info.Symbol.Kind})");
        }

        Console.WriteLine($"CandidateReason: {info.CandidateReason}");
        if (info.CandidateSymbols.Length == 0)
        {
            Console.WriteLine("Candidates: <none>");
        }
        else
        {
            Console.WriteLine("Candidates:");
            foreach (var c in info.CandidateSymbols)
                Console.WriteLine($"  - {c.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)}  ({c.Kind})");
        }
        Console.WriteLine();
    }

    private static IMethodSymbol? TryGetMethodFromOperation(IOperation? op)
    {
        // In a success case, Roslyn exposes `IDelegateCreationOperation` → `IMethodReferenceOperation`.
        if (op is Microsoft.CodeAnalysis.Operations.IDelegateCreationOperation d &&
            d.Target is Microsoft.CodeAnalysis.Operations.IMethodReferenceOperation mr1)
        {
            return mr1.Method;
        }

        // Sometimes the operation is directly the method reference.
        if (op is Microsoft.CodeAnalysis.Operations.IMethodReferenceOperation mr2)
            return mr2.Method;

        return null;
    }


    private static void DumpAll(SemanticModel model, SyntaxNode exprOrName, GenericNameSyntax? g)
    {
        Console.WriteLine($"Noeud: {exprOrName.Kind()}  Text: {exprOrName}");
        Console.WriteLine($"GenericName? {(g != null)}");
        Console.WriteLine();

        // 1) GetSymbolInfo sur l'expression
        var infoExpr = model.GetSymbolInfo(exprOrName);
        DumpSymbolInfo("GetSymbolInfo(expr)", infoExpr);

        // 2) GetSymbolInfo sur le GenericName
        if (g != null)
        {
            var infoGen = model.GetSymbolInfo(g);
            DumpSymbolInfo("GetSymbolInfo(GenericName)", infoGen);
        }

        // 3) IOperation → IDelegateCreationOperation/IMethodReferenceOperation
        var op = model.GetOperation(exprOrName);
        Console.WriteLine("=== IOperation ===");
        Console.WriteLine(op is null ? "NoneOperation" : $"Kind: {op.Kind}");

        var methodFromOp = TryGetMethodFromOperation(op);
        if (methodFromOp != null)
        {
            Console.WriteLine("  Resolved via IOperation:");
            Console.WriteLine($"    {methodFromOp.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)}");
        }
        else
        {
            Console.WriteLine("  Aucune méthode résolue via IOperation.");
        }
        Console.WriteLine();
        
    }
}

Run twice: dotnet run -c Release -p:RoslynVersion=4.13.0 dotnet run -c Release -p:RoslynVersion=5.0.0

Expected behavior Continuing to get the candidates symbols

Contributor guide