Roslyn recommends incorrect symbols for a method - c#

Just came across that in VS2015 community(as well as VS2017 RC)
public class Class1
{
public static void Bar() { }
public static void Foo(int n)
{
Bar.
}
}
After Bar. the following auto-completions are recommended.
(parameter) int n
MemberwiseClone
Equals
GetHashCode
GetType
ToString
I think the latter 4 methods (from object) makes sense because I can add a variable/field named Bar later, but I don't see (parameter) int n and MemberwiseClone make any sense. By the way, Rider has the correct behavior here. To make sure it's not a IDE specific issue, I try to get completions from Roslyn via
namespace Microsoft.CodeAnalysis.Recommendations
{
public static class Recommender
{
public static Task<IEnumerable<ISymbol>> GetRecommendedSymbolsAtPositionAsync(
SemanticModel semanticModel,
int position,
Workspace workspace,
OptionSet options = null,
CancellationToken cancellationToken = default(CancellationToken));
}
}
My first attempt
var code = #"
namespace IntellisenseTest
{
public class Class1
{
public static void Bar() { }
public static void Foo(int n)
{
Bar.
}
}
}";
var intellisense = "Bar.";
var sourceText = SourceText.From(code);
var syntaxTree = CSharpSyntaxTree.ParseText(sourceText);
var compilation = CSharpCompilation.Create("IntellisenseTest").AddSyntaxTrees(syntaxTree);
var semanticModel = compilation.GetSemanticModel(syntaxTree);
var position = code.IndexOf(intellisense) + intellisense.Length + 1;
var workspace = MSBuildWorkspace.Create();
var symbols = await Recommender.GetRecommendedSymbolsAtPositionAsync(
semanticModel,
position,
workspace);
//print symbols
But I only get one recommended completion named n.
Then I create a real solution/project with the string code above on my hard drive.
Second attempt
var solutionPath = #"C:\workspace\IntellisenseTest\IntellisenseTest.sln";
var fileName = "Class1.cs";
var intellisense = "Bar.";
var workspace = MSBuildWorkspace.Create();
var solution = await workspace.OpenSolutionAsync(solutionPath);
var project = solution.Projects.First();
var document = project.Documents.Single(d => d.Name == fileName);
var text = await document.GetTextAsync();
var position = text.ToString().IndexOf(intellisense) + intellisense.Length + 1;
var syntaxTree = await document.GetSyntaxTreeAsync();
var compilation = await project.GetCompilationAsync();
var semanticModel = compilation.GetSemanticModel(syntaxTree);
var symbols = await Recommender.GetRecommendedSymbolsAtPositionAsync(
semanticModel,
position,
workspace);
//print symbols I got
n
ToString
Equals
GetHashCode
GetType
MemberwiseClone
Q1: What's the difference between the two implementations above? IMO I should get the same result.
Q2: Why Roslyn recommends these completions? IMO n and MemberwiseClone don't make any sense.

A2: All of the suggested completions are System.Object methods, from which all types in C# derive from.
A1: You don't have a reference to the assembly that defines System.Object in your manually constructed msbuild workspace. The one with the .sln probably has it.
In your first attempt, add it to the project
project = project.AddMetadataReference(
MetadataReference.CreateFromFile(typeof(System.Object).Assembly.Location));
or the compilation
compilation = compilation.AddReferences(
MetadataReference.CreateFromFile(typeof(System.Object).Assembly.Location));
and you should get identical results.

Related

How to use AvroSerializer without a schema registry

I am trying to write a unit test that verifies that adding a new property to an Avro schema is backwards compatible.
First I took the Avro generated .cs model and saved it as MyModelOld.cs and renamed the class inside to MyModelOld.
Then I re-ran Avro gen against the avsc file with the new property.
What I'm trying to do is this:
var schemaRegistry = -> something that doesn't require a running docker image <-;
var deserializerOld = new AvroDeserializer<MyModelOld>(schemaRegistry);
var serializerNew = new AvroSerializer<MyModel>(schemaRegistry);
var myModel = new MyModel() {...};
var myModelBytes = await serializerNew.SerializeAsync(myModel, new());
var myModelOld = await deserializerOld.DeserializeAsync(myModelBytes, false, new());
// Check properties...
Then I was going to go the opposite direction and check that the new property uses the specified default value.
The problem I'm having is what to use for the schema registry. I don't want to have a docker image running for these tests because I don't think it shouldn't be necessary.
I've tried a mock of ISchemaRegistry, but it appears to need a fully functional class in order for serialize/deserialize to work.
I could probably walk through the logic for CachedSchemaRegistryClient and try to munge it to work, but before I do so I'd like to find out if someone knows of an ISchemaRegistry implementaion that would work for my use case.
Has anyone tried to write tests to validate backwards compatibility of Avro schema updates?
If so, how did you go about doing so?
Thanks.
I ended up doing it this way:
private ISchemaRegistryClient NewTestRegistry(string topic)
{
// Code to mock SchemaRegistry taken from:
// https://github.com/confluentinc/confluent-kafka-dotnet/blob/master/test/Confluent.SchemaRegistry.Serdes.UnitTests/SerializeDeserialize.cs
Dictionary<string, int> store = new Dictionary<string, int>();
var schemaRegistryMock = new Mock<ISchemaRegistryClient>();
#pragma warning disable CS0618 // Type or member is obsolete
schemaRegistryMock.Setup(x => x.ConstructValueSubjectName(topic, It.IsAny<string>()))
.Returns($"{topic}-value");
schemaRegistryMock.Setup(x => x.RegisterSchemaAsync($"{topic}-value", It.IsAny<string>(), It.IsAny<bool>()))
.ReturnsAsync((string topic, string schema, bool normalize) =>
store.TryGetValue(schema, out int id) ? id : store[schema] = store.Count + 1
);
#pragma warning restore CS0618 // Type or member is obsolete
schemaRegistryMock.Setup(x => x.GetSchemaAsync(It.IsAny<int>(), It.IsAny<string>()))
.ReturnsAsync((int id, string format) =>
new Schema(store.Where(x => x.Value == id).First().Key, null, SchemaType.Avro)
);
return schemaRegistryMock.Object;
}
[TestMethod]
public async Task BackwardsCompatible()
{
var topic = "MyCoolTopic";
var schemaRegistry = NewTestRegistry(topic);
var context = new SerializationContext(MessageComponentType.Value, topic);
var deserializerOld = new AvroDeserializer<MyModelOld>(schemaRegistry);
var serializerNew = new AvroSerializer<MyModel>(schemaRegistry);
var myModel = new MyModel() { /* Set properties */};
var myModelBytes = await serializerNew.SerializeAsync(myModel, context);
var myModelOld = await deserializerOld.DeserializeAsync(myModelBytes, false, context);
// Check properties...
}
[TestMethod]
public async Task ForwardsCompatible()
{
// Similar to the above test.
}
If you want to test schemas, you don't need Kafka-related serializers; just use raw Avro C# library.
Alternatively, look at the existing tests
var config = new SchemaRegistryConfig { Url = "irrelevanthost:8081" };
var src = new CachedSchemaRegistryClient(config);
Assert...(src... );

How can I make the CompletionService aware of other documents in the project?

I'm building an application that allows users to define, edit and execute C# scripts.
The definition consists of a method name, an array of parameter names and the method's inner code, e.g:
Name: Script1
Parameter Names: arg1, arg2
Code: return $"Arg1: {arg1}, Arg2: {arg2}";
Based on this definition the following code can be generated:
public static object Script1(object arg1, object arg2)
{
return $"Arg1: {arg1}, Arg2: {arg2}";
}
I've successfully set up an AdhocWorkspace and a Project like this:
private readonly CSharpCompilationOptions _options = new CSharpCompilationOptions(OutputKind.ConsoleApplication,
moduleName: "MyModule",
mainTypeName: "MyMainType",
scriptClassName: "MyScriptClass"
)
.WithUsings("System");
private readonly MetadataReference[] _references = {
MetadataReference.CreateFromFile(typeof(object).Assembly.Location)
};
private void InitializeWorkspaceAndProject(out AdhocWorkspace ws, out ProjectId projectId)
{
var assemblies = new[]
{
Assembly.Load("Microsoft.CodeAnalysis"),
Assembly.Load("Microsoft.CodeAnalysis.CSharp"),
Assembly.Load("Microsoft.CodeAnalysis.Features"),
Assembly.Load("Microsoft.CodeAnalysis.CSharp.Features")
};
var partTypes = MefHostServices.DefaultAssemblies.Concat(assemblies)
.Distinct()
.SelectMany(x => x.GetTypes())
.ToArray();
var compositionContext = new ContainerConfiguration()
.WithParts(partTypes)
.CreateContainer();
var host = MefHostServices.Create(compositionContext);
ws = new AdhocWorkspace(host);
var projectInfo = ProjectInfo.Create(
ProjectId.CreateNewId(),
VersionStamp.Create(),
"MyProject",
"MyProject",
LanguageNames.CSharp,
compilationOptions: _options, parseOptions: new CSharpParseOptions(LanguageVersion.CSharp7_3, DocumentationMode.None, SourceCodeKind.Script)).
WithMetadataReferences(_references);
projectId = ws.AddProject(projectInfo).Id;
}
And I can create documents like this:
var document = _workspace.AddDocument(_projectId, "MyFile.cs", SourceText.From(code)).WithSourceCodeKind(SourceCodeKind.Script);
For each script the user defines, I'm currently creating a separate Document.
Executing the code works as well, using the following methods:
First, to compile all documents:
public async Task<Compilation> GetCompilations(params Document[] documents)
{
var treeTasks = documents.Select(async (d) => await d.GetSyntaxTreeAsync());
var trees = await Task.WhenAll(treeTasks);
return CSharpCompilation.Create("MyAssembly", trees, _references, _options);
}
Then, to create an assembly out of the compilation:
public Assembly GetAssembly(Compilation compilation)
{
try
{
using (MemoryStream ms = new MemoryStream())
{
var emitResult = compilation.Emit(ms);
if (!emitResult.Success)
{
foreach (Diagnostic diagnostic in emitResult.Diagnostics)
{
Console.Error.WriteLine("{0}: {1}", diagnostic.Id, diagnostic.GetMessage());
}
}
else
{
ms.Seek(0, SeekOrigin.Begin);
var buffer = ms.GetBuffer();
var assembly = Assembly.Load(buffer);
return assembly;
}
return null;
}
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
And, finally, to execute the script:
public async Task<object> Execute(string method, object[] params)
{
var compilation = await GetCompilations(_documents);
var a = GetAssembly(compilation);
try
{
Type t = a.GetTypes().First();
var res = t.GetMethod(method)?.Invoke(null, params);
return res;
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
So far, so good. This allows users to define scripts that can all each other.
For editing I would like to offer code completion and am currently doing this:
public async Task<CompletionList> GetCompletionList(Document doc, string code, int offset)
{
var newDoc = doc.WithText(SourceText.From(code));
_workspace.TryApplyChanges(newDoc.Project.Solution);
var completionService = CompletionService.GetService(newDoc);
return await completionService.GetCompletionsAsync(newDoc, offset);
}
NOTE: The code snippet above was updated to fix the error Jason mentioned in his answer regarding the use of doc and document. That was, indeed, due to the fact that the code shown here was extracted (and thereby modified) from my actual application code. You can find the original erroneous snippet I posted in his answer and, also, further below a new version which addresses the actual issue that was causing my problems.
The problem now is that GetCompletionsAsync is only aware of definitions within the same Document and the references used when creating the workspace and project, but it apparently does not have any reference to the other documents within the same project. So the CompletionList does not contain symbols for the other user scripts.
This seems strange, because in a "live" Visual Studio project, of course, all files within a project are aware of each other.
What am I missing? Are the project and/or workspace set up incorrectly? Is there another way of calling the CompletionService? Are the generated document codes missing something, like a common namespace?
My last resort would be to merge all methods generated from users' script definitions into one file - is there another way?
FYI, here are a few useful links that helped me get this far:
https://www.strathweb.com/2018/12/using-roslyn-c-completion-service-programmatically/
Roslyn throws The language 'C#' is not supported
Roslyn service is null
Updating AdHocWorkspace is slow
Roslyn: is it possible to pass variables to documents (with SourceCodeKind.Script)
UPDATE 1:
Thanks to Jason's answer I've updated the GetCompletionList method as follows:
public async Task<CompletionList> GetCompletionList(Document doc, string code, int offset)
{
var docId = doc.Id;
var newDoc = doc.WithText(SourceText.From(code));
_workspace.TryApplyChanges(newDoc.Project.Solution);
var currentDoc = _workspace.CurrentSolution.GetDocument(docId);
var completionService = CompletionService.GetService(currentDoc);
return await completionService.GetCompletionsAsync(currentDoc, offset);
}
As Jason pointed out, the cardinal error was not fully taking the immutability of the project and it's documents into account. The Document instance I need for calling CompletionService.GetService(doc) must be the actual instance contained in the current solution - and not the instance created by doc.WithText(...), because that instance has no knowledge of anything.
By storing the DocumentId of the original instance and using it to retrieve the updated instance within the solution, currentDoc, after applying the changes, the completion service can (as in "live" solutions) reference the other documents.
UPDATE 2: In my original question the code snippets used SourceCodeKind.Regular, but - at least in this case - it must be SourceCodeKind.Script, because otherwise the compiler will complain that top-level static methods are not allowed (when using C# 7.3). I've now updated the post.
So one thing looks a bit fishy here:
public async Task<CompletionList> GetCompletionList(Document doc, string code, int offset)
{
var newDoc = document.WithText(SourceText.From(code));
_workspace.TryApplyChanges(newDoc.Project.Solution);
var completionService = CompletionService.GetService(newDoc);
return await completionService.GetCompletionsAsync(document, offset);
}
(Note: your parameter name is "doc" but you're using "document" so I'm guessing this code is something you pared down from the full example. But just wanted to call that out since you might have introduced errors while doing that.)
So main fishy bit: Roslyn Documents are snapshots; a document is a pointer within the entire snapshot of the entire solution. Your "newDoc" is a new document with the text that you've substituted, and you're updating thew workspace to contain that. You're however still handing in the original document to GetCompletionsAsync, which means you're still asking for the old document in that case, which might have stale code. Furthermore, because it's all a snapshot, the changes made to the main workspace by calling TryApplyChanges won't in any way be reflected in your new document objects. So what I'm guessing might be happening here is you're passing in a Document object that doesn't actually have all the text documents updated at once, but most of them are still empty or something similar.

Roslyn Find All assignments made to a Field

I'm trying write an analyzer, I need to find all assignments made to a field using Roslyn.
private async static Task<bool> VariableDoesNotMutate(SyntaxNodeAnalysisContext context, VariableDeclaratorSyntax firstVariable)
{
var variableSymbol = context.SemanticModel.GetDeclaredSymbol(firstVariable);
var references = await SymbolFinder.FindReferencesAsync(variableSymbol, context.GetSolution());
foreach (var reference in references)
{
//How do I check for assignment?
}
//need to filter by assignments
return references.Count() > 1;
}
I heard using the symbolFinder was correct, but I'm not sure how to do this.
The symbol finder requires a solution, which I only have access to through a hack, so I'm assuming there is another way to do this.
Issues:
When I try to Find all references to a variable only the Declaration is returned an I do not find any other references how can I fix this?
Once I have a reference How can I determine if it's an assignment?
I Couldn't find any references originally because my document was not in the correct solution. Analyzer's don't provide you a way to get to the Solution and as #SLaks say's for preformance reasons you should not do this:
To Get the Solution You need to reflect into the AnalyzerOptions I've written an answer how to do so here
However, If you need to you can do this Get The Equivalent Symbol in the Solution and work off of that. This is potentially dangerous
static async Task<ISymbol> GetEquivalentSymbol(SyntaxNodeAnalysisContext context, FieldDeclarationSyntax field, CancellationToken cancellationToken)
{
var solution = context.GetSolution();
var classDeclaration = field.Ancestors().OfType<ClassDeclarationSyntax>().FirstOrDefault();
var namespaceDeclaration = field.Ancestors().OfType<NamespaceDeclarationSyntax>().FirstOrDefault();
var className = classDeclaration?.Identifier.ValueText;
var initialVariable = field.Declaration.Variables.FirstOrDefault();
foreach (var project in solution.Projects)
{
foreach (var document in project.Documents)
{
var semanticModel = await document.GetSemanticModelAsync(cancellationToken);
var root = await document.GetSyntaxRootAsync(cancellationToken);
if (null != namespaceDeclaration)
{
var namespaceNode = root.DescendantNodes().OfType<NamespaceDeclarationSyntax>()
.FirstOrDefault(node => node.Name.ToString() == namespaceDeclaration.Name.ToString());
if (null == namespaceNode)
{
continue;
}
}
var classNode = root.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.FirstOrDefault(node => node.Identifier.ValueText == className);
var desiredField = classNode?.DescendantNodes().OfType<FieldDeclarationSyntax>()
.FirstOrDefault(x => x.Declaration.Variables.First().Identifier.ValueText == initialVariable.Identifier.ValueText);
if (desiredField == null)
{
continue;
}
var symbol = semanticModel.GetDeclaredSymbol(desiredField.Declaration.Variables.FirstOrDefault());
return symbol;
}
}
return null;
}
Then you can get the references like so:
var equivalentSymbol = await GetEquivalentSymbol(context, field, cancellationToken);
var references = await SymbolFinder.FindReferencesAsync(equivalentSymbol, context.GetSolution(), cancellationToken);

WF 4.5 Compile CSharpValue<T> error. What is the correct method?

I've been uniting testing two simple examples of compiling CSharpValue activities. One works and the other doesn't I can't figure out why. If someone could point out the issue and optionally a change to correct it if possible.
Details:
The first unit test works SequenceActivityCompile() the second CodeActivityCompile fails with a NotSupportedException (Expression Activity type CSharpValue requires compilation in order to run. Please ensure that the workflow has been compiled.)
I heard somewhere this can be related to ForImplementation but CodeActivityCompile has the same error whether its value is true or false.
This example is a basic adaption of the Microsoft example at: https://msdn.microsoft.com/en-us/library/jj591618(v=vs.110).aspx
This example blog post discussing compiling C# expressions in WF 4+ at length. If anyone reaching this question needs a basic introduction to the topic:
http://blogs.msdn.com/b/tilovell/archive/2012/05/25/wf4-5-using-csharpvalue-lt-t-gt-and-csharpreference-lt-t-gt-in-net-4-5-compiling-expressions-and-changes-in-visual-studio-generated-xaml.aspx
Related Code:
[TestMethod]
public void SequenceActivityCompile()
{
Activity sequence = new Sequence
{
Activities = { new CSharpValue<string>("\"Hello World \"") }
};
CompileExpressions(sequence);
var result = WorkflowInvoker.Invoke(sequence);
}
[TestMethod]
public void CodeActivityCompile()
{
var code = new CSharpValue<String>("\"Hello World\"");
CompileExpressions(code);
var result = WorkflowInvoker.Invoke(code);
}
void CompileExpressions(Activity activity)
{
// activityName is the Namespace.Type of the activity that contains the
// C# expressions.
string activityName = activity.GetType().ToString();
// Split activityName into Namespace and Type.Append _CompiledExpressionRoot to the type name
// to represent the new type that represents the compiled expressions.
// Take everything after the last . for the type name.
//string activityType = activityName.Split('.').Last() + "_CompiledExpressionRoot";
string activityType = "TestType";
// Take everything before the last . for the namespace.
//string activityNamespace = string.Join(".", activityName.Split('.').Reverse().Skip(1).Reverse());
string activityNamespace = "TestSpace";
// Create a TextExpressionCompilerSettings.
TextExpressionCompilerSettings settings = new TextExpressionCompilerSettings
{
Activity = activity,
Language = "C#",
ActivityName = activityType,
ActivityNamespace = activityNamespace,
RootNamespace = null,
GenerateAsPartialClass = false,
AlwaysGenerateSource = true,
ForImplementation = false
};
// Compile the C# expression.
TextExpressionCompilerResults results =
new TextExpressionCompiler(settings).Compile();
// Any compilation errors are contained in the CompilerMessages.
if (results.HasErrors)
{
throw new Exception("Compilation failed.");
}
// Create an instance of the new compiled expression type.
ICompiledExpressionRoot compiledExpressionRoot =
Activator.CreateInstance(results.ResultType,
new object[] { activity }) as ICompiledExpressionRoot;
// Attach it to the activity.
System.Activities.Expressions.CompiledExpressionInvoker.SetCompiledExpressionRoot(
activity, compiledExpressionRoot);
}

How to access and modify a node with code fix provider from another node

I have a situation where I need to modify a situation where a user writes this kind of code :
bool SomeMethod(object obj)
{
if(obj == null)
return false;
return true;
}
To the following code :
bool SomeMethod(object obj)
{
return obj == null;
}
Currently, I have built an analyzer that works. I'll put the code below. Basically, the analyzer looks for if statements and verifies if the only statement of the if is a return statement.Not only that, it verifies that also, the next statement inside the method's declaration is a return statement.
The code fix provider looks for the ifStatement's condition and creates a new return statement using that condition. What I'm trying to do after replacing the if statement by the return statement, is to remove the second return statement. At this, I'm failing without knowing how.
At first, when the node's been replaced, I create a new root, because they can't be modify like a string, it's about the same thing. I try to delete the node, but this instruction is being ignored for some reason. And I've debugged it, when I access the next node(ReturnStatement), it's not null.
I guess my question is basically, how can I build a code fix provider which can modify a node without being "linked" on it.
Here's the code for the analyzer
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using System.Collections.Immutable;
using System.Linq;
namespace RefactoringEssentials.CSharp.Diagnostics
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class RewriteIfReturnToReturnAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor descriptor = new DiagnosticDescriptor(
CSharpDiagnosticIDs.RewriteIfReturnToReturnAnalyzerID,
GettextCatalog.GetString("Convert 'if...return' to 'return'"),
GettextCatalog.GetString("Convert to 'return' statement"),
DiagnosticAnalyzerCategories.Opportunities,
DiagnosticSeverity.Info,
isEnabledByDefault: true,
helpLinkUri: HelpLink.CreateFor(CSharpDiagnosticIDs.RewriteIfReturnToReturnAnalyzerID)
);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(descriptor);
public override void Initialize(AnalysisContext context)
{
context.RegisterSyntaxNodeAction(
(nodeContext) =>
{
Diagnostic diagnostic;
if (TryGetDiagnostic(nodeContext, out diagnostic))
{
nodeContext.ReportDiagnostic(diagnostic);
}
}, SyntaxKind.IfStatement);
}
private static bool TryGetDiagnostic(SyntaxNodeAnalysisContext nodeContext, out Diagnostic diagnostic)
{
diagnostic = default(Diagnostic);
if (nodeContext.IsFromGeneratedCode())
return false;
var node = nodeContext.Node as IfStatementSyntax;
var methodBody = node?.Parent as BlockSyntax;
var ifStatementIndex = methodBody?.Statements.IndexOf(node);
if (node?.Statement is ReturnStatementSyntax &&
methodBody?.Statements.ElementAt(ifStatementIndex.Value + 1) is ReturnStatementSyntax)
{
diagnostic = Diagnostic.Create(descriptor, node.GetLocation());
return true;
}
return false;
}
}
}
Here's the code for the Code fix provider
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Formatting;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
namespace RefactoringEssentials.CSharp.Diagnostics
{
[ExportCodeFixProvider(LanguageNames.CSharp), System.Composition.Shared]
public class RewriteIfReturnToReturnCodeFixProvider : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds
{
get
{
return ImmutableArray.Create(CSharpDiagnosticIDs.RewriteIfReturnToReturnAnalyzerID);
}
}
public override FixAllProvider GetFixAllProvider()
{
return WellKnownFixAllProviders.BatchFixer;
}
public async override Task RegisterCodeFixesAsync(CodeFixContext context)
{
var document = context.Document;
var cancellationToken = context.CancellationToken;
var span = context.Span;
var diagnostics = context.Diagnostics;
var root = await document.GetSyntaxRootAsync(cancellationToken);
var diagnostic = diagnostics.First();
var node = root.FindNode(context.Span);
if (node == null)
return;
context.RegisterCodeFix(
CodeActionFactory.Create(node.Span, diagnostic.Severity, "Convert to 'return' statement", token =>
{
var statementCondition = (node as IfStatementSyntax)?.Condition;
var newReturn = SyntaxFactory.ReturnStatement(SyntaxFactory.Token(SyntaxKind.ReturnKeyword),
statementCondition, SyntaxFactory.Token(SyntaxKind.SemicolonToken));
var newRoot = root.ReplaceNode(node as IfStatementSyntax, newReturn
.WithLeadingTrivia(node.GetLeadingTrivia())
.WithAdditionalAnnotations(Formatter.Annotation));
var block = node.Parent as BlockSyntax;
if (block == null)
return null;
//This code (starting from here) does not do what I'd like to do ...
var returnStatementAfterIfStatementIndex = block.Statements.IndexOf(node as IfStatementSyntax) + 1;
var returnStatementToBeEliminated = block.Statements.ElementAt(returnStatementAfterIfStatementIndex) as ReturnStatementSyntax;
var secondNewRoot = newRoot.RemoveNode(returnStatementToBeEliminated, SyntaxRemoveOptions.KeepNoTrivia);
return Task.FromResult(document.WithSyntaxRoot(secondNewRoot));
}), diagnostic);
}
}
}
And finally, this is my NUnit test :
[Test]
public void When_Retrurn_Statement_Corrected()
{
var input = #"
class TestClass
{
bool TestMethod (object obj)
{
$if (obj != null)
return true;$
return false;
}
}";
var output = #"
class TestClass
{
bool TestMethod (object obj)
{
return obj!= null;
}
}";
Analyze<RewriteIfReturnToReturnAnalyzer>(input, output);
}
I believe the problem is probably this line:
var block = node.Parent as BlockSyntax;
You're using the node from the original tree, with a .Parent also from the original tree (not the one with the updated newReturn).
Then, it eventually calculates returnStatementToBeEliminated using this old tree that's no longer up-to-date, so when you call var secondNewRoot = newRoot.RemoveNode(returnStatementToBeEliminated, SyntaxRemoveOptions.KeepNoTrivia);, nothing happens because newRoot does not contain returnStatementToBeEliminated.
So, you basically want to use the equivalent of node.Parent, but the version that lives under newRoot. The low-level tool we use for this is called a SyntaxAnnotation, and these have the property that they track forward between tree edits. You can add a specific annotation to the node.Parent before making any edits, then make your edits, and then ask the newRoot to find the node with your annotation.
You can track nodes manually like this, or you can use the SyntaxEditor class, which abstracts the Annotations part away into simpler methods like TrackNode (there's a few other nice features in SyntaxEditor that you may want to check out).
For this issue, I was refer to the following post :
How do I create a new root by adding and removing nodes retrieved from the old root?
This post showed this class called DocumentEditor, which lets a user modify a document like he wants even though it's suppose to be immutable. The previous issue was that after deleting a node, if I was referring something that had a connection to that node, the relation would have disappear and I would be able to stuff.
Basically, the documentation comment says that this class is "an editor for making changes to a document's syntax tree."
After you're done modifying that document, you need to create a new document and return it as a Task in the code fix provider.
To solve this issue I had with my code fix provider, I used the following code :
context.RegisterCodeFix(CodeAction.Create("Convert to 'return' statement", async token =>
{
var editor = await DocumentEditor.CreateAsync(document, cancellationToken);
var statementCondition = (node as IfStatementSyntax)?.Condition;
var newReturn = SyntaxFactory.ReturnStatement(SyntaxFactory.Token(SyntaxKind.ReturnKeyword),
statementCondition, SyntaxFactory.Token(SyntaxKind.SemicolonToken));
editor.ReplaceNode(node as IfStatementSyntax, newReturn
.WithLeadingTrivia(node.GetLeadingTrivia())
.WithAdditionalAnnotations(Formatter.Annotation));
var block = node.Parent as BlockSyntax;
if (block == null)
return null;
var returnStatementAfterIfStatementIndex = block.Statements.IndexOf(node as IfStatementSyntax) + 1;
var returnStatementToBeEliminated = block.Statements.ElementAt(returnStatementAfterIfStatementIndex) as ReturnStatementSyntax;
editor.RemoveNode(returnStatementToBeEliminated);
var newDocument = editor.GetChangedDocument();
return newDocument;
}, string.Empty), diagnostic);
It was really simple to solve my issue thanks to that class.

Categories