Skip to content

Commit 065fc23

Browse files
committed
init the analyzer
1 parent dc68e3b commit 065fc23

File tree

6 files changed

+253
-0
lines changed

6 files changed

+253
-0
lines changed

src/Framework/AspNetCoreAnalyzers/src/Analyzers/DiagnosticDescriptors.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,4 +233,14 @@ internal static class DiagnosticDescriptors
233233
isEnabledByDefault: true,
234234
helpLinkUri: "https://aka.ms/aspnet/analyzers",
235235
customTags: WellKnownDiagnosticTags.Unnecessary);
236+
237+
internal static readonly DiagnosticDescriptor KestrelShouldListenOnIPv6AnyInsteadOfIpAny = new(
238+
"ASP0028",
239+
new LocalizableResourceString(nameof(Resources.Analyzer_KestrelShouldListenOnIPv6AnyInsteadOfIpAny_Title), Resources.ResourceManager, typeof(Resources)),
240+
new LocalizableResourceString(nameof(Resources.Analyzer_KestrelShouldListenOnIPv6AnyInsteadOfIpAny_Message), Resources.ResourceManager, typeof(Resources)), // make a doc? https://aka.ms/aspnetcore-warnings/ASP0027
241+
"Usage",
242+
DiagnosticSeverity.Info,
243+
isEnabledByDefault: true,
244+
helpLinkUri: "https://aka.ms/aspnet/analyzers",
245+
customTags: WellKnownDiagnosticTags.Unnecessary);
236246
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.CodeAnalysis.Diagnostics;
5+
using Microsoft.CodeAnalysis;
6+
using System.Collections.Immutable;
7+
using Microsoft.CodeAnalysis.CSharp;
8+
using Microsoft.CodeAnalysis.Operations;
9+
using Microsoft.CodeAnalysis.CSharp.Syntax;
10+
using System.Linq;
11+
12+
namespace Microsoft.AspNetCore.Analyzers.Kestrel;
13+
14+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
15+
public class ListenOnIPv6AnyAnalyzer : DiagnosticAnalyzer
16+
{
17+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [ DiagnosticDescriptors.KestrelShouldListenOnIPv6AnyInsteadOfIpAny ];
18+
19+
public override void Initialize(AnalysisContext context)
20+
{
21+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
22+
context.EnableConcurrentExecution();
23+
24+
context.RegisterSyntaxNodeAction(KestrelServerOptionsListenInvocation, SyntaxKind.InvocationExpression);
25+
}
26+
27+
private void KestrelServerOptionsListenInvocation(SyntaxNodeAnalysisContext context)
28+
{
29+
// fail fast before accessing SemanticModel
30+
if (context.Node is not InvocationExpressionSyntax
31+
{
32+
Expression: MemberAccessExpressionSyntax
33+
{
34+
Name: IdentifierNameSyntax { Identifier.ValueText: "Listen" }
35+
}
36+
} kestrelOptionsListenExpressionSyntax)
37+
{
38+
return;
39+
}
40+
41+
var nodeOperation = context.SemanticModel.GetOperation(context.Node, context.CancellationToken);
42+
if (!IsKestrelServerOptionsType(nodeOperation, out var kestrelOptionsListenInvocation))
43+
{
44+
return;
45+
}
46+
47+
var addressArgument = kestrelOptionsListenInvocation?.Arguments.FirstOrDefault();
48+
if (!IsIPAddressType(addressArgument?.Parameter))
49+
{
50+
return;
51+
}
52+
53+
var args = kestrelOptionsListenExpressionSyntax.ArgumentList;
54+
var ipAddressArgumentSyntax = args.Arguments.FirstOrDefault();
55+
56+
// explicit usage like `options.Listen(IPAddress.Any, ...)`
57+
if (ipAddressArgumentSyntax is ArgumentSyntax
58+
{
59+
Expression: MemberAccessExpressionSyntax
60+
{
61+
Name: IdentifierNameSyntax { Identifier.ValueText: "Any" }
62+
}
63+
})
64+
{
65+
context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.KestrelShouldListenOnIPv6AnyInsteadOfIpAny, ipAddressArgumentSyntax.GetLocation()));
66+
}
67+
68+
69+
}
70+
71+
private static bool IsIPAddressType(IParameterSymbol? parameter) => parameter is
72+
{
73+
Type: // searching type `System.Net.IPAddress`
74+
{
75+
Name: "IPAddress",
76+
ContainingNamespace: { Name: "Net", ContainingNamespace: { Name: "System", ContainingNamespace.IsGlobalNamespace: true } }
77+
}
78+
};
79+
80+
private static bool IsKestrelServerOptionsType(IOperation? operation, out IInvocationOperation? kestrelOptionsListenInvocation)
81+
{
82+
var result = operation is IInvocationOperation // searching type `Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions`
83+
{
84+
TargetMethod: { Name: "Listen" },
85+
Instance.Type:
86+
{
87+
Name: "KestrelServerOptions",
88+
ContainingNamespace:
89+
{
90+
Name: "Core",
91+
ContainingNamespace:
92+
{
93+
Name: "Kestrel",
94+
ContainingNamespace:
95+
{
96+
Name: "Server",
97+
ContainingNamespace:
98+
{
99+
Name: "AspNetCore",
100+
ContainingNamespace:
101+
{
102+
Name: "Microsoft",
103+
ContainingNamespace.IsGlobalNamespace: true
104+
}
105+
}
106+
}
107+
}
108+
}
109+
}
110+
};
111+
112+
kestrelOptionsListenInvocation = result ? (IInvocationOperation)operation! : null;
113+
return result;
114+
}
115+
}

src/Framework/AspNetCoreAnalyzers/src/Analyzers/Resources.resx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,4 +327,10 @@
327327
<data name="Analyzer_PublicPartialProgramClass_Title" xml:space="preserve">
328328
<value>Unnecessary public Program class declaration</value>
329329
</data>
330+
<data name="Analyzer_KestrelShouldListenOnIPv6AnyInsteadOfIpAny_Title" xml:space="preserve">
331+
<value>Consider using IPAddress.IPv6Any instead of IPAddress.Any</value>
332+
</data>
333+
<data name="Analyzer_KestrelShouldListenOnIPv6AnyInsteadOfIpAny_Message" xml:space="preserve">
334+
<value>If server does not specifically reject IPv6, IPAddress.IPv6Any is preferred over IPAddress.Any usage due to safety and performance reasons. See https://aka.ms/aspnetcore-warnings/ASP0028 for more details.</value>
335+
</data>
330336
</root>
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Composition;
5+
using Microsoft.CodeAnalysis.CodeFixes;
6+
using Microsoft.CodeAnalysis;
7+
using System.Collections.Immutable;
8+
using System.Threading.Tasks;
9+
using Microsoft.AspNetCore.Analyzers;
10+
using Microsoft.CodeAnalysis.CodeActions;
11+
using Microsoft.CodeAnalysis.Editing;
12+
using Microsoft.CodeAnalysis.CSharp.Syntax;
13+
14+
namespace Microsoft.AspNetCore.Fixers.Kestrel;
15+
16+
[ExportCodeFixProvider(LanguageNames.CSharp), Shared]
17+
public class ListenOnIPv6AnyFixer : CodeFixProvider
18+
{
19+
public override ImmutableArray<string> FixableDiagnosticIds => [ DiagnosticDescriptors.KestrelShouldListenOnIPv6AnyInsteadOfIpAny.Id ];
20+
21+
public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
22+
23+
public override Task RegisterCodeFixesAsync(CodeFixContext context)
24+
{
25+
foreach (var diagnostic in context.Diagnostics)
26+
{
27+
context.RegisterCodeFix(
28+
CodeAction.Create(
29+
"Consider using IPAddress.IPv6Any instead of IPAddress.Any",
30+
async cancellationToken =>
31+
{
32+
throw new System.NotImplementedException();
33+
34+
//var editor = await DocumentEditor.CreateAsync(context.Document, cancellationToken).ConfigureAwait(false);
35+
//var root = await context.Document.GetSyntaxRootAsync(cancellationToken);
36+
//if (root is null)
37+
//{
38+
// return context.Document;
39+
//}
40+
41+
//var classDeclaration = root.FindNode(diagnostic.Location.SourceSpan)
42+
// .FirstAncestorOrSelf<ClassDeclarationSyntax>();
43+
//if (classDeclaration is null)
44+
//{
45+
// return context.Document;
46+
//}
47+
//editor.RemoveNode(classDeclaration, SyntaxRemoveOptions.KeepExteriorTrivia);
48+
//return editor.GetChangedDocument();
49+
50+
},
51+
equivalenceKey: DiagnosticDescriptors.KestrelShouldListenOnIPv6AnyInsteadOfIpAny.Id),
52+
diagnostic);
53+
}
54+
55+
return Task.CompletedTask;
56+
}
57+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Text;
7+
using Microsoft.CodeAnalysis.Testing;
8+
using VerifyCS = Microsoft.AspNetCore.Analyzers.Verifiers.CSharpCodeFixVerifier<
9+
Microsoft.AspNetCore.Analyzers.Kestrel.ListenOnIPv6AnyAnalyzer,
10+
Microsoft.AspNetCore.Fixers.Kestrel.ListenOnIPv6AnyFixer>;
11+
12+
namespace Microsoft.AspNetCore.Analyzers.Kestrel;
13+
14+
public class ListenOnIPv6AnyAnalyzerTests
15+
{
16+
[Fact] // do we need any other scenarios except the direct usage one?
17+
public async Task ReportsDiagnostic_IPAddressAsLocalVariable()
18+
{
19+
var source = GetKestrelSetupSource("myIp", "var myIp = IPAddress.Any;");
20+
21+
await VerifyCS.VerifyAnalyzerAsync(source, [
22+
new DiagnosticResult(DiagnosticDescriptors.KestrelShouldListenOnIPv6AnyInsteadOfIpAny).WithLocation(0)
23+
]);
24+
}
25+
26+
[Fact]
27+
public async Task ReportsDiagnostic_ExplicitUsage()
28+
{
29+
var source = GetKestrelSetupSource("IPAddress.Any");
30+
31+
await VerifyCS.VerifyAnalyzerAsync(source, [
32+
new DiagnosticResult(DiagnosticDescriptors.KestrelShouldListenOnIPv6AnyInsteadOfIpAny).WithLocation(0)
33+
]);
34+
}
35+
36+
static string GetKestrelSetupSource(string ipAddressArgument, string extraInlineCode = null) => $$"""
37+
using Microsoft.Extensions.Hosting;
38+
using Microsoft.AspNetCore.Hosting;
39+
using Microsoft.AspNetCore.Server.Kestrel.Core;
40+
using System.Net;
41+
42+
var hostBuilder = new HostBuilder()
43+
.ConfigureWebHost(webHost =>
44+
{
45+
webHost.UseKestrel().ConfigureKestrel(options =>
46+
{
47+
{{extraInlineCode}}
48+
49+
options.ListenLocalhost(5000);
50+
options.ListenAnyIP(5000);
51+
options.Listen({|#0:{{ipAddressArgument}}|}, 5000, listenOptions =>
52+
{
53+
listenOptions.UseHttps();
54+
listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
55+
});
56+
});
57+
});
58+
59+
var host = hostBuilder.Build();
60+
host.Run();
61+
""";
62+
}

src/Framework/AspNetCoreAnalyzers/test/Verifiers/CSharpAnalyzerVerifier.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using Microsoft.CodeAnalysis.Testing;
1212
using Microsoft.CodeAnalysis.Testing.Verifiers;
1313
using Microsoft.Extensions.DependencyInjection;
14+
using Microsoft.AspNetCore.Hosting;
1415

1516
namespace Microsoft.AspNetCore.Analyzers.Verifiers;
1617

@@ -63,10 +64,12 @@ internal static ReferenceAssemblies GetReferenceAssemblies()
6364

6465
return net10Ref.AddAssemblies(ImmutableArray.Create(
6566
TrimAssemblyExtension(typeof(System.IO.Pipelines.PipeReader).Assembly.Location),
67+
TrimAssemblyExtension(typeof(Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServer).Assembly.Location),
6668
TrimAssemblyExtension(typeof(Microsoft.AspNetCore.Authorization.IAuthorizeData).Assembly.Location),
6769
TrimAssemblyExtension(typeof(Microsoft.AspNetCore.Mvc.ModelBinding.IBinderTypeProviderMetadata).Assembly.Location),
6870
TrimAssemblyExtension(typeof(Microsoft.AspNetCore.Mvc.BindAttribute).Assembly.Location),
6971
TrimAssemblyExtension(typeof(Microsoft.AspNetCore.Hosting.WebHostBuilderExtensions).Assembly.Location),
72+
TrimAssemblyExtension(typeof(Microsoft.AspNetCore.Hosting.WebHostBuilderKestrelExtensions).Assembly.Location),
7073
TrimAssemblyExtension(typeof(Microsoft.Extensions.Hosting.IHostBuilder).Assembly.Location),
7174
TrimAssemblyExtension(typeof(Microsoft.Extensions.Hosting.HostingHostBuilderExtensions).Assembly.Location),
7275
TrimAssemblyExtension(typeof(Microsoft.AspNetCore.Builder.ConfigureHostBuilder).Assembly.Location),

0 commit comments

Comments
 (0)