dotnet / roslyn

The Roslyn .NET compiler provides C# and Visual Basic languages with rich code analysis APIs.
https://docs.microsoft.com/dotnet/csharp/roslyn-sdk/
MIT License
18.96k stars 4.03k forks source link

Incremental Source Generator - Not generating empty file, when last syntax node is deleted #61162

Closed lskyum closed 1 year ago

lskyum commented 2 years ago

The issue described here happens when when editing C# source with Visual Studio 2022. The generator seems to work, but it doesn't generate an empty source file, when the syntax provider should find zero syntax nodes. Instead it seems to use some cached syntax nodes.

Version Used: 4.0.1

Steps to Reproduce: Using Visual Studio 2022 (Version 17.1.6)

  1. Compile the generator ListMethodInvocationsGenerator (code below) and reference it in a test project. (For example a Class Library or Console App)
  2. Add some method invocations, for example Console.WriteLine (observe that InvokedMethods.g.cs now lists the method names)
  3. Delete all the method invocations again, and observe that the InvokedMethods.g.cs still contains something.
[Generator]
public class ListMethodInvocationsGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        IncrementalValueProvider<ImmutableArray<string>> invokedMethodsProvider = context.SyntaxProvider.CreateSyntaxProvider(
                predicate: (node, _) => node is InvocationExpressionSyntax,
                transform: (ctx, _) => (ctx.SemanticModel.GetSymbolInfo(ctx.Node).Symbol)?.Name ?? "<< method not found >>")
            .Where(m => m != null)
            .Collect();

        context.RegisterSourceOutput(invokedMethodsProvider, (SourceProductionContext spc, ImmutableArray<string> invokedMethods) =>
        {
            var src = new StringBuilder();
            foreach (var method in invokedMethods)
            {
                src.AppendLine("// " + method);
            }
            spc.AddSource("InvokedMethods.g.cs", src.ToString());
        });
    }
}

Expected Behavior: I would expect the file InvokedMethods.g.cs to be empty after deleting all method invocations in the code.

Actual Behavior: Instead InvokedMethods.g.cs contains one or more of the deleted method invocations. Note that when restarting Visual Studio, the generator produces an empty file, so maybe the is a bug in the caching?

chsienki commented 2 years ago

@jaredpar Seems like a good issue for @RikkiGibson to look into?

jaredpar commented 2 years ago

@jcouv can you take a look at this. Could be related to the bug that you just fixed.

ltrzesniewski commented 2 years ago

I've noticed a similar issue, and reduced it to a minimal repro:

[Generator(LanguageNames.CSharp)]
public class SourceGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        var data = context.SyntaxProvider.CreateSyntaxProvider(
            static (syntaxNode, _) => syntaxNode.IsKind(SyntaxKind.ClassDeclaration) && ((ClassDeclarationSyntax)syntaxNode).Modifiers.Any(SyntaxKind.PartialKeyword),
            static (ctx, _) => ((ClassDeclarationSyntax)ctx.Node).Identifier.ToString()
        ).Collect();

        context.RegisterSourceOutput(data, (ctx, items) =>
        {
            Console.Beep();
            ctx.AddSource("Test.g.cs", string.Join(Environment.NewLine, items.Select(i => $"// {i}")));
        });
    }
}

Target project:

public partial class Foo
{
}

public partial class Bar
{
}

This generator will beep every time the code is generated, which is very convenient.

Note the predicate of CreateSyntaxProvider returns true for all partial class nodes.

Here's the observed behavior:

This looks like an off-by-one error, given it only fails for the last class in the file.

tom-englert commented 2 years ago

Looks like the filtering of the CreateSyntaxProvider works fine - no code is generated when the last item is gone (it does not generate an empty file, but no file, so the generator is not involved at all) - but intellisense still seems to refer to the latest generated file, so that seems to be a caching issue.

Version 17.4.0 Preview 1.0: image

jjonescz commented 1 year ago

Fixed by https://github.com/dotnet/roslyn/pull/66992.