diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RouteStringSyntaxDetector.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RouteStringSyntaxDetector.cs index 345336918ea8..d5814e002b5b 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RouteStringSyntaxDetector.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RouteStringSyntaxDetector.cs @@ -149,6 +149,19 @@ private static bool HasLanguageComment( return true; } + // Check for the common case of a string literal in a large binary expression. For example `"..." + "..." + + // "..."` We never want to consider these as regex/json tokens as processing them would require knowing the + // contents of every string literal, and having our lexers/parsers somehow stitch them all together. This is + // beyond what those systems support (and would only work for constant strings anyways). This prevents both + // incorrect results *and* avoids heavy perf hits walking up large binary expressions (often while a caller is + // themselves walking down such a large expression). + if (token.Parent.IsLiteralExpression() && + token.Parent.Parent.IsBinaryExpression() && + token.Parent.Parent.RawKind == (int)SyntaxKind.AddExpression) + { + return false; + } + for (var node = token.Parent; node != null; node = node.Parent) { if (HasLanguageComment(node.GetLeadingTrivia(), out identifier, out options)) diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/SyntaxNodeExtensions.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/SyntaxNodeExtensions.cs index 525854a63eb4..3eba331a5ffb 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/SyntaxNodeExtensions.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/SyntaxNodeExtensions.cs @@ -33,6 +33,12 @@ public static SyntaxNode GetRequiredParent(this SyntaxNode node) return parent; } + public static bool IsLiteralExpression([NotNullWhen(true)] this SyntaxNode? node) + => node is LiteralExpressionSyntax; + + public static bool IsBinaryExpression([NotNullWhen(true)] this SyntaxNode? node) + => node is BinaryExpressionSyntax; + [return: NotNullIfNotNull("node")] public static SyntaxNode? WalkUpParentheses(this SyntaxNode? node) { diff --git a/src/Framework/AspNetCoreAnalyzers/test/Microsoft.AspNetCore.App.Analyzers.Test.csproj b/src/Framework/AspNetCoreAnalyzers/test/Microsoft.AspNetCore.App.Analyzers.Test.csproj index 5f223d266110..8070e80c0b51 100644 --- a/src/Framework/AspNetCoreAnalyzers/test/Microsoft.AspNetCore.App.Analyzers.Test.csproj +++ b/src/Framework/AspNetCoreAnalyzers/test/Microsoft.AspNetCore.App.Analyzers.Test.csproj @@ -1,4 +1,4 @@ - + $(DefaultNetCoreTargetFramework) diff --git a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternAnalyzerTests.cs b/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternAnalyzerTests.cs index 921e7f7f5d2f..a4c06cd0a261 100644 --- a/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternAnalyzerTests.cs +++ b/src/Framework/AspNetCoreAnalyzers/test/RouteEmbeddedLanguage/RoutePatternAnalyzerTests.cs @@ -1,17 +1,28 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using System.Globalization; +using System.Text; using Microsoft.AspNetCore.Analyzer.Testing; using Microsoft.AspNetCore.Analyzers.RenderTreeBuilder; using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; +using Microsoft.CodeAnalysis; +using Xunit.Abstractions; namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage; public partial class RoutePatternAnalyzerTests { + private readonly ITestOutputHelper _testOutputHelper; + private TestDiagnosticAnalyzerRunner Runner { get; } = new(new RoutePatternAnalyzer()); + public RoutePatternAnalyzerTests(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + } + [Fact] public async Task CommentOnString_ReportResults() { @@ -512,4 +523,125 @@ public object TestAction(int id) // Assert Assert.Empty(diagnostics); } + + [Fact] + public async Task ConcatString_PerformanceTest() + { + // Arrange + var builder = new StringBuilder(); + builder.AppendLine(""" + class Program + { + static void Main() { } + static readonly string _s = + """); + for (var i = 0; i < 2000; i++) + { + builder.AppendLine(" \"a{}bc\" +"); + } + builder.AppendLine(""" + ""; + } + """); + var source = TestSource.Read(builder.ToString()); + + // Act 1 + // Warm up. + var diagnostics1 = await Runner.GetDiagnosticsAsync(source.Source); + + // Assert 1 + Assert.Empty(diagnostics1); + + // Act 2 + // Measure analysis. + var stopwatch = Stopwatch.StartNew(); + + var diagnostics2 = await Runner.GetDiagnosticsAsync(source.Source); + _testOutputHelper.WriteLine($"Elapsed time: {stopwatch.Elapsed}"); + + // Assert 2 + Assert.Empty(diagnostics2); + } + + [Fact] + public async Task ConcatString_DetectLanguage_NoWarningsBecauseConcatString() + { + // Arrange + var builder = new StringBuilder(); + builder.AppendLine(""" + class Program + { + static void Main() { } + // lang=Route + static readonly string _s = + """); + for (var i = 0; i < 2000; i++) + { + builder.AppendLine(" \"a{}bc\" +"); + } + builder.AppendLine(""" + ""; + } + """); + var source = TestSource.Read(builder.ToString()); + + // Act + var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); + + // Assert + Assert.Empty(diagnostics); + } + + [Fact] + public async Task NestedLangComment_NoWarningsBecauseConcatString() + { + // Arrange + var builder = new StringBuilder(); + builder.AppendLine(""" + class Program + { + static void Main() { } + static readonly string _s = + "{/*MM0*/te*st0}" + + // lang=Route + "{/*MM1*/te*st1}" + + "{/*MM2*/te*st2}" + + "{test3}"; + } + """); + var source = TestSource.Read(builder.ToString()); + + // Act + var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); + + // Assert + Assert.Empty(diagnostics); + } + + [Fact] + public async Task TopLangComment_NoWarningsBecauseConcatString() + { + // Arrange + var builder = new StringBuilder(); + builder.AppendLine(""" + class Program + { + static void Main() { } + static readonly string _s = + // lang=Route + "{/*MM0*/te*st0}" + + "{/*MM1*/te*st1}" + + "{/*MM2*/te*st2}" + + // lang=regex + "{test3}"; + } + """); + var source = TestSource.Read(builder.ToString()); + + // Act + var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); + + // Assert + Assert.Empty(diagnostics); + } }