Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions src/Components/Analyzers/src/ComponentSymbols.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ public static bool TryCreate(Compilation compilation, out ComponentSymbols symbo
var supplyParameterFromFormAttribute = compilation.GetTypeByMetadataName(ComponentsApi.SupplyParameterFromFormAttribute.MetadataName);
var persistentStateAttribute = compilation.GetTypeByMetadataName(ComponentsApi.PersistentStateAttribute.MetadataName);
var componentBaseType = compilation.GetTypeByMetadataName(ComponentsApi.ComponentBase.MetadataName);
var layoutAttribute = compilation.GetTypeByMetadataName(ComponentsApi.LayoutAttribute.MetadataName);
var layoutComponentBase = compilation.GetTypeByMetadataName(ComponentsApi.LayoutComponentBase.MetadataName);

symbols = new ComponentSymbols(
parameterAttribute,
Expand All @@ -59,7 +61,9 @@ public static bool TryCreate(Compilation compilation, out ComponentSymbols symbo
persistentStateAttribute,
componentBaseType,
parameterCaptureUnmatchedValuesRuntimeType,
icomponentType);
icomponentType,
layoutAttribute,
layoutComponentBase);
return true;
}

Expand All @@ -70,7 +74,9 @@ private ComponentSymbols(
INamedTypeSymbol persistentStateAttribute,
INamedTypeSymbol componentBaseType,
INamedTypeSymbol parameterCaptureUnmatchedValuesRuntimeType,
INamedTypeSymbol icomponentType)
INamedTypeSymbol icomponentType,
INamedTypeSymbol layoutAttribute,
INamedTypeSymbol layoutComponentBase)
{
ParameterAttribute = parameterAttribute;
CascadingParameterAttribute = cascadingParameterAttribute;
Expand All @@ -79,6 +85,8 @@ private ComponentSymbols(
ComponentBaseType = componentBaseType; // Can be null
ParameterCaptureUnmatchedValuesRuntimeType = parameterCaptureUnmatchedValuesRuntimeType;
IComponentType = icomponentType;
LayoutAttribute = layoutAttribute; // Can be null
LayoutComponentBase = layoutComponentBase; // Can be null
}

public INamedTypeSymbol ParameterAttribute { get; }
Expand All @@ -95,4 +103,8 @@ private ComponentSymbols(
public INamedTypeSymbol ComponentBaseType { get; } // Can be null if not available

public INamedTypeSymbol IComponentType { get; }

public INamedTypeSymbol LayoutAttribute { get; } // Can be null if not available

public INamedTypeSymbol LayoutComponentBase { get; } // Can be null if not available
}
13 changes: 13 additions & 0 deletions src/Components/Analyzers/src/ComponentsApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,17 @@ public static class IComponent
public const string FullTypeName = "Microsoft.AspNetCore.Components.IComponent";
public const string MetadataName = FullTypeName;
}

public static class LayoutAttribute
{
public const string FullTypeName = "Microsoft.AspNetCore.Components.LayoutAttribute";
public const string MetadataName = FullTypeName;
public const string LayoutType = "LayoutType";
}

public static class LayoutComponentBase
{
public const string FullTypeName = "Microsoft.AspNetCore.Components.LayoutComponentBase";
public const string MetadataName = FullTypeName;
}
}
9 changes: 9 additions & 0 deletions src/Components/Analyzers/src/DiagnosticDescriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,13 @@ internal static class DiagnosticDescriptors
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: CreateLocalizableResourceString(nameof(Resources.PersistentStateShouldNotHavePropertyInitializer_Description)));

public static readonly DiagnosticDescriptor LayoutComponentCannotReferenceItself = new(
"BL0010",
CreateLocalizableResourceString(nameof(Resources.LayoutComponentCannotReferenceItself_Title)),
CreateLocalizableResourceString(nameof(Resources.LayoutComponentCannotReferenceItself_Format)),
Usage,
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: CreateLocalizableResourceString(nameof(Resources.LayoutComponentCannotReferenceItself_Description)));
}
93 changes: 93 additions & 0 deletions src/Components/Analyzers/src/LayoutCycleAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// 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.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Microsoft.AspNetCore.Components.Analyzers;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class LayoutCycleAnalyzer : DiagnosticAnalyzer
{
public LayoutCycleAnalyzer()
{
SupportedDiagnostics = ImmutableArray.Create(new[]
{
DiagnosticDescriptors.LayoutComponentCannotReferenceItself,
});
}

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }

public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
context.RegisterCompilationStartAction(context =>
{
if (!ComponentSymbols.TryCreate(context.Compilation, out var symbols))
{
// Types we need are not defined.
return;
}

// Check if LayoutAttribute and LayoutComponentBase are available
if (symbols.LayoutAttribute == null || symbols.LayoutComponentBase == null)
{
return;
}

context.RegisterSymbolAction(context =>
{
var namedType = (INamedTypeSymbol)context.Symbol;

// Check if the type inherits from LayoutComponentBase (directly or indirectly)
if (!InheritsFromLayoutComponentBase(namedType, symbols.LayoutComponentBase))
{
return;
}

// Check if the type has a LayoutAttribute
var layoutAttribute = namedType.GetAttributes()
.FirstOrDefault(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, symbols.LayoutAttribute));

if (layoutAttribute == null)
{
return;
}

// Get the LayoutType from the attribute constructor argument
if (layoutAttribute.ConstructorArguments.Length > 0)
{
var layoutType = layoutAttribute.ConstructorArguments[0].Value as INamedTypeSymbol;

// Check if the layout type is the same as the current type (self-reference)
if (layoutType != null && SymbolEqualityComparer.Default.Equals(namedType, layoutType))
{
var location = namedType.Locations.FirstOrDefault() ?? Location.None;
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.LayoutComponentCannotReferenceItself,
location,
namedType.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat)));
}
}
}, SymbolKind.NamedType);
});
}

private static bool InheritsFromLayoutComponentBase(INamedTypeSymbol type, INamedTypeSymbol layoutComponentBase)
{
var current = type.BaseType;
while (current != null)
{
if (SymbolEqualityComparer.Default.Equals(current, layoutComponentBase))
{
return true;
}
current = current.BaseType;
}
return false;
}
}
9 changes: 9 additions & 0 deletions src/Components/Analyzers/src/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -198,4 +198,13 @@
<data name="PersistentStateShouldNotHavePropertyInitializer_Title" xml:space="preserve">
<value>Property with [PersistentState] should not have initializer</value>
</data>
<data name="LayoutComponentCannotReferenceItself_Description" xml:space="preserve">
<value>Layout components cannot reference themselves as their layout, as this would create an infinite rendering loop.</value>
</data>
<data name="LayoutComponentCannotReferenceItself_Format" xml:space="preserve">
<value>Layout component '{0}' has a [Layout] attribute that references itself, which will cause an infinite rendering loop.</value>
</data>
<data name="LayoutComponentCannotReferenceItself_Title" xml:space="preserve">
<value>Layout component cannot reference itself</value>
</data>
</root>
99 changes: 99 additions & 0 deletions src/Components/Analyzers/test/LayoutCycleAnalyzerTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Analyzer.Testing;

namespace Microsoft.AspNetCore.Components.Analyzers;

public class LayoutCycleAnalyzerTest : AnalyzerTestBase
{
public LayoutCycleAnalyzerTest()
{
Analyzer = new LayoutCycleAnalyzer();
Runner = new ComponentAnalyzerDiagnosticAnalyzerRunner(Analyzer);
}

private LayoutCycleAnalyzer Analyzer { get; }
private ComponentAnalyzerDiagnosticAnalyzerRunner Runner { get; }

[Fact]
public async Task LayoutComponentReferencesSelf_ReportsDiagnostic()
{
// Arrange
var source = TestSource.Read(@"
using Microsoft.AspNetCore.Components;

[Layout(typeof(MyLayout))]
public class /*MM*/MyLayout : LayoutComponentBase
{
}");

// Act
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);

// Assert
var diagnostic = Assert.Single(diagnostics);
Assert.Same(DiagnosticDescriptors.LayoutComponentCannotReferenceItself, diagnostic.Descriptor);
AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location);
}

[Fact]
public async Task LayoutComponentDoesNotReferenceSelf_NoDiagnostic()
{
// Arrange
var source = @"
using Microsoft.AspNetCore.Components;

public class MyLayout : LayoutComponentBase
{
}";

// Act
var diagnostics = await Runner.GetDiagnosticsAsync(source);

// Assert
Assert.Empty(diagnostics);
}

[Fact]
public async Task LayoutComponentReferencesOtherLayout_NoDiagnostic()
{
// Arrange
var source = @"
using Microsoft.AspNetCore.Components;

public class MainLayout : LayoutComponentBase
{
}

[Layout(typeof(MainLayout))]
public class MyLayout : LayoutComponentBase
{
}";

// Act
var diagnostics = await Runner.GetDiagnosticsAsync(source);

// Assert
Assert.Empty(diagnostics);
}

[Fact]
public async Task NonLayoutComponent_NoDiagnostic()
{
// Arrange
var source = @"
using Microsoft.AspNetCore.Components;

[Layout(typeof(MyComponent))]
public class MyComponent : ComponentBase
{
}";

// Act
var diagnostics = await Runner.GetDiagnosticsAsync(source);

// Assert
Assert.Empty(diagnostics);
}
}
Loading