From de82fb2c9d794b760558702629e2fcccdd8cf75a Mon Sep 17 00:00:00 2001 From: Mike Rousos Date: Mon, 28 Jun 2021 15:20:45 -0400 Subject: [PATCH 01/11] Initial WIP ApiAlertAnalyzer implementation --- .../DefaultExtensionServiceProvider.cs | 1 + .../SyntaxNodeExtensions.cs | 15 +- .../ApiAlertAnalyzer.cs | 190 ++++++++++++++++++ .../DefaultApiAlerts.json | 45 +++++ ...istant.Extensions.Default.Analyzers.csproj | 6 + .../Resources.Designer.cs | 27 +++ .../Resources.resx | 9 + .../TargetSyntax.cs | 63 ++++++ .../TargetSyntaxMessage.cs | 13 ++ .../TargetSyntaxType.cs | 23 +++ 10 files changed, 383 insertions(+), 9 deletions(-) create mode 100644 src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/ApiAlertAnalyzer.cs create mode 100644 src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/DefaultApiAlerts.json create mode 100644 src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/TargetSyntax.cs create mode 100644 src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/TargetSyntaxMessage.cs create mode 100644 src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/TargetSyntaxType.cs diff --git a/src/extensions/default/Microsoft.DotNet.UpgradeAssistant.Extensions.Default/DefaultExtensionServiceProvider.cs b/src/extensions/default/Microsoft.DotNet.UpgradeAssistant.Extensions.Default/DefaultExtensionServiceProvider.cs index 90092f9c4..d756c87ad 100644 --- a/src/extensions/default/Microsoft.DotNet.UpgradeAssistant.Extensions.Default/DefaultExtensionServiceProvider.cs +++ b/src/extensions/default/Microsoft.DotNet.UpgradeAssistant.Extensions.Default/DefaultExtensionServiceProvider.cs @@ -47,6 +47,7 @@ private static void AddAnalyzersAndCodeFixProviders(IServiceCollection services) { // Add source analyzers and code fix providers (note that order doesn't matter as they're run alphabetically) // Analyzers + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Common/SyntaxNodeExtensions.cs b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Common/SyntaxNodeExtensions.cs index cc4cb1898..94819da07 100644 --- a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Common/SyntaxNodeExtensions.cs +++ b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Common/SyntaxNodeExtensions.cs @@ -94,15 +94,12 @@ public static SyntaxNode GetQualifiedName(this SyntaxNode node) throw new ArgumentNullException(nameof(node)); } - // If the node is part of a qualified name, we want to get the full qualified name - while (node.Parent is CSSyntax.NameSyntax || node.Parent is VBSyntax.NameSyntax) - { - node = node.Parent; - } - - // If the node is part of a member access expression (a static member access, for example), then the - // qualified name will be a member access expression rather than a name syntax. - if ((node.Parent is CSSyntax.MemberAccessExpressionSyntax csMAE && csMAE.Name.IsEquivalentTo(node)) + // If the node is part of a qualified name or a member access expression, we want to get the full qualified name + // which will be the parent of that node. It's not necessary to recurse to the parent's ancestors since qualified + // names and member access expressions are composed of simple names on the right and the entire qualifier on the left. + if ((node.Parent is CSSyntax.QualifiedNameSyntax csName && csName.Right.IsEquivalentTo(node)) + || (node.Parent is VBSyntax.QualifiedNameSyntax vbName && vbName.Right.IsEquivalentTo(node)) + || (node.Parent is CSSyntax.MemberAccessExpressionSyntax csMAE && csMAE.Name.IsEquivalentTo(node)) || (node.Parent is VBSyntax.MemberAccessExpressionSyntax vbMAE && vbMAE.Name.IsEquivalentTo(node))) { node = node.Parent; diff --git a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/ApiAlertAnalyzer.cs b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/ApiAlertAnalyzer.cs new file mode 100644 index 000000000..1fc66f1f2 --- /dev/null +++ b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/ApiAlertAnalyzer.cs @@ -0,0 +1,190 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using CS = Microsoft.CodeAnalysis.CSharp; +using CSSyntax = Microsoft.CodeAnalysis.CSharp.Syntax; +using VB = Microsoft.CodeAnalysis.VisualBasic; +using VBSyntax = Microsoft.CodeAnalysis.VisualBasic.Syntax; + +namespace Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers +{ + /* + Extenders can supply: + 1. An API to alert on + 2. Message + 3. Whether the API needs to resolve to match symbolically or not (maybe call 'support partial syntax match'?) + + Scenarios to support: + - Base types and interfaces (name syntax) + - HttpApplication, IHttpModule + - Attributes (ChildActionOnlyAttribute) + - Method calls or property invocations (member access syntax)? + Do I need this or not? There are some things that I'd want to flag on the + method level but *most* could be on the type level. + - global.asax.cs registration APIs (RouteCollection.MapMvcAttributeRoutes) + - Removed HttpContext APIs? + */ + + /// + /// Analyzer for identifying usage of APIs that should be reported to the + /// user along with messaging about how the API should be (manually) replaced + /// to complete the upgrade. + /// + [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] + public class ApiAlertAnalyzer : DiagnosticAnalyzer + { + /// + /// The base diagnostic ID that diagnostics produced by this analyzer will use as a prefix. + /// + public const string BaseDiagnosticId = "UA0013"; + + /// + /// The diagnsotic category for diagnostics produced by this analyzer. + /// + private const string Category = "Upgrade"; + + private const string DefaultApiAlertsResourceName = "Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.DefaultApiAlerts.json"; + private IEnumerable _targetSyntaxes; + + public override ImmutableArray SupportedDiagnostics { get; } + + public ApiAlertAnalyzer() + { + var jsonSerializerOptions = new JsonSerializerOptions + { + AllowTrailingCommas = true, + }; + + jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + + using var resourceStream = new StreamReader(typeof(ApiAlertAnalyzer).Assembly.GetManifestResourceStream(DefaultApiAlertsResourceName)); + _targetSyntaxes = JsonSerializer.Deserialize(resourceStream.ReadToEnd(), jsonSerializerOptions) + ?? throw new InvalidOperationException($"Could not read target syntax messages from resource {DefaultApiAlertsResourceName}"); + + // Assemby the list of supported diagnostics + var supportedDiagnostics = ImmutableArray.CreateBuilder(); + + // First, support a generic diagnostic that will be used for API targets specified in additional files + var genericTitle = new LocalizableResourceString(nameof(Resources.ApiAlertGenericTitle), Resources.ResourceManager, typeof(Resources)); + var genericMessageFormat = new LocalizableResourceString(nameof(Resources.ApiAlertGenericMessageFormat), Resources.ResourceManager, typeof(Resources)); + var genericDesription = new LocalizableResourceString(nameof(Resources.ApiAlertGenericDescription), Resources.ResourceManager, typeof(Resources)); + supportedDiagnostics.Add(new(BaseDiagnosticId, genericTitle, genericMessageFormat, Category, DiagnosticSeverity.Warning, true, genericDesription)); + + // Also add all API targets specified specifically in embedded resources + supportedDiagnostics.AddRange(_targetSyntaxes.Select(s => + new DiagnosticDescriptor($"{BaseDiagnosticId}-{s.Id}", $"Replace usage of {string.Join(", ", s.TargetSyntaxes.Select(t => t.FullName))}", s.Message, Category, DiagnosticSeverity.Warning, true, s.Message))); + + SupportedDiagnostics = supportedDiagnostics.ToImmutable(); + } + + /// + /// Initializes the analyzer by registering analysis callback methods. + /// + /// The context to use for initialization. + public override void Initialize(AnalysisContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + + context.RegisterCompilationStartAction(context => + { + // Load analyzer configuration defining the types that should be mapped. + var additionalTargetSyntaxes = + // TODO : + // TargetSyntaxMessageLoader.LoadMappings(context.Options.AdditionalFiles); + Enumerable.Empty(); + + var combinedTargetSyntaxes = _targetSyntaxes.Concat(additionalTargetSyntaxes); + + // Register actions for handling both C# and VB identifiers + context.RegisterSyntaxNodeAction(context => AnalyzeCSharpIdentifier(context, combinedTargetSyntaxes), CS.SyntaxKind.IdentifierName); + context.RegisterSyntaxNodeAction(context => AnalyzeVBIdentifier(context, combinedTargetSyntaxes), VB.SyntaxKind.IdentifierName); + }); + } + + private void AnalyzeCSharpIdentifier(SyntaxNodeAnalysisContext context, IEnumerable targetSyntaxMessages) + { + Console.WriteLine(); + var identifier = (CSSyntax.IdentifierNameSyntax)context.Node; + var symbol = identifier.Parent is CSSyntax.AttributeSyntax + ? context.SemanticModel.GetTypeInfo(identifier).Type + : context.SemanticModel.GetSymbolInfo(identifier).Symbol; + + AnalyzeIdentifier(context, symbol, targetSyntaxMessages, identifier.Identifier.ValueText); + } + + private void AnalyzeVBIdentifier(SyntaxNodeAnalysisContext context, IEnumerable targetSyntaxMessages) + { + var identifier = (VBSyntax.IdentifierNameSyntax)context.Node; + var symbol = identifier.Parent is VBSyntax.AttributeSyntax + ? context.SemanticModel.GetTypeInfo(identifier).Type + : context.SemanticModel.GetSymbolInfo(identifier).Symbol; + + AnalyzeIdentifier(context, symbol, targetSyntaxMessages, identifier.Identifier.ValueText); + } + + private void AnalyzeIdentifier(SyntaxNodeAnalysisContext context, ISymbol? symbol, IEnumerable targetSyntaxMessages, string simpleName) + { + // Find target syntax/message mappings that include this node's simple name + var possibleMatches = targetSyntaxMessages.SelectMany(m => m.TargetSyntaxes.Select(s => (TargetSyntax: s, Mapping: m))).Where(t => t.TargetSyntax.SimpleName.Equals(simpleName, StringComparison.Ordinal)); + + if (!possibleMatches.Any()) + { + return; + } + + foreach (var match in possibleMatches) + { + if (match.TargetSyntax.SyntaxType switch + { + TargetSyntaxType.Member => AnalyzeMember(context.Node, symbol, simpleName, match.TargetSyntax), + TargetSyntaxType.Type => AnalyzeType(context.Node, symbol, simpleName, match.TargetSyntax), + TargetSyntaxType.Namespace => AnalyzeNamespace(context.Node, symbol, simpleName, match.TargetSyntax), + _ => false + }) + { + // Get the diagnostic descriptor correspdoning to the target API message map or, if a specific descriptor doesn't exist for it, + // get the default (first) one. + var id = $"{BaseDiagnosticId}-{match.Mapping.Id}"; + var diagnosticDescriptor = SupportedDiagnostics.FirstOrDefault(d => d.Id.Equals(id, StringComparison.Ordinal)) ?? SupportedDiagnostics.First(); + + // Create and report the diagnostic. Note that the fully qualified name's location is used so + // that any future code fix provider can directly replace the node without needing to consider its parents. + context.ReportDiagnostic(Diagnostic.Create(diagnosticDescriptor, context.Node.GetQualifiedName().GetLocation(), match.Mapping.Message)); + } + } + } + + private bool AnalyzeNamespace(SyntaxNode node, ISymbol? symbol, string simpleName, TargetSyntax targetSyntax) + { + // TODO + return false; + } + + private bool AnalyzeType(SyntaxNode node, ISymbol? symbol, string simpleName, TargetSyntax targetSyntax) + { + // TODO + return false; + } + + private bool AnalyzeMember(SyntaxNode node, ISymbol? symbol, string simpleName, TargetSyntax targetSyntax) + { + // TODO + return false; + } + } +} diff --git a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/DefaultApiAlerts.json b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/DefaultApiAlerts.json new file mode 100644 index 000000000..080631132 --- /dev/null +++ b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/DefaultApiAlerts.json @@ -0,0 +1,45 @@ +[ + { + "Id": "A", + "TargetSyntaxes": [ + { + "FullName": "System.Web.IHttpModule", + "SyntaxType": "Type", + "AlertOnAmbiguousMatch": true + }, + { + "FullName": "System.Web.IHttpHandler", + "SyntaxType": "Type", + "AlertOnAmbiguousMatch": true + } + ], + "Message": "HTTP modules and HTTP handlers should be rewritten as middleware in ASP.NET Core. https://docs.microsoft.com/en-us/aspnet/core/migration/http-modulesIHttpModule", + }, + { + "Id": "B", + "TargetSyntaxes": [ + { + "FullName": "System.ServiceModel.ServiceHost", + "SyntaxType": "Type", + "AlertOnAmbiguousMatch": true + }, + { + "FullName": "System.ServiceModel.ServiceHostBase", + "SyntaxType": "Type", + "AlertOnAmbiguousMatch": true + } + ], + "Message": "WCF server APIs are unsupported on .NET Core. Consider rewriting to use gRPC (https://docs.microsoft.com/en-us/dotnet/architecture/grpc-for-wcf-developers), ASP.NET Core, or CoreWCF (https://github.com/CoreWCF/CoreWCF) instead.", + }, + { + "Id": "C", + "TargetSyntaxes": [ + { + "FullName": "System.Web.Optimization.BundleCollection", + "SyntaxType": "Type", + "AlertOnAmbiguousMatch": true + } + ], + "Message": "Script and style bundling works differently in ASP.NET Core. BundleCollection should be replaced by alternative bundling technologies. https://docs.microsoft.com/en-us/aspnet/core/client-side/bundling-and-minification" + } +] \ No newline at end of file diff --git a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.csproj b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.csproj index f879e5005..80eedc6cf 100644 --- a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.csproj +++ b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.csproj @@ -5,6 +5,12 @@ *$(MSBuildProjectFullPath)* + + + + + + 3.3.2 diff --git a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/Resources.Designer.cs b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/Resources.Designer.cs index 8688a8b9c..9bef4241a 100644 --- a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/Resources.Designer.cs +++ b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/Resources.Designer.cs @@ -60,6 +60,33 @@ internal Resources() { } } + /// + /// Looks up a localized string similar to This type is not supported on .NET Core/.NET 5+ and should be replaced with a modern equivalent.. + /// + internal static string ApiAlertGenericDescription { + get { + return ResourceManager.GetString("ApiAlertGenericDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {}. + /// + internal static string ApiAlertGenericMessageFormat { + get { + return ResourceManager.GetString("ApiAlertGenericMessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Replace unsupported API. + /// + internal static string ApiAlertGenericTitle { + get { + return ResourceManager.GetString("ApiAlertGenericTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to This attribute type is not supported on .NET Core/.NET 5+ and should be replaced with a modern equivalent.. /// diff --git a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/Resources.resx b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/Resources.resx index 8f65d94c3..84c7636ae 100644 --- a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/Resources.resx +++ b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/Resources.resx @@ -117,6 +117,15 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + This type is not supported on .NET Core/.NET 5+ and should be replaced with a modern equivalent. + + + {} + + + Replace unsupported API + This attribute type is not supported on .NET Core/.NET 5+ and should be replaced with a modern equivalent. diff --git a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/TargetSyntax.cs b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/TargetSyntax.cs new file mode 100644 index 000000000..101345a8c --- /dev/null +++ b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/TargetSyntax.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers +{ + /// + /// Describes a syntax (namspace, type, or member) users might reference in their projects. + /// + public class TargetSyntax + { + private string _fullName = default!; + + /// + /// Gets the fully qualified name of the namespace or API. + /// + public string FullName + { + get => _fullName; + private init + { + _fullName = value; + SimpleName = _fullName.LastIndexOf('.') < 0 + ? _fullName + : _fullName.Substring(_fullName.LastIndexOf('.') + 1); + } + } + + /// + /// Gets the simple name of the namespace or API. + /// + public string SimpleName { get; private init; } = default!; + + /// + /// Gets the type of syntax. + /// + public TargetSyntaxType SyntaxType { get; } + + /// + /// Gets a value indicating whether the syntax should be matched if the user + /// uses syntax that ambiguously matches it. For example, if the simple name and + /// type match but no symbolic information is available and the used syntax doesn't have a + /// fully qualified name, the syntax will only be matched if this property is true. + /// + public bool AlertOnAmbiguousMatch { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The full name of the namespace or API. + /// The type of the syntax. + /// True if the syntax should be matched if only the simple name is matched and the full name is unknown. + public TargetSyntax(string fullName, TargetSyntaxType syntaxType, bool alertOnAmbiguousMatch) + { + FullName = fullName ?? throw new ArgumentNullException(nameof(fullName)); + SyntaxType = syntaxType; + AlertOnAmbiguousMatch = alertOnAmbiguousMatch; + } + } +} diff --git a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/TargetSyntaxMessage.cs b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/TargetSyntaxMessage.cs new file mode 100644 index 000000000..a17f7186e --- /dev/null +++ b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/TargetSyntaxMessage.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers +{ + /// + /// Describes a mapping of syntaxes (namespaces, types, or members) with a + /// diagnostic message that should be displayed if a user uses those syntaxes. + /// + public record TargetSyntaxMessage(string Id, IEnumerable TargetSyntaxes, string Message); +} diff --git a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/TargetSyntaxType.cs b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/TargetSyntaxType.cs new file mode 100644 index 000000000..ba0818529 --- /dev/null +++ b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/TargetSyntaxType.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers +{ + public enum TargetSyntaxType + { + /// + /// Syntax representing a namespace reference. + /// + Namespace, + + /// + /// Syntax representing a type or interface. + /// + Type, + + /// + /// Syntax representing a method, property, field, or event. + /// + Member + } +} From 09ffb870aa829ebff838819475314a9888c51936 Mon Sep 17 00:00:00 2001 From: Mike Rousos Date: Wed, 30 Jun 2021 16:45:31 -0400 Subject: [PATCH 02/11] Implement ApiAnalyzer --- .../SyntaxNodeExtensions.cs | 8 + .../AnalyzerReleases.Unshipped.md | 6 + .../ApiAlertAnalyzer.cs | 139 +++++++++++++----- .../AttributeUpgradeAnalyzer.cs | 3 +- ...inaryFormatterUnsafeDeserializeAnalyzer.cs | 2 +- .../DefaultApiAlerts.json | 22 +++ .../NameMatcher.cs | 41 +++++- .../TargetSyntax.cs | 11 +- .../AnalyzerTests.cs | 21 +++ .../TestHelper.cs | 3 +- .../assets/TestClasses/ApiAlert.cs | 46 ++++++ 11 files changed, 253 insertions(+), 49 deletions(-) create mode 100644 tests/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Test/assets/TestClasses/ApiAlert.cs diff --git a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Common/SyntaxNodeExtensions.cs b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Common/SyntaxNodeExtensions.cs index 026c1d4b7..3a2fbb905 100644 --- a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Common/SyntaxNodeExtensions.cs +++ b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Common/SyntaxNodeExtensions.cs @@ -275,6 +275,14 @@ private static bool RootIncludesImport(this SyntaxNode documentRoot, string name throw new NotImplementedException(Resources.UnknownLanguage); } + public static SyntaxNode? GetMAEExpressionSyntax(this SyntaxNode memberAccessSyntax) => + memberAccessSyntax switch + { + CSSyntax.MemberAccessExpressionSyntax csMAE => csMAE.Expression, + VBSyntax.MemberAccessExpressionSyntax vbMAE => vbMAE.Expression, + _ => null + }; + public static StringComparison GetStringComparison(this SyntaxNode? node) => node?.Language switch { diff --git a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/AnalyzerReleases.Unshipped.md b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/AnalyzerReleases.Unshipped.md index 1ad576767..4deafd8f8 100644 --- a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/AnalyzerReleases.Unshipped.md +++ b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/AnalyzerReleases.Unshipped.md @@ -13,3 +13,9 @@ UA0008 | Upgrade | Warning | UrlHelperAnalyzer UA0010 | Upgrade | Warning | AllowHtmlAttributeAnalyzer UA0011 | Upgrade | Warning | SystemDeploymentAnalyzer UA0012 | Upgrade | Warning | BinaryFormatterUnsafeDeserializeAnalyzer +UA0013 | Upgrade | Warning | ApiAlert +UA0013_A | Upgrade | Warning | HttpModuleAnalyzer +UA0013_B | Upgrade | Warning | ServiceHostAnalyzer +UA0013_C | Upgrade | Warning | BundleCollectionAnalyzer +UA0013_D | Upgrade | Warning | SystemDeploymentApplicationAnalyzer +UA0013_E | Upgrade | Warning | ChildActionOnlyAttributeAnalyzer diff --git a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/ApiAlertAnalyzer.cs b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/ApiAlertAnalyzer.cs index 1fc66f1f2..848103541 100644 --- a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/ApiAlertAnalyzer.cs +++ b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/ApiAlertAnalyzer.cs @@ -51,8 +51,9 @@ public class ApiAlertAnalyzer : DiagnosticAnalyzer /// The diagnsotic category for diagnostics produced by this analyzer. /// private const string Category = "Upgrade"; - + private const string AttributeSuffix = "Attribute"; private const string DefaultApiAlertsResourceName = "Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.DefaultApiAlerts.json"; + private IEnumerable _targetSyntaxes; public override ImmutableArray SupportedDiagnostics { get; } @@ -81,7 +82,7 @@ public ApiAlertAnalyzer() // Also add all API targets specified specifically in embedded resources supportedDiagnostics.AddRange(_targetSyntaxes.Select(s => - new DiagnosticDescriptor($"{BaseDiagnosticId}-{s.Id}", $"Replace usage of {string.Join(", ", s.TargetSyntaxes.Select(t => t.FullName))}", s.Message, Category, DiagnosticSeverity.Warning, true, s.Message))); + new DiagnosticDescriptor($"{BaseDiagnosticId}_{s.Id}", $"Replace usage of {string.Join(", ", s.TargetSyntaxes.Select(t => t.FullName))}", s.Message, Category, DiagnosticSeverity.Warning, true, s.Message))); SupportedDiagnostics = supportedDiagnostics.ToImmutable(); } @@ -111,55 +112,52 @@ public override void Initialize(AnalysisContext context) var combinedTargetSyntaxes = _targetSyntaxes.Concat(additionalTargetSyntaxes); // Register actions for handling both C# and VB identifiers - context.RegisterSyntaxNodeAction(context => AnalyzeCSharpIdentifier(context, combinedTargetSyntaxes), CS.SyntaxKind.IdentifierName); - context.RegisterSyntaxNodeAction(context => AnalyzeVBIdentifier(context, combinedTargetSyntaxes), VB.SyntaxKind.IdentifierName); + context.RegisterSyntaxNodeAction(context => AnalyzeIdentifier(context, combinedTargetSyntaxes), CS.SyntaxKind.IdentifierName); + context.RegisterSyntaxNodeAction(context => AnalyzeIdentifier(context, combinedTargetSyntaxes), VB.SyntaxKind.IdentifierName); }); } - private void AnalyzeCSharpIdentifier(SyntaxNodeAnalysisContext context, IEnumerable targetSyntaxMessages) + private void AnalyzeIdentifier(SyntaxNodeAnalysisContext context, IEnumerable targetSyntaxMessages) { - Console.WriteLine(); - var identifier = (CSSyntax.IdentifierNameSyntax)context.Node; - var symbol = identifier.Parent is CSSyntax.AttributeSyntax - ? context.SemanticModel.GetTypeInfo(identifier).Type - : context.SemanticModel.GetSymbolInfo(identifier).Symbol; + var simpleName = context.Node switch + { + CSSyntax.IdentifierNameSyntax csIdentifier => csIdentifier.Identifier.ValueText, + VBSyntax.IdentifierNameSyntax vbIdentifier => vbIdentifier.Identifier.ValueText, + _ => throw new InvalidOperationException($"Unsupported syntax kind (expected C# or VB identifier name): {context.Node.GetType()}") + }; - AnalyzeIdentifier(context, symbol, targetSyntaxMessages, identifier.Identifier.ValueText); - } + // Find target syntax/message mappings that include this node's simple name + var fullyQualifiedName = context.Node.GetQualifiedName().ToString(); + var stringComparison = context.Node.GetStringComparison(); + var partialMatches = targetSyntaxMessages.SelectMany(m => m.TargetSyntaxes.Select(s => (TargetSyntax: s, Mapping: m))) + .Where(t => t.TargetSyntax.SyntaxType is TargetSyntaxType.Member - private void AnalyzeVBIdentifier(SyntaxNodeAnalysisContext context, IEnumerable targetSyntaxMessages) - { - var identifier = (VBSyntax.IdentifierNameSyntax)context.Node; - var symbol = identifier.Parent is VBSyntax.AttributeSyntax - ? context.SemanticModel.GetTypeInfo(identifier).Type - : context.SemanticModel.GetSymbolInfo(identifier).Symbol; + // For members, check that the syntax is a method access expression and only match the simple name since + // the expression portion of a member access expression may be a local name + ? context.Node.IsMemberAccessExpression() && t.TargetSyntax.SimpleName.Equals(simpleName, stringComparison) - AnalyzeIdentifier(context, symbol, targetSyntaxMessages, identifier.Identifier.ValueText); - } + // For types and namespaces, the syntax's entire name needs to match the target + : t.TargetSyntax.NameMatcher.MatchesPartiallyQualifiedType(fullyQualifiedName, stringComparison) + || t.TargetSyntax.NameMatcher.MatchesPartiallyQualifiedType($"{fullyQualifiedName}{AttributeSuffix}", stringComparison)); - private void AnalyzeIdentifier(SyntaxNodeAnalysisContext context, ISymbol? symbol, IEnumerable targetSyntaxMessages, string simpleName) - { - // Find target syntax/message mappings that include this node's simple name - var possibleMatches = targetSyntaxMessages.SelectMany(m => m.TargetSyntaxes.Select(s => (TargetSyntax: s, Mapping: m))).Where(t => t.TargetSyntax.SimpleName.Equals(simpleName, StringComparison.Ordinal)); - - if (!possibleMatches.Any()) + if (!partialMatches.Any()) { return; } - foreach (var match in possibleMatches) + foreach (var match in partialMatches) { if (match.TargetSyntax.SyntaxType switch { - TargetSyntaxType.Member => AnalyzeMember(context.Node, symbol, simpleName, match.TargetSyntax), - TargetSyntaxType.Type => AnalyzeType(context.Node, symbol, simpleName, match.TargetSyntax), - TargetSyntaxType.Namespace => AnalyzeNamespace(context.Node, symbol, simpleName, match.TargetSyntax), - _ => false + TargetSyntaxType.Member => AnalyzeMember(context, match.TargetSyntax), + TargetSyntaxType.Type => AnalyzeType(context, match.TargetSyntax), + TargetSyntaxType.Namespace => AnalyzeNamespace(context, match.TargetSyntax), + _ => false, }) { // Get the diagnostic descriptor correspdoning to the target API message map or, if a specific descriptor doesn't exist for it, // get the default (first) one. - var id = $"{BaseDiagnosticId}-{match.Mapping.Id}"; + var id = $"{BaseDiagnosticId}_{match.Mapping.Id}"; var diagnosticDescriptor = SupportedDiagnostics.FirstOrDefault(d => d.Id.Equals(id, StringComparison.Ordinal)) ?? SupportedDiagnostics.First(); // Create and report the diagnostic. Note that the fully qualified name's location is used so @@ -169,22 +167,83 @@ private void AnalyzeIdentifier(SyntaxNodeAnalysisContext context, ISymbol? symbo } } - private bool AnalyzeNamespace(SyntaxNode node, ISymbol? symbol, string simpleName, TargetSyntax targetSyntax) + private static bool AnalyzeNamespace(SyntaxNodeAnalysisContext context, TargetSyntax targetSyntax) { - // TODO + // If the node is a fully qualified name from the specified namespace, return true + // This should cover both import statements and fully qualified type names, so no addtional checks + // for import statements are needed. + var qualifiedName = context.Node.GetQualifiedName().ToString(); + if (qualifiedName.Equals(targetSyntax.FullName, context.Node.GetStringComparison())) + { + return true; + } + + // This method intentionally doesn't check type symbols to see if they're part of the + // targeted namespace as that diagnostic would likely be too noisy. This will just flag + // the import statement or fully-qualified use of the namespace name, instead. return false; } - private bool AnalyzeType(SyntaxNode node, ISymbol? symbol, string simpleName, TargetSyntax targetSyntax) + private static bool AnalyzeType(SyntaxNodeAnalysisContext context, TargetSyntax targetSyntax) { - // TODO - return false; + // If the node matches the target's fully qualified name, return true. + var qualifiedName = context.Node.GetQualifiedName().ToString(); + if (qualifiedName.Equals(targetSyntax.FullName, context.Node.GetStringComparison())) + { + return true; + } + + // Attempt to get the type symbol (either by getting the type symbol directly, + // or by getting general symbol info, as required to get the type symbol a ctor + // corresponds to) + var symbol = context.SemanticModel.GetTypeInfo(context.Node).Type + ?? context.SemanticModel.GetSymbolInfo(context.Node).Symbol; + + // If the node's type can be resolved, return true only if it matches + // the expected full name. If the node resolves to a non-type symbol, + // return false as this could indicate a local variable or something similar + // with a name that matches the target. + if (symbol is not null) + { + if (symbol is ITypeSymbol typeSymbol && typeSymbol is not IErrorTypeSymbol) + { + return targetSyntax.NameMatcher.Matches(typeSymbol); + } + else + { + return false; + } + } + + // If the node's full type can't be determined (either by symbol or fully qualified syntax), + // return true for the partial match only if ambiguous matching is enabled + return targetSyntax.AlertOnAmbiguousMatch; } - private bool AnalyzeMember(SyntaxNode node, ISymbol? symbol, string simpleName, TargetSyntax targetSyntax) + private static bool AnalyzeMember(SyntaxNodeAnalysisContext context, TargetSyntax targetSyntax) { - // TODO - return false; + // If the node matches the target's fully qualified name, return true. + var qualifiedName = context.Node.GetQualifiedName().ToString(); + if (qualifiedName.Equals(targetSyntax.FullName, context.Node.GetStringComparison())) + { + return true; + } + + // If the parent type's symbol is resolvable, return true if + // it corresponds to the target type + var typeSyntax = context.Node.GetMAEExpressionSyntax(); + if (typeSyntax is not null) + { + var symbol = context.SemanticModel.GetTypeInfo(typeSyntax).Type; + if (symbol is not null && symbol is not IErrorTypeSymbol) + { + return targetSyntax.NameMatcher.Matches(symbol); + } + } + + // If the node's full type can't be determined (either by symbol or fully qualified syntax), + // return true for the partial match only if ambiguous matching is enabled + return targetSyntax.AlertOnAmbiguousMatch; } } } diff --git a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/AttributeUpgradeAnalyzer.cs b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/AttributeUpgradeAnalyzer.cs index 7347c73f4..e323cb0c6 100644 --- a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/AttributeUpgradeAnalyzer.cs +++ b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/AttributeUpgradeAnalyzer.cs @@ -77,10 +77,11 @@ private static void AnalyzeAttribute(SyntaxNodeAnalysisContext context, IEnumera } // If the attribute name isn't one of the mapped names, bail out + var stringComparison = context.Node.GetStringComparison(); var mapping = mappings.FirstOrDefault(m => { var matcher = NameMatcher.MatchType(m.OldName); - return matcher.MatchesPartiallyQualifiedType(attributeName) || matcher.MatchesPartiallyQualifiedType($"{attributeName}{AttributeSuffix}"); + return matcher.MatchesPartiallyQualifiedType(attributeName, stringComparison) || matcher.MatchesPartiallyQualifiedType($"{attributeName}{AttributeSuffix}", stringComparison); }); if (mapping is null) { diff --git a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/BinaryFormatterUnsafeDeserializeAnalyzer.cs b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/BinaryFormatterUnsafeDeserializeAnalyzer.cs index 8d66769cf..ed6017ab2 100644 --- a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/BinaryFormatterUnsafeDeserializeAnalyzer.cs +++ b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/BinaryFormatterUnsafeDeserializeAnalyzer.cs @@ -26,7 +26,7 @@ public sealed class BinaryFormatterUnsafeDeserializeAnalyzer : DiagnosticAnalyze private static readonly DiagnosticDescriptor Rule = new(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description); - public static NameMatcher BinaryFormatterUnsafeDeserialize { get; } = NameMatcher.MatchPropertyAccess(QualifiedTargetSymbolName, TargetMember); + public static NameMatcher BinaryFormatterUnsafeDeserialize { get; } = NameMatcher.MatchMemberAccess(QualifiedTargetSymbolName, TargetMember); public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); diff --git a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/DefaultApiAlerts.json b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/DefaultApiAlerts.json index 080631132..6b6d8014e 100644 --- a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/DefaultApiAlerts.json +++ b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/DefaultApiAlerts.json @@ -41,5 +41,27 @@ } ], "Message": "Script and style bundling works differently in ASP.NET Core. BundleCollection should be replaced by alternative bundling technologies. https://docs.microsoft.com/en-us/aspnet/core/client-side/bundling-and-minification" + }, + { + "Id": "D", + "TargetSyntaxes": [ + { + "FullName": "System.Deployment.Application", + "SyntaxType": "Namespace", + "AlertOnAmbiguousMatch": false + } + ], + "Message": "Although ClickOnce is supported on .NET 5+, apps do not have access to the System.Deployment.Application namespace. For more details see https://github.com/dotnet/deployment-tools/issues/27 and https://github.com/dotnet/deployment-tools/issues/53" + }, + { + "Id": "E", + "TargetSyntaxes": [ + { + "FullName": "System.Web.Mvc.ChildActionOnlyAttribute", + "SyntaxType": "Type", + "AlertOnAmbiguousMatch": true + } + ], + "Message": "Child actions should be replaced with view components. For more details, see https://docs.microsoft.com/en-us/aspnet/core/mvc/views/view-components and https://www.davepaquette.com/archive/2016/01/02/goodbye-child-actions-hello-view-components.aspx" } ] \ No newline at end of file diff --git a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/NameMatcher.cs b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/NameMatcher.cs index c1cb6585d..e8b596ee5 100644 --- a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/NameMatcher.cs +++ b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/NameMatcher.cs @@ -25,11 +25,27 @@ private NameMatcher(string typeName, string? memberName = null) public static NameMatcher MatchType(string typeName) => new NameMatcher(typeName); - public static NameMatcher MatchPropertyAccess(string typeName, string memberName) => new(typeName, memberName); + public static NameMatcher MatchMemberAccess(string memberName) + { + if (memberName is null) + { + throw new ArgumentNullException(nameof(memberName)); + } + + var idx = memberName.LastIndexOf('.'); + if (idx < 0) + { + throw new ArgumentException("Member name must be fully qualified"); + } + + return new NameMatcher(memberName.Substring(0, idx), memberName.Substring(idx + 1)); + } + + public static NameMatcher MatchMemberAccess(string typeName, string memberName) => new(typeName, memberName); public string TypeName => _typeName[_typeName.Length - 1]; - public bool MatchesPartiallyQualifiedType(string partiallyQualifiedTypeName) + public bool MatchesPartiallyQualifiedType(string partiallyQualifiedTypeName, StringComparison stringComparison) { if (partiallyQualifiedTypeName is null) { @@ -51,7 +67,7 @@ public bool MatchesPartiallyQualifiedType(string partiallyQualifiedTypeName) for (var i = 1; i <= partialName.Length; i++) { - if (!partialName[partialName.Length - i].Equals(_typeName[_typeName.Length - i], StringComparison.Ordinal)) + if (!partialName[partialName.Length - i].Equals(_typeName[_typeName.Length - i], stringComparison)) { return false; } @@ -61,6 +77,23 @@ public bool MatchesPartiallyQualifiedType(string partiallyQualifiedTypeName) }); } + public bool MatchesPartiallyQualifiedMember(string partiallyQualifiedMemberName, StringComparison stringComparison) + { + if (string.IsNullOrEmpty(partiallyQualifiedMemberName)) + { + return false; + } + + return _partialMatchCache.GetOrAdd(partiallyQualifiedMemberName, p => + { + var idx = p.LastIndexOf('.'); + var typeName = p.Substring(0, idx); + var memberName = p.Substring(idx + 1); + + return memberName.Equals(_memberName, stringComparison) && MatchesPartiallyQualifiedType(typeName, stringComparison); + }); + } + public bool Matches(IPropertySymbol property) { if (property is null) @@ -99,7 +132,7 @@ public bool Matches(ITypeSymbol? typeSymbol) return false; } - if (!string.Equals(symbol.Name, _typeName[i], StringComparison.OrdinalIgnoreCase)) + if (!string.Equals(symbol.Name, _typeName[i], StringComparison.Ordinal)) { return false; } diff --git a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/TargetSyntax.cs b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/TargetSyntax.cs index 101345a8c..fe1296d45 100644 --- a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/TargetSyntax.cs +++ b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/TargetSyntax.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Collections.Generic; -using System.Text; namespace Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers { @@ -47,6 +45,12 @@ private init /// public bool AlertOnAmbiguousMatch { get; } + /// + /// Gets a NameMatcher that can be used to determine whether syntax nodes or symbols + /// matche this target syntax. + /// + public NameMatcher NameMatcher { get; } + /// /// Initializes a new instance of the class. /// @@ -58,6 +62,9 @@ public TargetSyntax(string fullName, TargetSyntaxType syntaxType, bool alertOnAm FullName = fullName ?? throw new ArgumentNullException(nameof(fullName)); SyntaxType = syntaxType; AlertOnAmbiguousMatch = alertOnAmbiguousMatch; + NameMatcher = syntaxType is TargetSyntaxType.Member + ? NameMatcher.MatchMemberAccess(fullName) + : NameMatcher.MatchType(fullName); } } } diff --git a/tests/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Test/AnalyzerTests.cs b/tests/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Test/AnalyzerTests.cs index 90714570a..597d56b59 100644 --- a/tests/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Test/AnalyzerTests.cs +++ b/tests/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Test/AnalyzerTests.cs @@ -205,6 +205,26 @@ public class AnalyzerTests new ExpectedDiagnostic("UA0010", new TextSpan(845, 13), Language.VisualBasic), } }, + { + "ApiAlert", + new[] + { + new ExpectedDiagnostic("UA0013_D", new TextSpan(272, 29)), + new ExpectedDiagnostic("UA0013_D", new TextSpan(310, 29)), + new ExpectedDiagnostic("UA0013_A", new TextSpan(534, 11)), + new ExpectedDiagnostic("UA0013_A", new TextSpan(547, 23)), + new ExpectedDiagnostic("UA0013_D", new TextSpan(604, 29)), + new ExpectedDiagnostic("UA0013_D", new TextSpan(848, 29)), + new ExpectedDiagnostic("UA0013_D", new TextSpan(916, 29)), + new ExpectedDiagnostic("UA0013_E", new TextSpan(987, 15)), + new ExpectedDiagnostic("UA0013_B", new TextSpan(1046, 15)), + new ExpectedDiagnostic("UA0013_E", new TextSpan(1091, 39)), + new ExpectedDiagnostic("UA0013_E", new TextSpan(1139, 28)), + new ExpectedDiagnostic("UA0013_B", new TextSpan(1184, 11)), + new ExpectedDiagnostic("UA0013_C", new TextSpan(1249, 16)), + new ExpectedDiagnostic("UA0013_C", new TextSpan(1270, 33)), + } + }, }; // No diagnostics expected to show up @@ -232,6 +252,7 @@ public async Task NegativeTest() [InlineData("UA0012")] [InlineData("ControllerUpgrade")] [InlineData("AttributesTest")] + [InlineData("ApiAlert")] [Theory] public async Task UpgradeAnalyzers(string scenarioName) { diff --git a/tests/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Test/TestHelper.cs b/tests/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Test/TestHelper.cs index c355cff85..089495b30 100644 --- a/tests/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Test/TestHelper.cs +++ b/tests/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Test/TestHelper.cs @@ -34,7 +34,8 @@ public static class TestHelper new HttpContextIsDebuggingEnabledAnalyzer(), new TypeUpgradeAnalyzer(), new UsingSystemWebAnalyzer(), - new UrlHelperAnalyzer()); + new UrlHelperAnalyzer(), + new ApiAlertAnalyzer()); internal static ImmutableArray AllCodeFixProviders => ImmutableArray.Create( new AttributeUpgradeCodeFixer(), diff --git a/tests/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Test/assets/TestClasses/ApiAlert.cs b/tests/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Test/assets/TestClasses/ApiAlert.cs new file mode 100644 index 000000000..e5d23cd58 --- /dev/null +++ b/tests/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Test/assets/TestClasses/ApiAlert.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Deployment.Internal; +using System.Deployment.Application; +using System.Deployment.Application.Foo; +using System.Threading.Tasks; +using System.Web.Mvc; + +namespace Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Test.assets.TestClasses +{ + public class ApiAlertTest: IHttpModule, System.Web.IHttpHandler, Foo.IHttpModule + { + System.Deployment.Application.ApplicationDeployment x { get; set; } + ApplicationDeployment y; + + public static void TestMethod() + { + var z = new Application.ApplicationDeployment(); + var z1 = new System.Deployment.Application.ApplicationDeployment(); + System.Deployment.Application.Foo.Bar x = null; + } + + [ChildActionOnly] + public static void TypeAlertTest(ServiceHostBase svc) + { + System.Web.Mvc.ChildActionOnlyAttribute a = new Mvc.ChildActionOnlyAttribute(); + ServiceHost x = new ServiceModel.ServiceHostBase(); + BundleCollection b = Web.Optimization.BundleCollection.SomeMethod(); + } + } +} + +namespace Foo +{ + public interface IHttpModule { } +} + +namespace ServiceModel +{ + public class ServiceHostBase { } +} From c34783b6afc1160a993cb445d1a4fc5d9f105c32 Mon Sep 17 00:00:00 2001 From: Mike Rousos Date: Fri, 2 Jul 2021 10:34:48 -0400 Subject: [PATCH 03/11] Add the ability to load target syntax messages dynamically --- .../AnalyzerReleases.Unshipped.md | 3 + .../ApiAlertAnalyzer.cs | 110 ++++++++---------- .../DefaultApiAlerts.json | 61 +++++++++- .../TargetSyntaxMessageLoader.cs | 65 +++++++++++ .../AnalyzerTests.cs | 10 ++ .../assets/TestClasses/ApiAlert.cs | 22 ++++ 6 files changed, 205 insertions(+), 66 deletions(-) create mode 100644 src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/TargetSyntaxMessageLoader.cs diff --git a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/AnalyzerReleases.Unshipped.md b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/AnalyzerReleases.Unshipped.md index 4deafd8f8..ad6996bb7 100644 --- a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/AnalyzerReleases.Unshipped.md +++ b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/AnalyzerReleases.Unshipped.md @@ -19,3 +19,6 @@ UA0013_B | Upgrade | Warning | ServiceHostAnalyzer UA0013_C | Upgrade | Warning | BundleCollectionAnalyzer UA0013_D | Upgrade | Warning | SystemDeploymentApplicationAnalyzer UA0013_E | Upgrade | Warning | ChildActionOnlyAttributeAnalyzer +UA0013_F | Upgrade | Warning | AspNetMembership +UA0013_G | Upgrade | Warning | AspNetIdentity +UA0013_H | Upgrade | Warning | HttpRequestRawUrl diff --git a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/ApiAlertAnalyzer.cs b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/ApiAlertAnalyzer.cs index 848103541..e059078fa 100644 --- a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/ApiAlertAnalyzer.cs +++ b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/ApiAlertAnalyzer.cs @@ -6,8 +6,6 @@ using System.Collections.Immutable; using System.IO; using System.Linq; -using System.Text.Json; -using System.Text.Json.Serialization; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using CS = Microsoft.CodeAnalysis.CSharp; @@ -17,23 +15,6 @@ namespace Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers { - /* - Extenders can supply: - 1. An API to alert on - 2. Message - 3. Whether the API needs to resolve to match symbolically or not (maybe call 'support partial syntax match'?) - - Scenarios to support: - - Base types and interfaces (name syntax) - - HttpApplication, IHttpModule - - Attributes (ChildActionOnlyAttribute) - - Method calls or property invocations (member access syntax)? - Do I need this or not? There are some things that I'd want to flag on the - method level but *most* could be on the type level. - - global.asax.cs registration APIs (RouteCollection.MapMvcAttributeRoutes) - - Removed HttpContext APIs? - */ - /// /// Analyzer for identifying usage of APIs that should be reported to the /// user along with messaging about how the API should be (manually) replaced @@ -54,38 +35,22 @@ public class ApiAlertAnalyzer : DiagnosticAnalyzer private const string AttributeSuffix = "Attribute"; private const string DefaultApiAlertsResourceName = "Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.DefaultApiAlerts.json"; - private IEnumerable _targetSyntaxes; - - public override ImmutableArray SupportedDiagnostics { get; } + private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.AttributeUpgradeTitle), Resources.ResourceManager, typeof(Resources)); + private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.AttributeUpgradeMessageFormat), Resources.ResourceManager, typeof(Resources)); + private static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.AttributeUpgradeDescription), Resources.ResourceManager, typeof(Resources)); + private static readonly DiagnosticDescriptor GenericRule = new(BaseDiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description); - public ApiAlertAnalyzer() + private Lazy> _targetSyntaxes = new Lazy>(() => { - var jsonSerializerOptions = new JsonSerializerOptions - { - AllowTrailingCommas = true, - }; - - jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); - using var resourceStream = new StreamReader(typeof(ApiAlertAnalyzer).Assembly.GetManifestResourceStream(DefaultApiAlertsResourceName)); - _targetSyntaxes = JsonSerializer.Deserialize(resourceStream.ReadToEnd(), jsonSerializerOptions) + return TargetSyntaxMessageLoader.LoadMappings(resourceStream.ReadToEnd()) ?? throw new InvalidOperationException($"Could not read target syntax messages from resource {DefaultApiAlertsResourceName}"); + }); - // Assemby the list of supported diagnostics - var supportedDiagnostics = ImmutableArray.CreateBuilder(); - - // First, support a generic diagnostic that will be used for API targets specified in additional files - var genericTitle = new LocalizableResourceString(nameof(Resources.ApiAlertGenericTitle), Resources.ResourceManager, typeof(Resources)); - var genericMessageFormat = new LocalizableResourceString(nameof(Resources.ApiAlertGenericMessageFormat), Resources.ResourceManager, typeof(Resources)); - var genericDesription = new LocalizableResourceString(nameof(Resources.ApiAlertGenericDescription), Resources.ResourceManager, typeof(Resources)); - supportedDiagnostics.Add(new(BaseDiagnosticId, genericTitle, genericMessageFormat, Category, DiagnosticSeverity.Warning, true, genericDesription)); - - // Also add all API targets specified specifically in embedded resources - supportedDiagnostics.AddRange(_targetSyntaxes.Select(s => - new DiagnosticDescriptor($"{BaseDiagnosticId}_{s.Id}", $"Replace usage of {string.Join(", ", s.TargetSyntaxes.Select(t => t.FullName))}", s.Message, Category, DiagnosticSeverity.Warning, true, s.Message))); - - SupportedDiagnostics = supportedDiagnostics.ToImmutable(); - } + // Supported diagnostics include all of the specific diagnostics read from DefaultApiAlerts.json and the generic diagnostic used for additional target syntax messages loaded at runtime. + public override ImmutableArray SupportedDiagnostics => ImmutableArray.CreateRange(_targetSyntaxes.Value + .Select(t => new DiagnosticDescriptor($"{BaseDiagnosticId}_{t.Id}", $"Replace usage of {string.Join(", ", t.TargetSyntaxes.Select(a => a.FullName))}", t.Message, Category, DiagnosticSeverity.Warning, true, t.Message)) + .Concat(new[] { GenericRule })); /// /// Initializes the analyzer by registering analysis callback methods. @@ -105,15 +70,13 @@ public override void Initialize(AnalysisContext context) { // Load analyzer configuration defining the types that should be mapped. var additionalTargetSyntaxes = - // TODO : - // TargetSyntaxMessageLoader.LoadMappings(context.Options.AdditionalFiles); - Enumerable.Empty(); + TargetSyntaxMessageLoader.LoadMappings(context.Options.AdditionalFiles); - var combinedTargetSyntaxes = _targetSyntaxes.Concat(additionalTargetSyntaxes); + var combinedTargetSyntaxes = _targetSyntaxes.Value.Concat(additionalTargetSyntaxes); // Register actions for handling both C# and VB identifiers - context.RegisterSyntaxNodeAction(context => AnalyzeIdentifier(context, combinedTargetSyntaxes), CS.SyntaxKind.IdentifierName); - context.RegisterSyntaxNodeAction(context => AnalyzeIdentifier(context, combinedTargetSyntaxes), VB.SyntaxKind.IdentifierName); + context.RegisterSyntaxNodeAction(context => AnalyzeIdentifier(context, combinedTargetSyntaxes), CS.SyntaxKind.IdentifierName, CS.SyntaxKind.GenericName); + context.RegisterSyntaxNodeAction(context => AnalyzeIdentifier(context, combinedTargetSyntaxes), VB.SyntaxKind.IdentifierName, VB.SyntaxKind.GenericName); }); } @@ -121,20 +84,21 @@ private void AnalyzeIdentifier(SyntaxNodeAnalysisContext context, IEnumerable csIdentifier.Identifier.ValueText, - VBSyntax.IdentifierNameSyntax vbIdentifier => vbIdentifier.Identifier.ValueText, + CSSyntax.SimpleNameSyntax csIdentifier => csIdentifier.Identifier.ValueText, + VBSyntax.SimpleNameSyntax vbIdentifier => vbIdentifier.Identifier.ValueText, _ => throw new InvalidOperationException($"Unsupported syntax kind (expected C# or VB identifier name): {context.Node.GetType()}") }; // Find target syntax/message mappings that include this node's simple name - var fullyQualifiedName = context.Node.GetQualifiedName().ToString(); + var fullyQualifiedName = GetFullName(context.Node); + var stringComparison = context.Node.GetStringComparison(); var partialMatches = targetSyntaxMessages.SelectMany(m => m.TargetSyntaxes.Select(s => (TargetSyntax: s, Mapping: m))) .Where(t => t.TargetSyntax.SyntaxType is TargetSyntaxType.Member // For members, check that the syntax is a method access expression and only match the simple name since // the expression portion of a member access expression may be a local name - ? context.Node.IsMemberAccessExpression() && t.TargetSyntax.SimpleName.Equals(simpleName, stringComparison) + ? (context.Node.Parent?.IsMemberAccessExpression() ?? false) && t.TargetSyntax.SimpleName.Equals(simpleName, stringComparison) // For types and namespaces, the syntax's entire name needs to match the target : t.TargetSyntax.NameMatcher.MatchesPartiallyQualifiedType(fullyQualifiedName, stringComparison) @@ -172,7 +136,7 @@ private static bool AnalyzeNamespace(SyntaxNodeAnalysisContext context, TargetSy // If the node is a fully qualified name from the specified namespace, return true // This should cover both import statements and fully qualified type names, so no addtional checks // for import statements are needed. - var qualifiedName = context.Node.GetQualifiedName().ToString(); + var qualifiedName = GetFullName(context.Node); if (qualifiedName.Equals(targetSyntax.FullName, context.Node.GetStringComparison())) { return true; @@ -187,7 +151,7 @@ private static bool AnalyzeNamespace(SyntaxNodeAnalysisContext context, TargetSy private static bool AnalyzeType(SyntaxNodeAnalysisContext context, TargetSyntax targetSyntax) { // If the node matches the target's fully qualified name, return true. - var qualifiedName = context.Node.GetQualifiedName().ToString(); + var qualifiedName = GetFullName(context.Node); if (qualifiedName.Equals(targetSyntax.FullName, context.Node.GetStringComparison())) { return true; @@ -205,9 +169,17 @@ private static bool AnalyzeType(SyntaxNodeAnalysisContext context, TargetSyntax // with a name that matches the target. if (symbol is not null) { - if (symbol is ITypeSymbol typeSymbol && typeSymbol is not IErrorTypeSymbol) + if (symbol is ITypeSymbol typeSymbol) { - return targetSyntax.NameMatcher.Matches(typeSymbol); + // This conditional can't be combined with the previous one because + // failing this condition should not lead to the else clause. + // In other words, if the symbol is a type symbol but is an error type + // symbol then don't return here (rather than returning false as would + // happen for non-type symbols). + if (typeSymbol is not IErrorTypeSymbol) + { + return targetSyntax.NameMatcher.Matches(typeSymbol); + } } else { @@ -223,7 +195,7 @@ private static bool AnalyzeType(SyntaxNodeAnalysisContext context, TargetSyntax private static bool AnalyzeMember(SyntaxNodeAnalysisContext context, TargetSyntax targetSyntax) { // If the node matches the target's fully qualified name, return true. - var qualifiedName = context.Node.GetQualifiedName().ToString(); + var qualifiedName = GetFullName(context.Node); if (qualifiedName.Equals(targetSyntax.FullName, context.Node.GetStringComparison())) { return true; @@ -231,7 +203,7 @@ private static bool AnalyzeMember(SyntaxNodeAnalysisContext context, TargetSynta // If the parent type's symbol is resolvable, return true if // it corresponds to the target type - var typeSyntax = context.Node.GetMAEExpressionSyntax(); + var typeSyntax = context.Node.Parent?.GetMAEExpressionSyntax(); if (typeSyntax is not null) { var symbol = context.SemanticModel.GetTypeInfo(typeSyntax).Type; @@ -245,5 +217,19 @@ private static bool AnalyzeMember(SyntaxNodeAnalysisContext context, TargetSynta // return true for the partial match only if ambiguous matching is enabled return targetSyntax.AlertOnAmbiguousMatch; } + + private static string GetFullName(SyntaxNode node) + { + var fullName = node.GetQualifiedName().ToString(); + + // Ignore generic parameters for now to simplify matching + var genericParameterIndex = fullName.IndexOfAny(new[] { '<', '(' }); + if (genericParameterIndex > 0) + { + fullName = fullName.Substring(0, genericParameterIndex); + } + + return fullName; + } } } diff --git a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/DefaultApiAlerts.json b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/DefaultApiAlerts.json index 6b6d8014e..a00f7242c 100644 --- a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/DefaultApiAlerts.json +++ b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/DefaultApiAlerts.json @@ -13,7 +13,7 @@ "AlertOnAmbiguousMatch": true } ], - "Message": "HTTP modules and HTTP handlers should be rewritten as middleware in ASP.NET Core. https://docs.microsoft.com/en-us/aspnet/core/migration/http-modulesIHttpModule", + "Message": "HTTP modules and HTTP handlers should be rewritten as middleware in ASP.NET Core. https://docs.microsoft.com/aspnet/core/migration/http-modulesIHttpModule", }, { "Id": "B", @@ -29,7 +29,7 @@ "AlertOnAmbiguousMatch": true } ], - "Message": "WCF server APIs are unsupported on .NET Core. Consider rewriting to use gRPC (https://docs.microsoft.com/en-us/dotnet/architecture/grpc-for-wcf-developers), ASP.NET Core, or CoreWCF (https://github.com/CoreWCF/CoreWCF) instead.", + "Message": "WCF server APIs are unsupported on .NET Core. Consider rewriting to use gRPC (https://docs.microsoft.com/dotnet/architecture/grpc-for-wcf-developers), ASP.NET Core, or CoreWCF (https://github.com/CoreWCF/CoreWCF) instead.", }, { "Id": "C", @@ -40,7 +40,7 @@ "AlertOnAmbiguousMatch": true } ], - "Message": "Script and style bundling works differently in ASP.NET Core. BundleCollection should be replaced by alternative bundling technologies. https://docs.microsoft.com/en-us/aspnet/core/client-side/bundling-and-minification" + "Message": "Script and style bundling works differently in ASP.NET Core. BundleCollection should be replaced by alternative bundling technologies. https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification" }, { "Id": "D", @@ -62,6 +62,59 @@ "AlertOnAmbiguousMatch": true } ], - "Message": "Child actions should be replaced with view components. For more details, see https://docs.microsoft.com/en-us/aspnet/core/mvc/views/view-components and https://www.davepaquette.com/archive/2016/01/02/goodbye-child-actions-hello-view-components.aspx" + "Message": "Child actions should be replaced with view components. For more details, see https://docs.microsoft.com/aspnet/core/mvc/views/view-components and https://www.davepaquette.com/archive/2016/01/02/goodbye-child-actions-hello-view-components.aspx" + }, + { + "Id": "F", + "TargetSyntaxes": [ + { + "FullName": "System.Web.Security.Membership", + "SyntaxType": "Type", + "AlertOnAmbiguousMatch": true + }, + { + "FullName": "System.Web.Security.FormsAuthentication", + "SyntaxType": "Type", + "AlertOnAmbiguousMatch": true + } + ], + "Message": "ASP.NET membership should be replaced with ASP.NET Core identity. For more details, see https://docs.microsoft.com/aspnet/core/migration/proper-to-2x/membership-to-core-identity" + }, + { + "Id": "G", + "TargetSyntaxes": [ + { + "FullName": "Microsoft.AspNet.Identity", + "SyntaxType": "Namespace", + "AlertOnAmbiguousMatch": false + }, + { + "FullName": "Microsoft.AspNet.Identity.UserManager", + "SyntaxType": "Type", + "AlertOnAmbiguousMatch": true + }, + { + "FullName": "Microsoft.AspNet.Identity.Owin.SignInManager", + "SyntaxType": "Type", + "AlertOnAmbiguousMatch": true + } + ], + "Message": "ASP.NET identity should be replaced with ASP.NET Core identity. For more details, see https://docs.microsoft.com/aspnet/core/migration/identity" + }, + { + "Id": "H", + "TargetSyntaxes": [ + { + "FullName": "Microsoft.AspNetCore.Http.HttpRequest.RawUrl", + "SyntaxType": "Member", + "AlertOnAmbiguousMatch": false + }, + { + "FullName": "System.Web.HttpRequest.RawUrl", + "SyntaxType": "Member", + "AlertOnAmbiguousMatch": false + } + ], + "Message": "HttpRequest.RawUrl should be replaced with HttpRequest.GetEncodedUrl() or HttpRequest.GetDisplayUrl()" } ] \ No newline at end of file diff --git a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/TargetSyntaxMessageLoader.cs b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/TargetSyntaxMessageLoader.cs new file mode 100644 index 000000000..89888c444 --- /dev/null +++ b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/TargetSyntaxMessageLoader.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.CodeAnalysis; + +namespace Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers +{ + public static class TargetSyntaxMessageLoader + { + /// + /// The suffix that api/message map files are expected to use. + /// + private const string TargetSyntaxMessageFileSuffix = ".apitargets"; + + private static JsonSerializerOptions GetJsonSerializationOptions() + { + var jsonSerializerOptions = new JsonSerializerOptions + { + AllowTrailingCommas = true, + }; + + jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + + return jsonSerializerOptions; + } + + /// + /// Load target syntax messages from a serialized string. + /// + /// A JSON-serialized array of target syntax messages. + /// An enumerable of TargetSyntaxMessages corresponding to those serialized in the serializedContents argument. + public static IEnumerable? LoadMappings(string serializedContents) => + JsonSerializer.Deserialize(serializedContents, GetJsonSerializationOptions()); + + /// + /// Load target syntax messages from additional files. + /// + /// The additional texts to parse for type mappings. + /// Target syntax messages as defined in *.apitargets files in the project's additional files. + public static IEnumerable LoadMappings(ImmutableArray additionalTexts) + { + foreach (var file in additionalTexts) + { + if (file.Path.EndsWith(TargetSyntaxMessageFileSuffix, StringComparison.OrdinalIgnoreCase)) + { + var messageMaps = LoadMappings(file.GetText()?.ToString() ?? "[]"); + if (messageMaps is null) + { + continue; + } + + foreach (var messageMap in messageMaps) + { + yield return messageMap; + } + } + } + } + } +} diff --git a/tests/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Test/AnalyzerTests.cs b/tests/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Test/AnalyzerTests.cs index 597d56b59..db7fba408 100644 --- a/tests/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Test/AnalyzerTests.cs +++ b/tests/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Test/AnalyzerTests.cs @@ -223,6 +223,16 @@ public class AnalyzerTests new ExpectedDiagnostic("UA0013_B", new TextSpan(1184, 11)), new ExpectedDiagnostic("UA0013_C", new TextSpan(1249, 16)), new ExpectedDiagnostic("UA0013_C", new TextSpan(1270, 33)), + new ExpectedDiagnostic("UA0013_G", new TextSpan(1354, 27)), + new ExpectedDiagnostic("UA0013_F", new TextSpan(1477, 23)), + new ExpectedDiagnostic("UA0013_F", new TextSpan(1568, 39)), + + // Once for the namespace, once for the type + new ExpectedDiagnostic("UA0013_G", new TextSpan(1658, 25)), + new ExpectedDiagnostic("UA0013_G", new TextSpan(1658, 60)), + + new ExpectedDiagnostic("UA0013_H", new TextSpan(2057, 15)), + new ExpectedDiagnostic("UA0013_H", new TextSpan(2131, 29)), } }, }; diff --git a/tests/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Test/assets/TestClasses/ApiAlert.cs b/tests/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Test/assets/TestClasses/ApiAlert.cs index e5d23cd58..8e22e3cd2 100644 --- a/tests/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Test/assets/TestClasses/ApiAlert.cs +++ b/tests/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Test/assets/TestClasses/ApiAlert.cs @@ -32,7 +32,29 @@ public static void TypeAlertTest(ServiceHostBase svc) ServiceHost x = new ServiceModel.ServiceHostBase(); BundleCollection b = Web.Optimization.BundleCollection.SomeMethod(); } + + public static UserManager MembershipTest(Foo.Membership membership) + { + MembershipUser currentUser = Web.Security.Membership.GetUser(User.Identity.Name, true /* userIsOnline */); + System.Web.Security.FormsAuthentication.SetAuthCookie(currentUser.UserName); + Microsoft.AspNet.Identity.Owin.SignInManager = new SignInManager(); + } + + public HttpRequest MemberTest() + { + var x = HttpRequest.RawUrl; + System.Web.HttpRequest request1; + Microsoft.AspNetCore.Http.HttpRequest request2; + HttpRequest request3; + x = request1.RawUrl; + x = request2.RawUrl; + x = request3.RawUrl; + + var y = System.Web.HttpRequest.RawUrl; + } } + + public class SignInManager { } } namespace Foo From 624ce9a8fb0e76f3ea3c9b0ba24d1ae079853459 Mon Sep 17 00:00:00 2001 From: Mike Rousos Date: Fri, 2 Jul 2021 11:24:07 -0400 Subject: [PATCH 04/11] Fix-up AnalyzerReleases.Unshipped.md --- .../AnalyzerReleases.Unshipped.md | 8 -------- .../ApiAlertAnalyzer.cs | 4 +++- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/AnalyzerReleases.Unshipped.md b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/AnalyzerReleases.Unshipped.md index ad6996bb7..833e8e281 100644 --- a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/AnalyzerReleases.Unshipped.md +++ b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/AnalyzerReleases.Unshipped.md @@ -14,11 +14,3 @@ UA0010 | Upgrade | Warning | AllowHtmlAttributeAnalyzer UA0011 | Upgrade | Warning | SystemDeploymentAnalyzer UA0012 | Upgrade | Warning | BinaryFormatterUnsafeDeserializeAnalyzer UA0013 | Upgrade | Warning | ApiAlert -UA0013_A | Upgrade | Warning | HttpModuleAnalyzer -UA0013_B | Upgrade | Warning | ServiceHostAnalyzer -UA0013_C | Upgrade | Warning | BundleCollectionAnalyzer -UA0013_D | Upgrade | Warning | SystemDeploymentApplicationAnalyzer -UA0013_E | Upgrade | Warning | ChildActionOnlyAttributeAnalyzer -UA0013_F | Upgrade | Warning | AspNetMembership -UA0013_G | Upgrade | Warning | AspNetIdentity -UA0013_H | Upgrade | Warning | HttpRequestRawUrl diff --git a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/ApiAlertAnalyzer.cs b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/ApiAlertAnalyzer.cs index e059078fa..b4705c57a 100644 --- a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/ApiAlertAnalyzer.cs +++ b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/ApiAlertAnalyzer.cs @@ -40,7 +40,7 @@ public class ApiAlertAnalyzer : DiagnosticAnalyzer private static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.AttributeUpgradeDescription), Resources.ResourceManager, typeof(Resources)); private static readonly DiagnosticDescriptor GenericRule = new(BaseDiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description); - private Lazy> _targetSyntaxes = new Lazy>(() => + private readonly Lazy> _targetSyntaxes = new(() => { using var resourceStream = new StreamReader(typeof(ApiAlertAnalyzer).Assembly.GetManifestResourceStream(DefaultApiAlertsResourceName)); return TargetSyntaxMessageLoader.LoadMappings(resourceStream.ReadToEnd()) @@ -48,6 +48,8 @@ public class ApiAlertAnalyzer : DiagnosticAnalyzer }); // Supported diagnostics include all of the specific diagnostics read from DefaultApiAlerts.json and the generic diagnostic used for additional target syntax messages loaded at runtime. + // For some reason, Roslyn's analyzer scanning analyzer (that compares diagnostic IDs against AnalyzerReleases.* files) only identifies + // the generic UA0013 diagnostic here, so that's the only one added to AnalyzerReleases.Unshipped.md. public override ImmutableArray SupportedDiagnostics => ImmutableArray.CreateRange(_targetSyntaxes.Value .Select(t => new DiagnosticDescriptor($"{BaseDiagnosticId}_{t.Id}", $"Replace usage of {string.Join(", ", t.TargetSyntaxes.Select(a => a.FullName))}", t.Message, Category, DiagnosticSeverity.Warning, true, t.Message)) .Concat(new[] { GenericRule })); From 07f5e69cc2e2160464e95d37712bb4c369ff5d53 Mon Sep 17 00:00:00 2001 From: Mike Rousos Date: Fri, 2 Jul 2021 12:03:12 -0400 Subject: [PATCH 05/11] Update changelog --- CHANGELOG.md | 5 +++++ .../SymbolExtensions.cs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8016c8c57..7b0e99bbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ All notable changes to the .NET Upgrade Assistant will be documented in this fil The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). +## Current + +### Added +- Added analyzers for identifying common namespaces, types, and members that require manual fixup and will produce diagnostics with links to relevant docs. The list of APIs identified by the analyzer can be expanded by adding to DefaultApiAlerts.json or by adding a .apitargets file to a project's additional files. [#685](https://github.com/dotnet/upgrade-assistant/pull/685) + ## Version 0.2.233001 - 2021-06-30 ([Link](https://www.nuget.org/packages/upgrade-assistant/0.2.233001)) ### Added diff --git a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Common/SymbolExtensions.cs b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Common/SymbolExtensions.cs index ba0c64457..ac2d29e78 100644 --- a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Common/SymbolExtensions.cs +++ b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Common/SymbolExtensions.cs @@ -34,7 +34,7 @@ public static StringComparer GetStringComparer(this ISymbol? symbol) /// /// Finds all members including those on the base classes. /// - /// Symbol to search + /// Symbol to search. /// A collection of base members. public static IEnumerable GetAllMembers(this INamedTypeSymbol? symbol) { From 243a8a4d8babfefaccaac6c44749359e651be1fc Mon Sep 17 00:00:00 2001 From: Mike Rousos Date: Fri, 16 Jul 2021 16:01:56 -0400 Subject: [PATCH 06/11] Remove System.Text.Json dependency from ApiAnalyzer --- .../SyntaxNodeExtensions.cs | 23 +++- .../ApiAlertAnalyzer.cs | 26 +--- .../DefaultApiAlerts.apitargets | 33 +++++ .../DefaultApiAlerts.json | 120 ------------------ ...istant.Extensions.Default.Analyzers.csproj | 6 +- .../TargetSyntax.cs | 4 +- .../TargetSyntaxMessageLoader.cs | 105 +++++++++++---- 7 files changed, 147 insertions(+), 170 deletions(-) create mode 100644 src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/DefaultApiAlerts.apitargets delete mode 100644 src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/DefaultApiAlerts.json diff --git a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Common/SyntaxNodeExtensions.cs b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Common/SyntaxNodeExtensions.cs index 3a2fbb905..6b347f5c1 100644 --- a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Common/SyntaxNodeExtensions.cs +++ b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Common/SyntaxNodeExtensions.cs @@ -275,7 +275,12 @@ private static bool RootIncludesImport(this SyntaxNode documentRoot, string name throw new NotImplementedException(Resources.UnknownLanguage); } - public static SyntaxNode? GetMAEExpressionSyntax(this SyntaxNode memberAccessSyntax) => + /// + /// Gets the ExpressionSyntax (the left part of the member access) from a C# or VB MemberAccessExpressionSyntax. + /// + /// The syntax whose expression propety should be returned. Note that this syntax must be of type Microsoft.CodeAnalysis.CSharp.Syntax.MemberAccessExpressionSyntax or Microsoft.CodeAnalysis.VisualBasic.Syntax.MemberAccessExpressionSyntax. + /// The member access expression's expression (the left part of the member access). + public static SyntaxNode? GetChildExpressionSyntax(this SyntaxNode memberAccessSyntax) => memberAccessSyntax switch { CSSyntax.MemberAccessExpressionSyntax csMAE => csMAE.Expression, @@ -298,5 +303,21 @@ public static StringComparer GetStringComparer(this SyntaxNode? node) LanguageNames.VisualBasic => StringComparer.OrdinalIgnoreCase, _ => throw new NotImplementedException(Resources.UnknownLanguage), }; + + private static readonly char[] GenericParameterDelimiters = new[] { '<', '>', '(', ')' }; + + public static string GetFullName(this SyntaxNode node) + { + var fullName = node.GetQualifiedName().ToString(); + + // Ignore generic parameters for now to simplify matching + var genericParameterIndex = fullName.IndexOfAny(GenericParameterDelimiters); + if (genericParameterIndex > 0) + { + fullName = fullName.Substring(0, genericParameterIndex); + } + + return fullName; + } } } diff --git a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/ApiAlertAnalyzer.cs b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/ApiAlertAnalyzer.cs index b4705c57a..e51e8db08 100644 --- a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/ApiAlertAnalyzer.cs +++ b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/ApiAlertAnalyzer.cs @@ -33,7 +33,7 @@ public class ApiAlertAnalyzer : DiagnosticAnalyzer /// private const string Category = "Upgrade"; private const string AttributeSuffix = "Attribute"; - private const string DefaultApiAlertsResourceName = "Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.DefaultApiAlerts.json"; + private const string DefaultApiAlertsResourceName = "Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.DefaultApiAlerts.apitargets"; private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.AttributeUpgradeTitle), Resources.ResourceManager, typeof(Resources)); private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.AttributeUpgradeMessageFormat), Resources.ResourceManager, typeof(Resources)); @@ -92,7 +92,7 @@ private void AnalyzeIdentifier(SyntaxNodeAnalysisContext context, IEnumerable m.TargetSyntaxes.Select(s => (TargetSyntax: s, Mapping: m))) @@ -138,7 +138,7 @@ private static bool AnalyzeNamespace(SyntaxNodeAnalysisContext context, TargetSy // If the node is a fully qualified name from the specified namespace, return true // This should cover both import statements and fully qualified type names, so no addtional checks // for import statements are needed. - var qualifiedName = GetFullName(context.Node); + var qualifiedName = context.Node.GetFullName(); if (qualifiedName.Equals(targetSyntax.FullName, context.Node.GetStringComparison())) { return true; @@ -153,7 +153,7 @@ private static bool AnalyzeNamespace(SyntaxNodeAnalysisContext context, TargetSy private static bool AnalyzeType(SyntaxNodeAnalysisContext context, TargetSyntax targetSyntax) { // If the node matches the target's fully qualified name, return true. - var qualifiedName = GetFullName(context.Node); + var qualifiedName = context.Node.GetFullName(); if (qualifiedName.Equals(targetSyntax.FullName, context.Node.GetStringComparison())) { return true; @@ -197,7 +197,7 @@ private static bool AnalyzeType(SyntaxNodeAnalysisContext context, TargetSyntax private static bool AnalyzeMember(SyntaxNodeAnalysisContext context, TargetSyntax targetSyntax) { // If the node matches the target's fully qualified name, return true. - var qualifiedName = GetFullName(context.Node); + var qualifiedName = context.Node.GetFullName(); if (qualifiedName.Equals(targetSyntax.FullName, context.Node.GetStringComparison())) { return true; @@ -205,7 +205,7 @@ private static bool AnalyzeMember(SyntaxNodeAnalysisContext context, TargetSynta // If the parent type's symbol is resolvable, return true if // it corresponds to the target type - var typeSyntax = context.Node.Parent?.GetMAEExpressionSyntax(); + var typeSyntax = context.Node.Parent?.GetChildExpressionSyntax(); if (typeSyntax is not null) { var symbol = context.SemanticModel.GetTypeInfo(typeSyntax).Type; @@ -219,19 +219,5 @@ private static bool AnalyzeMember(SyntaxNodeAnalysisContext context, TargetSynta // return true for the partial match only if ambiguous matching is enabled return targetSyntax.AlertOnAmbiguousMatch; } - - private static string GetFullName(SyntaxNode node) - { - var fullName = node.GetQualifiedName().ToString(); - - // Ignore generic parameters for now to simplify matching - var genericParameterIndex = fullName.IndexOfAny(new[] { '<', '(' }); - if (genericParameterIndex > 0) - { - fullName = fullName.Substring(0, genericParameterIndex); - } - - return fullName; - } } } diff --git a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/DefaultApiAlerts.apitargets b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/DefaultApiAlerts.apitargets new file mode 100644 index 000000000..1d9e11b73 --- /dev/null +++ b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/DefaultApiAlerts.apitargets @@ -0,0 +1,33 @@ +# Target syntax messages are defined as an ID and message (formatted as `ID: Message`) followed by APIs that the message applies to. +# APIs are described by a three-tuple of API type, API name, and a boolean indicating whether the alert should be given in cases where +# the API is only ambiguously matched. + +A: HTTP modules and HTTP handlers should be rewritten as middleware in ASP.NET Core. https://docs.microsoft.com/aspnet/core/migration/http-modulesIHttpModule +Type, System.Web.IHttpModule, true +Type, System.Web.IHttpHandler, true + +B: WCF server APIs are unsupported on .NET Core. Consider rewriting to use gRPC (https://docs.microsoft.com/dotnet/architecture/grpc-for-wcf-developers), ASP.NET Core, or CoreWCF (https://github.com/CoreWCF/CoreWCF) instead. +Type, System.ServiceModel.ServiceHost, true +Type, System.ServiceModel.ServiceHostBase, true + +C: Script and style bundling works differently in ASP.NET Core. BundleCollection should be replaced by alternative bundling technologies. https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification +Type, System.Web.Optimization.BundleCollection, true + +D: Although ClickOnce is supported on .NET 5+, apps do not have access to the System.Deployment.Application namespace. For more details see https://github.com/dotnet/deployment-tools/issues/27 and https://github.com/dotnet/deployment-tools/issues/53 +Namespace, System.Deployment.Application, false + +E: Child actions should be replaced with view components. For more details, see https://docs.microsoft.com/aspnet/core/mvc/views/view-components and https://www.davepaquette.com/archive/2016/01/02/goodbye-child-actions-hello-view-components.aspx +Type, System.Web.Mvc.ChildActionOnlyAttribute, true + +F: ASP.NET membership should be replaced with ASP.NET Core identity. For more details, see https://docs.microsoft.com/aspnet/core/migration/proper-to-2x/membership-to-core-identity +Type, System.Web.Security.Membership, true +Type, System.Web.Security.FormsAuthentication, true + +G: ASP.NET identity should be replaced with ASP.NET Core identity. For more details, see https://docs.microsoft.com/aspnet/core/migration/identity +Namespace, Microsoft.AspNet.Identity, false +Type, Microsoft.AspNet.Identity.UserManager, true +Type, Microsoft.AspNet.Identity.Owin.SignInManager, true + +H: HttpRequest.RawUrl should be replaced with HttpRequest.GetEncodedUrl() or HttpRequest.GetDisplayUrl() +Member, Microsoft.AspNetCore.Http.HttpRequest.RawUrl, false +Member, System.Web.HttpRequest.RawUrl, false diff --git a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/DefaultApiAlerts.json b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/DefaultApiAlerts.json deleted file mode 100644 index a00f7242c..000000000 --- a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/DefaultApiAlerts.json +++ /dev/null @@ -1,120 +0,0 @@ -[ - { - "Id": "A", - "TargetSyntaxes": [ - { - "FullName": "System.Web.IHttpModule", - "SyntaxType": "Type", - "AlertOnAmbiguousMatch": true - }, - { - "FullName": "System.Web.IHttpHandler", - "SyntaxType": "Type", - "AlertOnAmbiguousMatch": true - } - ], - "Message": "HTTP modules and HTTP handlers should be rewritten as middleware in ASP.NET Core. https://docs.microsoft.com/aspnet/core/migration/http-modulesIHttpModule", - }, - { - "Id": "B", - "TargetSyntaxes": [ - { - "FullName": "System.ServiceModel.ServiceHost", - "SyntaxType": "Type", - "AlertOnAmbiguousMatch": true - }, - { - "FullName": "System.ServiceModel.ServiceHostBase", - "SyntaxType": "Type", - "AlertOnAmbiguousMatch": true - } - ], - "Message": "WCF server APIs are unsupported on .NET Core. Consider rewriting to use gRPC (https://docs.microsoft.com/dotnet/architecture/grpc-for-wcf-developers), ASP.NET Core, or CoreWCF (https://github.com/CoreWCF/CoreWCF) instead.", - }, - { - "Id": "C", - "TargetSyntaxes": [ - { - "FullName": "System.Web.Optimization.BundleCollection", - "SyntaxType": "Type", - "AlertOnAmbiguousMatch": true - } - ], - "Message": "Script and style bundling works differently in ASP.NET Core. BundleCollection should be replaced by alternative bundling technologies. https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification" - }, - { - "Id": "D", - "TargetSyntaxes": [ - { - "FullName": "System.Deployment.Application", - "SyntaxType": "Namespace", - "AlertOnAmbiguousMatch": false - } - ], - "Message": "Although ClickOnce is supported on .NET 5+, apps do not have access to the System.Deployment.Application namespace. For more details see https://github.com/dotnet/deployment-tools/issues/27 and https://github.com/dotnet/deployment-tools/issues/53" - }, - { - "Id": "E", - "TargetSyntaxes": [ - { - "FullName": "System.Web.Mvc.ChildActionOnlyAttribute", - "SyntaxType": "Type", - "AlertOnAmbiguousMatch": true - } - ], - "Message": "Child actions should be replaced with view components. For more details, see https://docs.microsoft.com/aspnet/core/mvc/views/view-components and https://www.davepaquette.com/archive/2016/01/02/goodbye-child-actions-hello-view-components.aspx" - }, - { - "Id": "F", - "TargetSyntaxes": [ - { - "FullName": "System.Web.Security.Membership", - "SyntaxType": "Type", - "AlertOnAmbiguousMatch": true - }, - { - "FullName": "System.Web.Security.FormsAuthentication", - "SyntaxType": "Type", - "AlertOnAmbiguousMatch": true - } - ], - "Message": "ASP.NET membership should be replaced with ASP.NET Core identity. For more details, see https://docs.microsoft.com/aspnet/core/migration/proper-to-2x/membership-to-core-identity" - }, - { - "Id": "G", - "TargetSyntaxes": [ - { - "FullName": "Microsoft.AspNet.Identity", - "SyntaxType": "Namespace", - "AlertOnAmbiguousMatch": false - }, - { - "FullName": "Microsoft.AspNet.Identity.UserManager", - "SyntaxType": "Type", - "AlertOnAmbiguousMatch": true - }, - { - "FullName": "Microsoft.AspNet.Identity.Owin.SignInManager", - "SyntaxType": "Type", - "AlertOnAmbiguousMatch": true - } - ], - "Message": "ASP.NET identity should be replaced with ASP.NET Core identity. For more details, see https://docs.microsoft.com/aspnet/core/migration/identity" - }, - { - "Id": "H", - "TargetSyntaxes": [ - { - "FullName": "Microsoft.AspNetCore.Http.HttpRequest.RawUrl", - "SyntaxType": "Member", - "AlertOnAmbiguousMatch": false - }, - { - "FullName": "System.Web.HttpRequest.RawUrl", - "SyntaxType": "Member", - "AlertOnAmbiguousMatch": false - } - ], - "Message": "HttpRequest.RawUrl should be replaced with HttpRequest.GetEncodedUrl() or HttpRequest.GetDisplayUrl()" - } -] \ No newline at end of file diff --git a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.csproj b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.csproj index 80eedc6cf..ac967fb65 100644 --- a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.csproj +++ b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.csproj @@ -6,10 +6,8 @@ *$(MSBuildProjectFullPath)* - - - - + + diff --git a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/TargetSyntax.cs b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/TargetSyntax.cs index fe1296d45..d9bd384fe 100644 --- a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/TargetSyntax.cs +++ b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/TargetSyntax.cs @@ -10,14 +10,14 @@ namespace Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers /// public class TargetSyntax { - private string _fullName = default!; + private string? _fullName; /// /// Gets the fully qualified name of the namespace or API. /// public string FullName { - get => _fullName; + get => _fullName!; private init { _fullName = value; diff --git a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/TargetSyntaxMessageLoader.cs b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/TargetSyntaxMessageLoader.cs index 89888c444..6eb6d3d0f 100644 --- a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/TargetSyntaxMessageLoader.cs +++ b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/TargetSyntaxMessageLoader.cs @@ -4,8 +4,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Text.Json; -using System.Text.Json.Serialization; +using System.Linq; using Microsoft.CodeAnalysis; namespace Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers @@ -16,26 +15,9 @@ public static class TargetSyntaxMessageLoader /// The suffix that api/message map files are expected to use. /// private const string TargetSyntaxMessageFileSuffix = ".apitargets"; - - private static JsonSerializerOptions GetJsonSerializationOptions() - { - var jsonSerializerOptions = new JsonSerializerOptions - { - AllowTrailingCommas = true, - }; - - jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); - - return jsonSerializerOptions; - } - - /// - /// Load target syntax messages from a serialized string. - /// - /// A JSON-serialized array of target syntax messages. - /// An enumerable of TargetSyntaxMessages corresponding to those serialized in the serializedContents argument. - public static IEnumerable? LoadMappings(string serializedContents) => - JsonSerializer.Deserialize(serializedContents, GetJsonSerializationOptions()); + private static readonly char[] NewLineCharacters = new[] { '\n', '\r' }; + private static readonly char[] IdMessageDelimiters = new[] { ':' }; + private static readonly char[] ApiSyntaxDelimiters = new[] { ',' }; /// /// Load target syntax messages from additional files. @@ -48,7 +30,7 @@ public static IEnumerable LoadMappings(ImmutableArray LoadMappings(ImmutableArray + /// Load target syntax messages from a serialized string. + /// + /// A collection of target syntax messages stored as strings. + /// An enumerable of TargetSyntaxMessages corresponding to those serialized in the serializedContents argument. + public static IEnumerable? LoadMappings(string serializedContents) + { + // NOTE: This uses a simple text serialization because dependencies like System.Text.Json or Newtonsoft.Json + // don't work well in analyzers (which need to run from a variety of hosts with different versions of those + // packages (or no version at all) available. A serialization technology common between .NET Framework and .NET Core + // like XML serialization could be used, but the serialization needs are simple enough here that just reading strings + // works and allows the input files to be more concise. + if (string.IsNullOrWhiteSpace(serializedContents)) + { + return Enumerable.Empty(); + } + + var ret = new List(); + TargetSyntaxMessage? nextMessage = null; + + foreach (var line in serializedContents.Split(NewLineCharacters, StringSplitOptions.RemoveEmptyEntries).Select(l => l.Trim())) + { + // Skip blank lines and comment lines (lines starting with #) + if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#", StringComparison.Ordinal)) + { + continue; + } + + // Check for new target syntax message start (ID: Message) + else if (line.Contains(':')) + { + AddNextMessageToRet(); + var idAndMessage = line.Split(IdMessageDelimiters, 2); + nextMessage = new TargetSyntaxMessage(idAndMessage[0].Trim(), new List(), idAndMessage[1].Trim()); + } + + // Check for API target syntax (API Type, API name, Alert on Ambiguous match) + else if (nextMessage is not null) + { + // RemoveEmptyEntries is specified here to allow trailing commas if users prefer that style + var apiTargetSyntax = line.Split(ApiSyntaxDelimiters, StringSplitOptions.RemoveEmptyEntries); + if (apiTargetSyntax.Length != 3) + { + // Syntaxes must have exactly three elements + throw new FormatException($"Invalid API target syntax; API target syntaxes must have exactly three elements delimited by commas: {line}"); + } + + if (!Enum.TryParse(apiTargetSyntax[0].Trim(), true, out var targetSyntaxType)) + { + // The first element must be an element of the TargetSyntaxType enum + throw new FormatException($"Invalid target syntax type; target syntax types must be an element of the TargetSyntaxType enum: {apiTargetSyntax[0]}"); + } + + if (!bool.TryParse(apiTargetSyntax[2].Trim(), out bool alertOnAmbiguousMatch)) + { + // The third element must be a bool + throw new FormatException($"Invalid alert on ambiguous match value; must be 'true' or 'false': {apiTargetSyntax[2]}"); + } + + ((List)nextMessage.TargetSyntaxes).Add(new TargetSyntax(apiTargetSyntax[1].Trim(), targetSyntaxType, alertOnAmbiguousMatch)); + } + } + + AddNextMessageToRet(); + + return ret; + + void AddNextMessageToRet() + { + if (nextMessage is not null) + { + ret.Add(nextMessage); + nextMessage = null; + } + } + } } } From 05a16ec777ab2ddc12a8ad515603262d7f477113 Mon Sep 17 00:00:00 2001 From: Mike Rousos Date: Fri, 16 Jul 2021 16:07:21 -0400 Subject: [PATCH 07/11] Fix UA0013 resource strings --- .../ApiAlertAnalyzer.cs | 6 +++--- .../Resources.Designer.cs | 2 +- .../Resources.resx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/ApiAlertAnalyzer.cs b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/ApiAlertAnalyzer.cs index e51e8db08..c5950a1f7 100644 --- a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/ApiAlertAnalyzer.cs +++ b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/ApiAlertAnalyzer.cs @@ -35,9 +35,9 @@ public class ApiAlertAnalyzer : DiagnosticAnalyzer private const string AttributeSuffix = "Attribute"; private const string DefaultApiAlertsResourceName = "Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.DefaultApiAlerts.apitargets"; - private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.AttributeUpgradeTitle), Resources.ResourceManager, typeof(Resources)); - private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.AttributeUpgradeMessageFormat), Resources.ResourceManager, typeof(Resources)); - private static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.AttributeUpgradeDescription), Resources.ResourceManager, typeof(Resources)); + private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.ApiAlertGenericTitle), Resources.ResourceManager, typeof(Resources)); + private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.ApiAlertGenericMessageFormat), Resources.ResourceManager, typeof(Resources)); + private static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.ApiAlertGenericDescription), Resources.ResourceManager, typeof(Resources)); private static readonly DiagnosticDescriptor GenericRule = new(BaseDiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description); private readonly Lazy> _targetSyntaxes = new(() => diff --git a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/Resources.Designer.cs b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/Resources.Designer.cs index 9bef4241a..fc9961ffa 100644 --- a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/Resources.Designer.cs +++ b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/Resources.Designer.cs @@ -70,7 +70,7 @@ internal static string ApiAlertGenericDescription { } /// - /// Looks up a localized string similar to {}. + /// Looks up a localized string similar to {0}. /// internal static string ApiAlertGenericMessageFormat { get { diff --git a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/Resources.resx b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/Resources.resx index 84c7636ae..79fe9753a 100644 --- a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/Resources.resx +++ b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/Resources.resx @@ -121,7 +121,7 @@ This type is not supported on .NET Core/.NET 5+ and should be replaced with a modern equivalent. - {} + {0} Replace unsupported API From a71d88d32ee3351f843c43a9e27ea3abf3ec32a2 Mon Sep 17 00:00:00 2001 From: Mike Rousos Date: Fri, 16 Jul 2021 16:17:37 -0400 Subject: [PATCH 08/11] Remove launchSettings which doesn't need to be in source control --- .gitignore | 1 + .../Properties/launchSettings.json | 10 ---------- 2 files changed, 1 insertion(+), 10 deletions(-) delete mode 100644 src/cli/Microsoft.DotNet.UpgradeAssistant.Cli/Properties/launchSettings.json diff --git a/.gitignore b/.gitignore index 3f59bf4e4..fe5ff7ab2 100644 --- a/.gitignore +++ b/.gitignore @@ -89,6 +89,7 @@ PublishScripts/ !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json +launchSettings.json .packages/ /.dotnet diff --git a/src/cli/Microsoft.DotNet.UpgradeAssistant.Cli/Properties/launchSettings.json b/src/cli/Microsoft.DotNet.UpgradeAssistant.Cli/Properties/launchSettings.json deleted file mode 100644 index 94ffb63de..000000000 --- a/src/cli/Microsoft.DotNet.UpgradeAssistant.Cli/Properties/launchSettings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "profiles": { - "Microsoft.DotNet.UpgradeAssistant.Cli": { - "commandName": "Project", - "environmentVariables": { - "DOTNET_ENVIRONMENT": "Development" - } - } - } -} \ No newline at end of file From 3f25c753927107279241b59c8f77352194b3f912 Mon Sep 17 00:00:00 2001 From: Mike Rousos Date: Wed, 21 Jul 2021 14:15:56 -0400 Subject: [PATCH 09/11] Add launchsettings.json back to avoid telemetry on dev runs --- .gitignore | 1 - .../Properties/launchSettings.json | 10 ++++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 src/cli/Microsoft.DotNet.UpgradeAssistant.Cli/Properties/launchSettings.json diff --git a/.gitignore b/.gitignore index fe5ff7ab2..3f59bf4e4 100644 --- a/.gitignore +++ b/.gitignore @@ -89,7 +89,6 @@ PublishScripts/ !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json -launchSettings.json .packages/ /.dotnet diff --git a/src/cli/Microsoft.DotNet.UpgradeAssistant.Cli/Properties/launchSettings.json b/src/cli/Microsoft.DotNet.UpgradeAssistant.Cli/Properties/launchSettings.json new file mode 100644 index 000000000..94ffb63de --- /dev/null +++ b/src/cli/Microsoft.DotNet.UpgradeAssistant.Cli/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "Microsoft.DotNet.UpgradeAssistant.Cli": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file From 57459d92dc12df7ed3846cdab7755e03e7b46885 Mon Sep 17 00:00:00 2001 From: Mike Rousos Date: Wed, 21 Jul 2021 14:55:22 -0400 Subject: [PATCH 10/11] Don't throw exceptions for malformed apitargets files --- .../TargetSyntaxMessageLoader.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/TargetSyntaxMessageLoader.cs b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/TargetSyntaxMessageLoader.cs index 6eb6d3d0f..ec1200e7d 100644 --- a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/TargetSyntaxMessageLoader.cs +++ b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/TargetSyntaxMessageLoader.cs @@ -87,20 +87,17 @@ public static IEnumerable LoadMappings(ImmutableArray(apiTargetSyntax[0].Trim(), true, out var targetSyntaxType)) { - // The first element must be an element of the TargetSyntaxType enum - throw new FormatException($"Invalid target syntax type; target syntax types must be an element of the TargetSyntaxType enum: {apiTargetSyntax[0]}"); + continue; } - if (!bool.TryParse(apiTargetSyntax[2].Trim(), out bool alertOnAmbiguousMatch)) + if (!bool.TryParse(apiTargetSyntax[2].Trim(), out var alertOnAmbiguousMatch)) { - // The third element must be a bool - throw new FormatException($"Invalid alert on ambiguous match value; must be 'true' or 'false': {apiTargetSyntax[2]}"); + continue; } ((List)nextMessage.TargetSyntaxes).Add(new TargetSyntax(apiTargetSyntax[1].Trim(), targetSyntaxType, alertOnAmbiguousMatch)); From d91d65bfb61f3163b3cd99861b534f113fbe2db4 Mon Sep 17 00:00:00 2001 From: Mike Rousos Date: Wed, 21 Jul 2021 14:55:53 -0400 Subject: [PATCH 11/11] Add support for generics in API syntax targets --- .../SyntaxNodeExtensions.cs | 16 --------- .../ApiAlertAnalyzer.cs | 36 +++++++++++++------ .../DefaultApiAlerts.apitargets | 7 ++-- .../TargetSyntax.cs | 15 ++++++-- 4 files changed, 43 insertions(+), 31 deletions(-) diff --git a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Common/SyntaxNodeExtensions.cs b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Common/SyntaxNodeExtensions.cs index 6b347f5c1..2b4bb7d3a 100644 --- a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Common/SyntaxNodeExtensions.cs +++ b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers.Common/SyntaxNodeExtensions.cs @@ -303,21 +303,5 @@ public static StringComparer GetStringComparer(this SyntaxNode? node) LanguageNames.VisualBasic => StringComparer.OrdinalIgnoreCase, _ => throw new NotImplementedException(Resources.UnknownLanguage), }; - - private static readonly char[] GenericParameterDelimiters = new[] { '<', '>', '(', ')' }; - - public static string GetFullName(this SyntaxNode node) - { - var fullName = node.GetQualifiedName().ToString(); - - // Ignore generic parameters for now to simplify matching - var genericParameterIndex = fullName.IndexOfAny(GenericParameterDelimiters); - if (genericParameterIndex > 0) - { - fullName = fullName.Substring(0, genericParameterIndex); - } - - return fullName; - } } } diff --git a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/ApiAlertAnalyzer.cs b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/ApiAlertAnalyzer.cs index c5950a1f7..5b8d8e92c 100644 --- a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/ApiAlertAnalyzer.cs +++ b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/ApiAlertAnalyzer.cs @@ -6,6 +6,7 @@ using System.Collections.Immutable; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using CS = Microsoft.CodeAnalysis.CSharp; @@ -92,7 +93,7 @@ private void AnalyzeIdentifier(SyntaxNodeAnalysisContext context, IEnumerable m.TargetSyntaxes.Select(s => (TargetSyntax: s, Mapping: m))) @@ -115,9 +116,9 @@ private void AnalyzeIdentifier(SyntaxNodeAnalysisContext context, IEnumerable AnalyzeMember(context, match.TargetSyntax), - TargetSyntaxType.Type => AnalyzeType(context, match.TargetSyntax), - TargetSyntaxType.Namespace => AnalyzeNamespace(context, match.TargetSyntax), + TargetSyntaxType.Member => AnalyzeMember(context, fullyQualifiedName, match.TargetSyntax), + TargetSyntaxType.Type => AnalyzeType(context, fullyQualifiedName, match.TargetSyntax), + TargetSyntaxType.Namespace => AnalyzeNamespace(context, fullyQualifiedName, match.TargetSyntax), _ => false, }) { @@ -133,12 +134,11 @@ private void AnalyzeIdentifier(SyntaxNodeAnalysisContext context, IEnumerable)]", RegexOptions.Compiled); + + private static string UnbindGenericName(string name) + { + if (name is null) + { + throw new ArgumentNullException(nameof(name)); + } + + var match = GenericParameterMatcher.Match(name); + + // If the name contains generic parameters, replace them with `x where x is the + // number of parameters. Otherwise, return the name as is. + return match.Success + ? $"{name.Substring(0, match.Index)}`{match.Value.Count(c => c == ',') + 1}" + : name; + } } } diff --git a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/DefaultApiAlerts.apitargets b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/DefaultApiAlerts.apitargets index 1d9e11b73..bacbab4ab 100644 --- a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/DefaultApiAlerts.apitargets +++ b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/DefaultApiAlerts.apitargets @@ -1,6 +1,8 @@ # Target syntax messages are defined as an ID and message (formatted as `ID: Message`) followed by APIs that the message applies to. # APIs are described by a three-tuple of API type, API name, and a boolean indicating whether the alert should be given in cases where # the API is only ambiguously matched. +# +# Generic types and members should end with `X where X is the number of generic parameters they take. A: HTTP modules and HTTP handlers should be rewritten as middleware in ASP.NET Core. https://docs.microsoft.com/aspnet/core/migration/http-modulesIHttpModule Type, System.Web.IHttpModule, true @@ -23,10 +25,11 @@ F: ASP.NET membership should be replaced with ASP.NET Core identity. For more de Type, System.Web.Security.Membership, true Type, System.Web.Security.FormsAuthentication, true +# Note that generic types should end with `x where x is the number of generic parameters they take G: ASP.NET identity should be replaced with ASP.NET Core identity. For more details, see https://docs.microsoft.com/aspnet/core/migration/identity Namespace, Microsoft.AspNet.Identity, false -Type, Microsoft.AspNet.Identity.UserManager, true -Type, Microsoft.AspNet.Identity.Owin.SignInManager, true +Type, Microsoft.AspNet.Identity.UserManager`2, true +Type, Microsoft.AspNet.Identity.Owin.SignInManager`2, true H: HttpRequest.RawUrl should be replaced with HttpRequest.GetEncodedUrl() or HttpRequest.GetDisplayUrl() Member, Microsoft.AspNetCore.Http.HttpRequest.RawUrl, false diff --git a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/TargetSyntax.cs b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/TargetSyntax.cs index d9bd384fe..9aaec1536 100644 --- a/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/TargetSyntax.cs +++ b/src/extensions/default/analyzers/Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers/TargetSyntax.cs @@ -21,9 +21,18 @@ public string FullName private init { _fullName = value; - SimpleName = _fullName.LastIndexOf('.') < 0 - ? _fullName - : _fullName.Substring(_fullName.LastIndexOf('.') + 1); + + // The simple name is the identifier after any namespace qualifications and before any generic type parameters + var startIndex = _fullName.LastIndexOf('.') + 1; + var endIndex = _fullName.IndexOf('`'); + if (endIndex < 0) + { + endIndex = _fullName.Length; + } + + SimpleName = startIndex > 0 || endIndex < _fullName.Length + ? _fullName.Substring(startIndex, endIndex - startIndex) + : _fullName; } }