Roslyn Code Action: How to check if preview or real execution? - c#

I am currently experimenting with Roslyn and Code Actions, more specific Code Refactorings.
It feels kind of easy, but I have a difficulty I cannot solve.
Code actions are executed once against a dummy workspace as a "preview" option, so that you can see the actual changes before you click the action and execute it against the real workspace.
Now I am dealing with some things Roslyn can't really do (yet), so I am doing some changes via EnvDTE. I know, it's bad, but I couldn't find another way.
So the issue here is:
When I hover over my code action, the code gets executed as preview, and it should NOT do the EnvDTE changes. Those should only be done when the real execute happens.
I have created a gist with a small example of my code. It doesn't really makes sense, but should show what I want to achieve. Do some modifications via roslyn, then do something via EnvDTE, like changing Cursor position. But of course only on the real execution.
The relevant part for those who can't click the gist:
public sealed override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(continueOnCapturedContext: false);
var node = root.FindNode(context.Span);
var dec = node as MethodDeclarationSyntax;
if (dec == null)
return;
context.RegisterRefactoring(CodeAction.Create("MyAction", c => DoMyAction(context.Document, dec, c)));
}
private static async Task<Solution> DoMyAction(Document document, MethodDeclarationSyntax method, CancellationToken cancellationToken)
{
var syntaxTree = await document.GetSyntaxTreeAsync(cancellationToken);
var root = await syntaxTree.GetRootAsync(cancellationToken);
// some - for the question irrelevant - roslyn changes, like:
document = document.WithSyntaxRoot(root.ReplaceNode(method, method.WithIdentifier(SyntaxFactory.ParseToken(method.Identifier.Text + "Suffix"))));
// now the DTE magic
var preview = false; // <--- TODO: How to check if I am in preview here?
if (!preview)
{
var requestedItem = DTE.Solution.FindProjectItem(document.FilePath);
var window = requestedItem.Open(Constants.vsViewKindCode);
window.Activate();
var position = method.Identifier.GetLocation().GetLineSpan().EndLinePosition;
var textSelection = (TextSelection) window.Document.Selection;
textSelection.MoveTo(position.Line, position.Character);
}
return document.Project.Solution;
}

You can choose to override ComputePreviewOperationsAsync to have different behavior for Previews from regular code.

I've found the solution to my problem by digging deeper and trial and error after Keven Pilch's answer. He bumped me in the right direction.
The solution was to override both the ComputePreviewOperationsAsync and the GetChangedSolutionAsync methods in my own CodeAction.
Here the relevant part of my CustomCodeAction, or full gist here.
private readonly Func<CancellationToken, bool, Task<Solution>> _createChangedSolution;
protected override async Task<IEnumerable<CodeActionOperation>> ComputePreviewOperationsAsync(CancellationToken cancellationToken)
{
const bool isPreview = true;
// Content copied from http://sourceroslyn.io/#Microsoft.CodeAnalysis.Workspaces/CodeActions/CodeAction.cs,81b0a0866b894b0e,references
var changedSolution = await GetChangedSolutionWithPreviewAsync(cancellationToken, isPreview).ConfigureAwait(false);
if (changedSolution == null)
return null;
return new CodeActionOperation[] { new ApplyChangesOperation(changedSolution) };
}
protected override Task<Solution> GetChangedSolutionAsync(CancellationToken cancellationToken)
{
const bool isPreview = false;
return GetChangedSolutionWithPreviewAsync(cancellationToken, isPreview);
}
protected virtual Task<Solution> GetChangedSolutionWithPreviewAsync(CancellationToken cancellationToken, bool isPreview)
{
return _createChangedSolution(cancellationToken, isPreview);
}
The code to create the action stays quite similar, except the bool is added and I can check against it then:
public sealed override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
{
// [...]
context.RegisterRefactoring(CustomCodeAction.Create("MyAction",
(c, isPreview) => DoMyAction(context.Document, dec, c, isPreview)));
}
private static async Task<Solution> DoMyAction(Document document, MethodDeclarationSyntax method, CancellationToken cancellationToken, bool isPreview)
{
// some - for the question irrelevant - roslyn changes, like:
// [...]
// now the DTE magic
if (!isPreview)
{
// [...]
}
return document.Project.Solution;
}
Why those two?
The ComputePreviewOperationsAsync calls the the normal ComputeOperationsAsync, which internally calls ComputeOperationsAsync. This computation executes GetChangedSolutionAsync. So both ways - preview and not - end up at GetChangedSolutionAsync. That's what I actually want, calling the same code, getting a very similar solution, but giving a bool flag if it is preview or not too.
So I've written my own GetChangedSolutionWithPreviewAsync which I use instead. I have overriden the default GetChangedSolutionAsync using my custom Get function, and then ComputePreviewOperationsAsync with a fully customized body. Instead of calling ComputeOperationsAsync, which the default one does, I've copied the code of that function, and modified it to use my GetChangedSolutionWithPreviewAsync instead.
Sounds rather complicated in written from, but I guess the code above should explain it quite well.
Hope this helps other people.

Related

Roslyn analyser - check for null references for a specific type

I am checking for the possibility to build custom roslyn analyzer for case specifics to our system.
The solution is in .net Framework 4.8. I started with the tutorial How to write csharp analyzer code fix and am making my way from there.
The first case I want to check is that when the programmer use the value of a specific service they must not assume that the result is not null.
Take this service definition :
public interface IConfigurationService
{
Task<IConfiguration> GetConfiguration(string clientId);
}
And a code sample to analyze :
public async Task DoSomeTask(string clientId)
{
var configuration = await _configurationService.GetConfiguration(clientId);
// This line should raise a warning because this specific client may not be configurated
var serviceUri = configuration.ServiceUri;
DoSomeSubTask(serviceUri);
}
So far I got this
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
// The goal is to target the variable declaration (var configuration = ...)
context.RegisterSyntaxNodeAction(
AnalyzeDecalaration,
SyntaxKind.LocalDeclarationStatement
);
}
private static void AnalyzeDecalaration(SyntaxNodeAnalysisContext context)
{
// Check for the type of the variable and exit if it is not 'IConfiguration'
var symbolInfo = context.SemanticModel.GetSymbolInfo(localDeclaration.Declaration.Type);
var typeSymbol = symbolInfo.Symbol;
if (typeSymbol.Name != "IConfiguration")
{
return;
}
// Stuck here. I'm pretty sure dataFlowAnalysis is the key, but I can't figure how to use it
var dataFlowAnalysis = context.SemanticModel.AnalyzeDataFlow(localDeclaration);
var variable = localDeclaration.Declaration.Variables.Single();
ISymbol variableSymbol = context.SemanticModel.GetDeclaredSymbol(
variable,
context.CancellationToken
);
}
So that's where I am. I have targeted variable declaration for the target type. Not very much.
Since it is a specific case for a specific type, the analysis does not have to be very fancy. For exemple, I don't need to check for instanaces of IConfiguration inside an array, that's not a thing in our code base. Basically juste property access without null check.

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.

Leaving custom prompt validation in Bot Framework V4

I started building a dialog in Microsoft's Bot Framework V4 and for that I want to use the custom validation of prompts. A couple of month ago, when version 4.4 was released, a new property "AttemptCount" was added to the PromptValidatorContext. This property gives information on how many times a user gave an answer. Obviously, it would be nice to end the current dialog if a user was reprompted several times. However, I did not find a way to get out of this state, because the given PromptValidatorContext does not offer a way to replace the dialog, unlike a DialogContext (or WaterfallStepContext). I asked that question on github, but didn't get an answer.
public class MyComponentDialog : ComponentDialog
{
readonly WaterfallDialog waterfallDialog;
public MyComponentDialog(string dialogId) : (dialogId)
{
// Waterfall dialog will be started when MyComponentDialog is called.
this.InitialDialogId = DialogId.MainDialog;
this.waterfallDialog = new WaterfallDialog(DialogId.MainDialog, new WaterfallStep[] { this.StepOneAsync, this.StepTwoAsync});
this.AddDialog(this.waterfallDialog);
this.AddDialog(new TextPrompt(DialogId.TextPrompt, CustomTextValidatorAsync));
}
public async Task<DialogTurnResult> StepOneAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
var promptOptions = new PromptOptions
{
Prompt = MessageFactory.Text("Hello from text prompt"),
RetryPrompt = MessageFactory.Text("Hello from retry prompt")
};
return await stepContext.PromptAsync(DialogId.TextPrompt, promptOptions, cancellationToken).ConfigureAwait(false);
}
public async Task<DialogTurnResult> StepTwoAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
// Handle validated result...
}
// Critical part:
public async Task<bool> CustomTextValidatorAsync(PromptValidatorContext<string> promptContext, CancellationToken cancellationToken)
{
if (promptContext.AttemptCount > 3)
{
// How do I get out of here? :-/
}
if (promptContext.Context.Activity.Text.Equals("password")
{
// valid user input
return true;
}
// invalid user input
return false;
}
}
If this feature is actually missing, I could probably do a workaround by saving the information in the TurnState and checking it in my StepTwo. Something like this:
promptContext.Context.TurnState["validation"] = ValidationEnum.TooManyAttempts;
But this doesn't really feel right ;-)
Does anyone has an idea?
Cheers,
Andreas
You have a few options depending on what you want to do in the validator function and where you want to put the code that manages the dialog stack.
Option 1: return false
Your first opportunity to pop dialogs off the stack will be in the validator function itself, like I mentioned in the comments.
if (promptContext.AttemptCount > 3)
{
var dc = await BotUtil.Dialogs.CreateContextAsync(promptContext.Context, cancellationToken);
await dc.CancelAllDialogsAsync(cancellationToken);
return false;
}
You were right to be apprehensive about this, because this actually can cause problems if you don't do it correctly. The SDK does not expect you to manipulate the dialog stack within a validator function, and so you need to be aware of what happens when the validator function returns and act accordingly.
Option 1.1: send an activity
You can see in the source code that a prompt will try to reprompt without checking to see if the prompt is still on the dialog stack:
if (!dc.Context.Responded)
{
await OnPromptAsync(dc.Context, state, options, true, cancellationToken).ConfigureAwait(false);
}
This means that even if you clear the dialog stack inside your validator function, the prompt will still try to reprompt after that when you return false. We don't want that to happen because the dialog has already been cancelled, and if the bot asks a question that it won't be accepting answers to then that will look bad and confuse the user. However, this source code does provide a hint about how to avoid reprompting. It will only reprompt if TurnContext.Responded is false. You can set it to true by sending an activity.
Option 1.1.1: send a message activity
It makes sense to let the user know that they've used up all their attempts, and if you send the user such a message in your validator function then you won't have to worry about any unwanted automatic reprompts:
await promptContext.Context.SendActivityAsync("Cancelling all dialogs...");
Option 1.1.2: send an event activity
If you don't want to display an actual message to the user, you can send an invisible event activity that won't get rendered in the conversation. This will still set TurnContext.Responded to true:
await promptContext.Context.SendActivityAsync(new Activity(ActivityTypes.Event));
Option 1.2: nullify the prompt
We may not need to avoid having the prompt call its OnPromptAsync if the specific prompt type allows a way to avoid reprompting inside OnPromptAsync. Again having a look at the source code but this time in TextPrompt.cs, we can see where OnPromptAsync does its reprompting:
if (isRetry && options.RetryPrompt != null)
{
await turnContext.SendActivityAsync(options.RetryPrompt, cancellationToken).ConfigureAwait(false);
}
else if (options.Prompt != null)
{
await turnContext.SendActivityAsync(options.Prompt, cancellationToken).ConfigureAwait(false);
}
So if we don't want to send any activities to the user (visible or otherwise), we can stop a text prompt from reprompting simply by setting both its Prompt and RetryPrompt properties to null:
promptContext.Options.Prompt = null;
promptContext.Options.RetryPrompt = null;
Option 2: return true
The second opportunity to cancel dialogs as we move up the call stack from the validator function is in the next waterfall step, like you mentioned in your question. This may be your best option because it's the least hacky: it doesn't depend on any special understanding of the internal SDK code that could be subject to change. In this case your whole validator function could be as simple as this:
private Task<bool> ValidateAsync(PromptValidatorContext<string> promptContext, CancellationToken cancellationToken)
{
if (promptContext.AttemptCount > 3 || IsCorrectPassword(promptContext.Context.Activity.Text))
{
// valid user input
// or continue to next step anyway because of too many attempts
return Task.FromResult(true);
}
// invalid user input
// when there haven't been too many attempts
return Task.FromResult(false);
}
Note that we're using a method called IsCorrectPassword to determine if the password is correct. This is important because this option depends on reusing that functionality in the next waterfall step. You had mentioned needing to save information in TurnState but this is unnecessary since everything we need to know is already in the turn context. The validation is based on the activity's text, so we can just validate that same text again in the next step.
Option 2.1: use WaterfallStepContext.Context.Activity.Text
The text that the user entered will still be available to you in WaterfallStepContext.Context.Activity.Text so your next waterfall step could look like this:
async (stepContext, cancellationToken) =>
{
if (IsCorrectPassword(stepContext.Context.Activity.Text))
{
return await stepContext.NextAsync(null, cancellationToken);
}
else
{
await stepContext.Context.SendActivityAsync("Cancelling all dialogs...");
return await stepContext.CancelAllDialogsAsync(cancellationToken);
}
},
Option 2.2: use WaterfallStepContext.Result
Waterfall step contexts have a builtin Result property that refers to the result of the previous step. In the case of a text prompt, it will be the string returned by that prompt. You can use it like this:
if (IsCorrectPassword((string)stepContext.Result))
Option 3: throw an exception
Going further up the call stack, you can handle things in the message handler that originally called DialogContext.ContinueDialogAsync by throwing an exception in your validator function, like CameronL mentioned in the deleted portion of their answer. While it's generally considered bad practice to use exceptions to trigger intentional code paths, this does closely resemble how retry limits worked in Bot Builder v3, which you mentioned wanting to replicate.
Option 3.1: use the base Exception type
You can throw just an ordinary exception. To make it easier to tell this exception apart from other exceptions when you catch it, you can optionally include some metadata in the exception's Source property:
if (promptContext.AttemptCount > 3)
{
throw new Exception(BotUtil.TooManyAttemptsMessage);
}
Then you can catch it like this:
try
{
await dc.ContinueDialogAsync(cancellationToken);
}
catch (Exception ex)
{
if (ex.Message == BotUtil.TooManyAttemptsMessage)
{
await turnContext.SendActivityAsync("Cancelling all dialogs...");
await dc.CancelAllDialogsAsync(cancellationToken);
}
else
{
throw ex;
}
}
Option 3.2: use a derived exception type
If you define your own exception type, you can use that to only catch this specific exception.
public class TooManyAttemptsException : Exception
You can throw it like this:
throw new TooManyAttemptsException();
Then you can catch it like this:
try
{
await dc.ContinueDialogAsync(cancellationToken);
}
catch (TooManyAttemptsException)
{
await turnContext.SendActivityAsync("Cancelling all dialogs...");
await dc.CancelAllDialogsAsync(cancellationToken);
}
Declare a flag variable in user state class and update the flag inside the if block:
if (promptContext.AttemptCount > 3)
{
\\fetch user state object
\\update flag here
return true;
}
After returning true you will be taken to the next dialog in waterfall step, where you can check the flag value, display an appropriate message and terminate dialog flow. You can refer to microsoft docs to know how to use the User state data
The prompt validator context object is a more specific object only concerned with passing or failing the validator.
** removed incorrect answer **
You can create a class with a WaterfallStep and a PromptValidator. That class would (i) handle the logic to exit the PromptValidator and (ii) handle the logic to cancel/end/proceed the dialog after that. This solution is category of Kyle Delaney answer which returns true in the PromptValidator.
I called that class WaterfallStepValidation:
private readonly Func<string, Task<bool>> _validator;
private readonly int _retryCount;
private bool _isInputValid = false;
public WaterfallStepValidation(Func<string, Task<bool>> validator, int retryCount)
{
_validator = validator;
_retryCount = retryCount;
}
public async Task<DialogTurnResult> CheckValidInputStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
if (!_isInputValid)
{
await stepContext.Context.SendActivityAsync("Could not proceed...");
// Here you could also end all dialogs or just proceed to the next step
return await stepContext.EndDialogAsync(false);
}
return await stepContext.NextAsync(stepContext.Result, cancellationToken);
}
public async Task<bool> PromptValidatorAsync(PromptValidatorContext<string> promptContext, CancellationToken cancellationToken)
{
_isInputValid = await _validator(promptContext.Recognized.Value);
if (!_isInputValid && promptContext.AttemptCount >= _retryCount)
{
_isInputValid = false;
return true;
}
return _isInputValid;
}
And then you call it like this:
var ageStepValidation = new WaterfallStepValidation(AgeValidator, retryCount: 3);
AddDialog(new TextPrompt("AgeTextPromptId", ageStepValidation.PromptValidatorAsync));
var waterfallSteps = new List<WaterfallStep>()
{
PromptNameStepAsync,
PromptAgeStepAsync,
ageStepValidation.CheckValidInputStepAsync,
PromptChoicesStepAsync
};
AddDialog(new WaterfallDialog(nameof(WaterfallDialog), waterfallSteps));
I think this is an elegant workaround for this problem.
The weak points of this aproch are:
you have to place CheckValidInputStepAsync right after the step with the validation
is still a kind of hack, since the PromptValidator returns true if fails the validation
However, the strong points are:
it doesn't make big hacks, just that return true
most of the logic is encapsulated in the WaterfallStepValidation class

Copy Method Body To Property

I am trying to create a new property that has the same body as a method.
Here is my code so far:
private async Task<Solution> ConvertMethodToProperty(Document document, MethodDeclarationSyntax methodNode, CancellationToken cancellationToken)
{
AccessorListSyntax accesors = SyntaxFactory.AccessorList(new SyntaxList<AccessorDeclarationSyntax>
{
SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration, methodNode.Body)
});
PropertyDeclarationSyntax newp = SyntaxFactory.PropertyDeclaration(new SyntaxList<AttributeListSyntax>(), methodNode.Modifiers, methodNode.ReturnType, methodNode.ExplicitInterfaceSpecifier, methodNode.Identifier, accesors);
SyntaxNode root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
document = document.WithSyntaxRoot(root.ReplaceNode(methodNode, newp));
return document.Project.Solution;
}
}
However when I run this on my test project I see this:
Even though the methodNode.Body from the code is populated with the method body I want:
What am I doing wrong when I create my AccessorListSyntax? Thanks!
I think your use of the collection initialiser on the SyntaxList is not valid. As that will implicitly call the Add method on your SyntaxList but the add doesn't modify the underlying collection it just returns a new SyntaxList. Try instantiating and manually adding afterwards like this.
var accessorList = new SyntaxList<AccessorDeclarationSyntax>();
accessorList = accessorList.Add(SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration, methodNode.Body));
I fell into the same trap when I started playing around with Roslyn, just have to remember the immutability thing.

GetAnalyzerDiagnosticsAsync returns nothing if a DiagnosticAnalyzer has an empty Initialize

I am writing a method which gets all the Diagnostics for a Project for a set of DiagnosticAnalyzers:
private static async Task<ImmutableArray<Diagnostic>> GetDiagnosticsAsync(
Project project,
ImmutableArray<DiagnosticAnalyzer> analyzers)
{
var compilation = await project.GetCompilationAsync();
var withAnalyzers = compilation.WithAnalyzers(analyzers);
var diagnostics = await withAnalyzers.GetAnalyzerDiagnosticsAsync();
return diagnostics;
}
This works fine if all the DiagnosticAnalyzers in analyzers have their Initialize(AnalysisContext) method containing some register call:
public override void Initialize(AnalysisContext context)
{
context.Register...(...);
}
However if any of them have an empty implementation
public override void Initialize(AnalysisContext context)
{
// nothing here
}
then diagnostics.Count() == 0.
Have I misunderstood what the behavior for GetAnalyzerDiagnosticsAsync() and DiagnosticAnalyzers are and therefore this a correct thing for it to be doing? Or is this a bug in Roslyn? Or is there another method I should be using? Or something else?
I'm tempted to think that this is a error in Roslyn (as such I have opened an issue). Digging into the source code and debugging results that the difference occurs in CompilationWithAnalyzers._driver.DiagnosticQueue - when it works this has the desired Count but is 0 when the issue occurs.
The plot thickens...
Turns out this was a bug with Roslyn, which has now been fixed:
Closed issue
Pull request

Categories