internal async Task <(InvocationExpressionSyntax?, FixDetails?)> CheckIfCanFixAsync(Document invocationDoc, TextSpan span, CancellationToken cancellationToken) { var root = await _getSyntaxRootAsync(invocationDoc, cancellationToken).ConfigureAwait(false); if (root?.FindNode(span) is not InvocationExpressionSyntax invocationExpression) { // shouldn't happen, we only get called for invocations return(null, null); } var sm = await _getSemanticModelAsync(invocationDoc, cancellationToken).ConfigureAwait(false); if (sm == null) { // shouldn't happen return(null, null); } var comp = sm.Compilation; var loggerExtensions = _getTypeByMetadataName1(comp, "Microsoft.Extensions.Logging.LoggerExtensions"); if (loggerExtensions == null) { // shouldn't happen, we only get called for methods on this type return(null, null); } var invocationOp = _getOperation(sm, invocationExpression, cancellationToken) as IInvocationOperation; if (invocationOp == null) { // shouldn't happen, we're dealing with an invocation expression return(null, null); } var method = invocationOp.TargetMethod; var details = new FixDetails(method, invocationOp, invocationDoc.Project.DefaultNamespace, invocationDoc.Project.Documents); if (string.IsNullOrWhiteSpace(details.Message)) { // can't auto-generate without a valid message string return(null, null); } if (details.EventIdParamIndex >= 0) { // can't auto-generate the variants using event id return(null, null); } if (string.IsNullOrWhiteSpace(details.Level)) { // can't auto-generate without a valid level return(null, null); } return(invocationExpression, details); }
/// <summary> /// Given a LoggerExtensions method invocation, produce an argument list in the shape of a corresponding generated logging method. /// </summary> private static IReadOnlyList <ITypeSymbol> MakeArgumentList(FixDetails details, IInvocationOperation invocationOp) { var args = new List <ITypeSymbol> { invocationOp.Arguments[0].Value.Type }; if (details.ExceptionParamIndex >= 0) { args.Add(invocationOp.Arguments[details.ExceptionParamIndex].Value.Type); } var paramsArg = invocationOp.Arguments[details.ArgsParamIndex]; if (paramsArg != null) { var arrayCreation = paramsArg.Value as IArrayCreationOperation; foreach (var e in arrayCreation !.Initializer.ElementValues) { foreach (var d in e.Descendants()) { args.Add(d.Type); } } } return(args); }
// Load attachment from embedded resource. private Attachment CreateFixDetailsAttachment(FixDetails fixDetails) { List <string> jumperList = new List <string>() { "jumper", "sweater", "hoodie", "oodie", "jersey" }; //To Do: Add logic for different fixes... call IFitIt api? var cardResourcePath = "FixerBot.Cards.welcomeCard.json"; if (jumperList.Contains(fixDetails.Item.Split(" ").Last().ToLower())) { cardResourcePath = "FixerBot.Cards.fixJumperCard.json"; } using (var stream = GetType().Assembly.GetManifestResourceStream(cardResourcePath)) { using (var reader = new StreamReader(stream)) { var adaptiveCard = reader.ReadToEnd(); return(new Attachment() { ContentType = "application/vnd.microsoft.card.adaptive", Content = JsonConvert.DeserializeObject(adaptiveCard), }); } } }
private async Task <DialogTurnResult> ActStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken) { if (!_luisRecognizer.IsConfigured) { // LUIS is not configured, we just run the BookingDialog path with an empty BookingDetailsInstance. return(await stepContext.BeginDialogAsync(nameof(FixItYourselfDialog), new BookingDetails(), cancellationToken)); //return await stepContext.BeginDialogAsync(nameof(BookingDialog), new BookingDetails(), cancellationToken); } // Call LUIS and gather any potential booking details. (Note the TurnContext has the response to the prompt.) var luisResult = await _luisRecognizer.RecognizeAsync <FlightBooking>(stepContext.Context, cancellationToken); switch (luisResult.TopIntent().intent) { case FlightBooking.Intent.BookFlight: await ShowWarningForUnsupportedCities(stepContext.Context, luisResult, cancellationToken); // Initialize BookingDetails with any entities we may have found in the response. var bookingDetails = new BookingDetails() { // Get destination and origin from the composite entities arrays. Destination = luisResult.ToEntities.Airport, Origin = luisResult.FromEntities.Airport, TravelDate = luisResult.TravelDate, }; // Run the BookingDialog giving it whatever details we have from the LUIS call, it will fill out the remainder. return(await stepContext.BeginDialogAsync(nameof(BookingDialog), bookingDetails, cancellationToken)); case FlightBooking.Intent.GetWeather: // We haven't implemented the GetWeatherDialog so we just display a TODO message. var getWeatherMessageText = "TODO: get weather flow here"; var getWeatherMessage = MessageFactory.Text(getWeatherMessageText, getWeatherMessageText, InputHints.IgnoringInput); await stepContext.Context.SendActivityAsync(getWeatherMessage, cancellationToken); break; case FlightBooking.Intent.FixItem: // Catch all for unhandled intents var fixDetails = new FixDetails(); fixDetails.ConfidenceLevel = luisResult.ConfidenceLevel; fixDetails.Fixer = luisResult.Fixer; fixDetails.Item = luisResult.Item; fixDetails.Problem = luisResult.Problem; return(await stepContext.BeginDialogAsync(nameof(FixerDialog), fixDetails, cancellationToken)); default: // Catch all for unhandled intents var didntUnderstandMessageText = $"Sorry, I didn't get that (intent was {luisResult.TopIntent().intent}). I assume you want to fix something."; var didntUnderstandMessage = MessageFactory.Text(didntUnderstandMessageText, didntUnderstandMessageText, InputHints.IgnoringInput); await stepContext.Context.SendActivityAsync(didntUnderstandMessage, cancellationToken); return(await stepContext.BeginDialogAsync(nameof(FixerDialog), new FixDetails(), cancellationToken)); } return(await stepContext.NextAsync(null, cancellationToken)); }
private static async Task <Solution> RewriteLoggingCallAsync( Document doc, InvocationExpressionSyntax invocationExpression, FixDetails details, string methodName, CancellationToken cancellationToken) { var solEditor = new SolutionEditor(doc.Project.Solution); var docEditor = await solEditor.GetDocumentEditorAsync(doc.Id, cancellationToken).ConfigureAwait(false); var sm = docEditor.SemanticModel; var comp = sm.Compilation; var gen = docEditor.Generator; var invocation = sm.GetOperation(invocationExpression, cancellationToken) as IInvocationOperation; var argList = new List <SyntaxNode>(); int index = 0; foreach (var arg in invocation !.Arguments) { if ((index == details.MessageParamIndex) || (index == details.LogLevelParamIndex)) { index++; continue; } index++; if (arg.ArgumentKind == ArgumentKind.ParamArray) { var arrayCreation = arg.Value as IArrayCreationOperation; foreach (var e in arrayCreation !.Initializer.ElementValues) { argList.Add(e.Syntax); } } else { argList.Add(arg.Syntax); } } var typeSyntax = comp.GetTypeByMetadataName(details.FullTargetClassName); var typeSymbol = gen.TypeExpression(typeSyntax); var memberAccessExpression = gen.MemberAccessExpression(typeSymbol, methodName); var call = gen.InvocationExpression(memberAccessExpression, argList); docEditor.ReplaceNode(invocationExpression, call.WithTriviaFrom(invocationExpression)); return(solEditor.GetChangedSolution()); }
private async Task <Solution> InsertLoggingMethodSignatureAsync( Document targetDoc, ClassDeclarationSyntax targetClass, Document invocationDoc, InvocationExpressionSyntax invocationExpression, FixDetails details, CancellationToken cancellationToken) { var invocationSM = (await invocationDoc.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false)) !; var invocationOp = (invocationSM.GetOperation(invocationExpression, cancellationToken) as IInvocationOperation) !; var solEditor = new SolutionEditor(targetDoc.Project.Solution); var docEditor = await solEditor.GetDocumentEditorAsync(targetDoc.Id, cancellationToken).ConfigureAwait(false); var sm = docEditor.SemanticModel; var comp = sm.Compilation; var gen = docEditor.Generator; var logMethod = gen.MethodDeclaration( details.TargetMethodName, MakeParameterList(details, invocationOp, gen), accessibility: Accessibility.Internal, modifiers: DeclarationModifiers.Partial | DeclarationModifiers.Static); var attrArgs = new[] { gen.LiteralExpression(CalcEventId(comp, targetClass, cancellationToken)), gen.MemberAccessExpression(gen.TypeExpression(comp.GetTypeByMetadataName("Microsoft.Extensions.Logging.LogLevel")), details.Level), gen.LiteralExpression(details.Message), }; var attr = gen.Attribute( gen.TypeExpression(comp.GetTypeByMetadataName(LoggerMessageAttribute)), attrArgs); logMethod = gen.AddAttributes(logMethod, attr); var comment = SyntaxFactory.ParseLeadingTrivia($@" /// <summary> /// Logs `{EscapeMessageString(details.Message)}` at `{details.Level}` level. /// </summary> "); logMethod = logMethod.WithLeadingTrivia(comment); docEditor.AddMember(targetClass, logMethod); return(solEditor.GetChangedSolution()); }
/// <summary> /// Given a LoggerExtensions method invocation, produce a parameter list for the corresponding generated logging method. /// </summary> private static IReadOnlyList <SyntaxNode> MakeParameterList( FixDetails details, IInvocationOperation invocationOp, SyntaxGenerator gen) { var parameters = new List <SyntaxNode> { gen.ParameterDeclaration("logger", gen.TypeExpression(invocationOp.Arguments[0].Value.Type)) }; if (details.ExceptionParamIndex >= 0) { parameters.Add(gen.ParameterDeclaration("exception", gen.TypeExpression(invocationOp.Arguments[details.ExceptionParamIndex].Value.Type))); } var paramsArg = invocationOp.Arguments[details.ArgsParamIndex]; if (paramsArg != null) { var arrayCreation = paramsArg.Value as IArrayCreationOperation; var index = 0; foreach (var e in arrayCreation !.Initializer.ElementValues) { foreach (var d in e.Descendants()) { string name; if (index < details.MessageArgs.Count) { name = details.MessageArgs[index]; } else { name = $"arg{index}"; } parameters.Add(gen.ParameterDeclaration(name, gen.TypeExpression(d.Type))); index++; } } } return(parameters); }
/// <summary> /// Orchestrate all the work needed to fix an issue. /// </summary> private async Task <Solution> ApplyFixAsync(Document invocationDoc, InvocationExpressionSyntax invocationExpression, FixDetails details, CancellationToken cancellationToken) { ClassDeclarationSyntax targetClass; Document targetDoc; Solution sol; // stable id surviving across solution generations var invocationDocId = invocationDoc.Id; // get a reference to the class where to insert the logging method, creating it if necessary (sol, targetClass, targetDoc) = await GetOrMakeTargetClassAsync(invocationDoc.Project, details, cancellationToken).ConfigureAwait(false); // find the doc and invocation in the current solution (invocationDoc, invocationExpression) = await RemapAsync(sol, invocationDocId, invocationExpression).ConfigureAwait(false); // determine the final name of the logging method and whether we need to generate it or not var(methodName, existing) = await GetFinalTargetMethodNameAsync(targetDoc, targetClass, invocationDoc, invocationExpression, details, cancellationToken).ConfigureAwait(false); // if the target method doesn't already exist, go make it if (!existing) { // generate the logging method signature in the target class sol = await InsertLoggingMethodSignatureAsync(targetDoc, targetClass, invocationDoc, invocationExpression, details, cancellationToken).ConfigureAwait(false); // find the doc and invocation in the current solution (invocationDoc, invocationExpression) = await RemapAsync(sol, invocationDocId, invocationExpression).ConfigureAwait(false); } // rewrite the call site to invoke the generated logging method sol = await RewriteLoggingCallAsync(invocationDoc, invocationExpression, details, methodName, cancellationToken).ConfigureAwait(false); return(sol); }
/// <summary> /// Finds the class into which to create the logging method signature, or creates it if it doesn't exist. /// </summary> private static async Task <(Solution, ClassDeclarationSyntax, Document)> GetOrMakeTargetClassAsync(Project proj, FixDetails details, CancellationToken cancellationToken) { while (true) { var comp = (await proj.GetCompilationAsync(cancellationToken).ConfigureAwait(false)) !; var allNodes = comp.SyntaxTrees.SelectMany(s => s.GetRoot().DescendantNodes()); var allClasses = allNodes.Where(d => d.IsKind(SyntaxKind.ClassDeclaration)).OfType <ClassDeclarationSyntax>(); foreach (var cl in allClasses) { var nspace = GetNamespace(cl); if (nspace != details.TargetNamespace) { continue; } if (cl.Identifier.Text == details.TargetClassName) { return(proj.Solution, cl, proj.GetDocument(cl.SyntaxTree) !); } } var text = $@" #pragma warning disable CS8019 using Microsoft.Extensions.Logging; using System; #pragma warning restore CS8019 static partial class {details.TargetClassName} {{ }} "; if (!string.IsNullOrEmpty(details.TargetNamespace)) { text = $@" namespace {details.TargetNamespace} {{ #pragma warning disable CS8019 using Microsoft.Extensions.Logging; using System; #pragma warning restore CS8019 static partial class {details.TargetClassName} {{ }} }} "; } proj = proj.AddDocument(details.TargetFilename, text).Project; } }
/// <summary> /// Get the final name of the target method. If there's an existing method with the right /// message, level, and argument types, we just use that. Otherwise, we create a new method. /// </summary> internal async Task <(string methodName, bool existing)> GetFinalTargetMethodNameAsync( Document targetDoc, ClassDeclarationSyntax targetClass, Document invocationDoc, InvocationExpressionSyntax invocationExpression, FixDetails details, CancellationToken cancellationToken) { var invocationSM = (await invocationDoc.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false)) !; var invocationOp = (invocationSM.GetOperation(invocationExpression, cancellationToken) as IInvocationOperation) !; var docEditor = await DocumentEditor.CreateAsync(targetDoc, cancellationToken).ConfigureAwait(false); var sm = docEditor.SemanticModel; var comp = sm.Compilation; var loggerMessageAttribute = _getTypeByMetadataName2(comp, LoggerMessageAttribute); if (loggerMessageAttribute is null) { // strange that we can't find the attribute, but supply a potential useful value instead return(details.TargetMethodName, false); } var invocationArgList = MakeArgumentList(details, invocationOp); var conflict = false; var count = 2; string methodName; do { methodName = details.TargetMethodName; if (conflict) { methodName = $"{methodName}{count}"; count++; conflict = false; } foreach (var method in targetClass.Members.Where(m => m.IsKind(SyntaxKind.MethodDeclaration)).OfType <MethodDeclarationSyntax>()) { var methodSymbol = _getDeclaredSymbol(sm, method, cancellationToken); if (methodSymbol == null) { // hmmm, this shouldn't happen should it? continue; } var matchName = method.Identifier.ToString() == methodName; var matchParams = invocationArgList.Count == methodSymbol.Parameters.Length; if (matchParams) { for (int i = 0; i < invocationArgList.Count; i++) { matchParams = invocationArgList[i].Equals(methodSymbol.Parameters[i].Type, SymbolEqualityComparer.Default); if (!matchParams) { break; } } } if (matchName && matchParams) { conflict = true; } foreach (var mal in method.AttributeLists) { foreach (var ma in mal.Attributes) { var mattrSymbolInfo = sm.GetSymbolInfo(ma, cancellationToken); if (mattrSymbolInfo.Symbol is IMethodSymbol ms) { if (loggerMessageAttribute.Equals(ms.ContainingType, SymbolEqualityComparer.Default)) { var arg = ma.ArgumentList !.Arguments[LoggerMessageAttrLevelArg]; var level = (LogLevel)sm.GetConstantValue(arg.Expression, cancellationToken).Value !; arg = ma.ArgumentList.Arguments[LoggerMessageAttrMessageArg]; var message = sm.GetConstantValue(arg.Expression, cancellationToken).ToString(); var matchMessage = message == details.Message; var matchLevel = FixDetails.GetLogLevelName(level) == details.Level; if (matchLevel && matchMessage && matchParams) { // found a match, use this one return(method.Identifier.ToString(), true); } break; } } } } } }while (conflict); return(methodName, false); }