Encourage encapsulation of component parameter properties (#713)
* Before refactoring ParameterCollection assignment logic, add more test coverage * Begin caching parameter assignment info * Factor out some reflection code to a reusable location * Use IPropertySetter to avoid all per-property-assignment reflection * More error cases and tests for parameter assignment * Enable binding to nonpublic properties * Add analyzer to warn and provide fix for public component parameters * Unit test for analyzer * Component tag helper now includes private properties if they have [Parameter] * CR feedback: Remove garbage from csproj * CR feedback: Rename .Build.Analyzers to .Analyzers * CR feedback: Move BlazorApi.cs to shared; use it from Analyzers test * Fix incorrect test name * Make as many parameters private as possible. Replace ILayoutComponent with BlazorLayoutComponent. * In component tag helper discovery, consider private members too * Reduce the work in component parameter discovery by not inspecting the BlazorComponent base class (or System.Object)
This commit is contained in:
parent
ec1b220b7d
commit
18b9a70dbe
22
Blazor.sln
22
Blazor.sln
|
|
@ -95,6 +95,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorLibrary-CSharp", "src\Microsoft.AspNetCore.Blazor.Templates\content\BlazorLibrary-CSharp\BlazorLibrary-CSharp.csproj", "{3A457B14-D91B-4FFF-A81A-8F350BDB911F}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor.Analyzers", "src\Microsoft.AspNetCore.Blazor.Analyzers\Microsoft.AspNetCore.Blazor.Analyzers.csproj", "{6DDD6A29-0A3E-417F-976C-5FE3FDA74055}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor.Analyzers.Test", "test\Microsoft.AspNetCore.Blazor.Analyzers.Test\Microsoft.AspNetCore.Blazor.Analyzers.Test.csproj", "{CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -330,6 +334,22 @@ Global
|
|||
{3A457B14-D91B-4FFF-A81A-8F350BDB911F}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3A457B14-D91B-4FFF-A81A-8F350BDB911F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3A457B14-D91B-4FFF-A81A-8F350BDB911F}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6DDD6A29-0A3E-417F-976C-5FE3FDA74055}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6DDD6A29-0A3E-417F-976C-5FE3FDA74055}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6DDD6A29-0A3E-417F-976C-5FE3FDA74055}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6DDD6A29-0A3E-417F-976C-5FE3FDA74055}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6DDD6A29-0A3E-417F-976C-5FE3FDA74055}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6DDD6A29-0A3E-417F-976C-5FE3FDA74055}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6DDD6A29-0A3E-417F-976C-5FE3FDA74055}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6DDD6A29-0A3E-417F-976C-5FE3FDA74055}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
|
||||
{CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -372,6 +392,8 @@ Global
|
|||
{C57382BC-EE93-49D5-BC40-5C98AF8AA048} = {4AE0D35B-D97A-44D0-8392-C9240377DCCE}
|
||||
{50F6820F-D058-4E68-9E15-801F893F514E} = {36A7DEB7-5F88-4BFB-B57E-79EEC9950E25}
|
||||
{3A457B14-D91B-4FFF-A81A-8F350BDB911F} = {E8EBA72C-D555-43AE-BC98-F0B2D05F6A07}
|
||||
{6DDD6A29-0A3E-417F-976C-5FE3FDA74055} = {B867E038-B3CE-43E3-9292-61568C46CDEB}
|
||||
{CF3B5990-7A05-4993-AACA-D2C8D7AFF6E6} = {ADA3AE29-F6DE-49F6-8C7C-B321508CAE8E}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {504DA352-6788-4DC0-8705-82167E72A4D3}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@implements ILayoutComponent
|
||||
@inherits BlazorLayoutComponent
|
||||
|
||||
<div class="sidebar">
|
||||
<NavMenu />
|
||||
|
|
@ -13,8 +13,3 @@
|
|||
@Body
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@functions {
|
||||
[Parameter]
|
||||
public RenderFragment Body { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Blazor.Shared;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Analyzers
|
||||
{
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
public class ComponentParametersShouldNotBePublicAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
public const string DiagnosticId = "BL9993";
|
||||
private const string Category = "Encapsulation";
|
||||
|
||||
private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.ComponentParametersShouldNotBePublic_Title), Resources.ResourceManager, typeof(Resources));
|
||||
private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.ComponentParametersShouldNotBePublic_Format), Resources.ResourceManager, typeof(Resources));
|
||||
private static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.ComponentParametersShouldNotBePublic_Description), Resources.ResourceManager, typeof(Resources));
|
||||
private static DiagnosticDescriptor Rule = new DiagnosticDescriptor(
|
||||
DiagnosticId,
|
||||
Title,
|
||||
MessageFormat,
|
||||
Category,
|
||||
DiagnosticSeverity.Warning,
|
||||
isEnabledByDefault: true,
|
||||
description: Description);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
|
||||
=> ImmutableArray.Create(Rule);
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.RegisterSyntaxNodeAction(AnalyzeSyntax, SyntaxKind.PropertyDeclaration);
|
||||
}
|
||||
|
||||
private void AnalyzeSyntax(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
var semanticModel = context.SemanticModel;
|
||||
var declaration = (PropertyDeclarationSyntax)context.Node;
|
||||
|
||||
var parameterAttribute = declaration.AttributeLists
|
||||
.SelectMany(list => list.Attributes)
|
||||
.Where(attr => semanticModel.GetTypeInfo(attr).Type?.ToDisplayString() == BlazorApi.ParameterAttribute.FullTypeName)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (parameterAttribute != null && IsPublic(declaration))
|
||||
{
|
||||
var identifierText = declaration.Identifier.Text;
|
||||
if (!string.IsNullOrEmpty(identifierText))
|
||||
{
|
||||
context.ReportDiagnostic(Diagnostic.Create(
|
||||
Rule,
|
||||
declaration.GetLocation(),
|
||||
identifierText));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsPublic(PropertyDeclarationSyntax declaration)
|
||||
=> declaration.Modifiers.Any(m => m.IsKind(SyntaxKind.PublicKeyword));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CodeActions;
|
||||
using Microsoft.CodeAnalysis.CodeFixes;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using System.Collections.Immutable;
|
||||
using System.Composition;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Analyzers
|
||||
{
|
||||
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ComponentParametersShouldNotBePublicCodeFixProvider)), Shared]
|
||||
public class ComponentParametersShouldNotBePublicCodeFixProvider : CodeFixProvider
|
||||
{
|
||||
private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.ComponentParametersShouldNotBePublic_FixTitle), Resources.ResourceManager, typeof(Resources));
|
||||
|
||||
public override ImmutableArray<string> FixableDiagnosticIds
|
||||
=> ImmutableArray.Create(ComponentParametersShouldNotBePublicAnalyzer.DiagnosticId);
|
||||
|
||||
public sealed override FixAllProvider GetFixAllProvider()
|
||||
{
|
||||
// See https://github.com/dotnet/roslyn/blob/master/docs/analyzers/FixAllProvider.md for more information on Fix All Providers
|
||||
return WellKnownFixAllProviders.BatchFixer;
|
||||
}
|
||||
|
||||
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
|
||||
{
|
||||
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
|
||||
var diagnostic = context.Diagnostics.First();
|
||||
var diagnosticSpan = diagnostic.Location.SourceSpan;
|
||||
|
||||
// Find the type declaration identified by the diagnostic.
|
||||
var declaration = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<PropertyDeclarationSyntax>().First();
|
||||
|
||||
// Register a code action that will invoke the fix.
|
||||
var title = Title.ToString();
|
||||
context.RegisterCodeFix(
|
||||
CodeAction.Create(
|
||||
title: title,
|
||||
createChangedDocument: c => GetTransformedDocumentAsync(context.Document, root, declaration),
|
||||
equivalenceKey: title),
|
||||
diagnostic);
|
||||
}
|
||||
|
||||
private Task<Document> GetTransformedDocumentAsync(
|
||||
Document document,
|
||||
SyntaxNode root,
|
||||
PropertyDeclarationSyntax declarationNode)
|
||||
{
|
||||
var updatedDeclarationNode = HandlePropertyDeclaration(declarationNode);
|
||||
var newSyntaxRoot = root.ReplaceNode(declarationNode, updatedDeclarationNode);
|
||||
return Task.FromResult(document.WithSyntaxRoot(newSyntaxRoot));
|
||||
}
|
||||
|
||||
private SyntaxNode HandlePropertyDeclaration(PropertyDeclarationSyntax node)
|
||||
{
|
||||
TypeSyntax type = node.Type;
|
||||
if (type == null || type.IsMissing)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var publicModifier = node.Modifiers.FirstOrDefault(m => m.IsKind(SyntaxKind.PublicKeyword));
|
||||
node = node.WithModifiers(
|
||||
node.Modifiers.Remove(publicModifier));
|
||||
return node;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard1.3</TargetFramework>
|
||||
<IncludeBuildOutput>false</IncludeBuildOutput>
|
||||
<NoPackageAnalysis>true</NoPackageAnalysis>
|
||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="2.4.0" PrivateAssets="all" />
|
||||
<PackageReference Update="NETStandard.Library" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Resources.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</Compile>
|
||||
|
||||
<EmbeddedResource Update="Resources.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\shared\BlazorApi.cs" Link="shared\%(Filename)%(Extension)" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Analyzers {
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||
/// </summary>
|
||||
// This class was auto-generated by the StronglyTypedResourceBuilder
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
internal class Resources {
|
||||
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
|
||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||
|
||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||
internal Resources() {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cached ResourceManager instance used by this class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Resources.ResourceManager ResourceManager {
|
||||
get {
|
||||
if (object.ReferenceEquals(resourceMan, null)) {
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNetCore.Blazor.Analyzers.Resources", typeof(Resources).GetTypeInfo().Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
return resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the current thread's CurrentUICulture property for all
|
||||
/// resource lookups using this strongly typed resource class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Globalization.CultureInfo Culture {
|
||||
get {
|
||||
return resourceCulture;
|
||||
}
|
||||
set {
|
||||
resourceCulture = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Component parameters should not be public..
|
||||
/// </summary>
|
||||
internal static string ComponentParametersShouldNotBePublic_Description {
|
||||
get {
|
||||
return ResourceManager.GetString("ComponentParametersShouldNotBePublic_Description", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Make component parameter private.
|
||||
/// </summary>
|
||||
internal static string ComponentParametersShouldNotBePublic_FixTitle {
|
||||
get {
|
||||
return ResourceManager.GetString("ComponentParametersShouldNotBePublic_FixTitle", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Component parameter '{0}' is marked public, but component parameters should not be public..
|
||||
/// </summary>
|
||||
internal static string ComponentParametersShouldNotBePublic_Format {
|
||||
get {
|
||||
return ResourceManager.GetString("ComponentParametersShouldNotBePublic_Format", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Component parameter is marked public.
|
||||
/// </summary>
|
||||
internal static string ComponentParametersShouldNotBePublic_Title {
|
||||
get {
|
||||
return ResourceManager.GetString("ComponentParametersShouldNotBePublic_Title", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="ComponentParametersShouldNotBePublic_Description" xml:space="preserve">
|
||||
<value>Component parameters should not be public.</value>
|
||||
</data>
|
||||
<data name="ComponentParametersShouldNotBePublic_FixTitle" xml:space="preserve">
|
||||
<value>Make component parameter private</value>
|
||||
</data>
|
||||
<data name="ComponentParametersShouldNotBePublic_Format" xml:space="preserve">
|
||||
<value>Component parameter '{0}' is marked public, but component parameters should not be public.</value>
|
||||
</data>
|
||||
<data name="ComponentParametersShouldNotBePublic_Title" xml:space="preserve">
|
||||
<value>Component parameter is marked public</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
<description>Build mechanism for Blazor applications.</description>
|
||||
<dependencies>
|
||||
<dependency id="Microsoft.AspNetCore.Razor.Design" version="$razorversion$" include="all" />
|
||||
<dependency id="Microsoft.AspNetCore.Blazor.Analyzers" version="$version$" include="all" />
|
||||
</dependencies>
|
||||
</metadata>
|
||||
<files>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Blazor.Shared;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Blazor.Shared;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Razor;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Blazor.Shared;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
|
|
|||
|
|
@ -170,6 +170,8 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
() => "Script tags should not be placed inside components because they cannot be updated dynamically. To fix this, move the script tag to the 'index.html' file or another static location. For more information see https://go.microsoft.com/fwlink/?linkid=872131",
|
||||
RazorDiagnosticSeverity.Error);
|
||||
|
||||
// Reserved: BL9993 Component parameters should not be public
|
||||
|
||||
public static RazorDiagnostic Create_DisallowedScriptTag(SourceSpan? source)
|
||||
{
|
||||
var diagnostic = RazorDiagnostic.Create(DisallowedScriptTag, source ?? SourceSpan.Undefined);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using System.Text;
|
|||
using AngleSharp;
|
||||
using AngleSharp.Html;
|
||||
using AngleSharp.Parser.Html;
|
||||
using Microsoft.AspNetCore.Blazor.Shared;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using System;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Blazor.Shared;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
|
|
|||
|
|
@ -4,8 +4,11 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Blazor.Shared;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.Razor;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Razor
|
||||
|
|
@ -17,6 +20,10 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted)
|
||||
.WithMiscellaneousOptions(SymbolDisplayFormat.FullyQualifiedFormat.MiscellaneousOptions & (~SymbolDisplayMiscellaneousOptions.UseSpecialTypes));
|
||||
|
||||
private static MethodInfo WithMetadataImportOptionsMethodInfo =
|
||||
typeof(CSharpCompilationOptions)
|
||||
.GetMethod("WithMetadataImportOptions", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
public bool IncludeDocumentation { get; set; }
|
||||
|
||||
public int Order { get; set; }
|
||||
|
|
@ -35,6 +42,9 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
return;
|
||||
}
|
||||
|
||||
// We need to see private members too
|
||||
compilation = WithMetadataImportOptionsAll(compilation);
|
||||
|
||||
var componentSymbol = compilation.GetTypeByMetadataName(BlazorApi.IComponent.MetadataName);
|
||||
if (componentSymbol == null)
|
||||
{
|
||||
|
|
@ -49,6 +59,13 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
return;
|
||||
}
|
||||
|
||||
var blazorComponentSymbol = compilation.GetTypeByMetadataName(BlazorApi.BlazorComponent.FullTypeName);
|
||||
if (blazorComponentSymbol == null)
|
||||
{
|
||||
// No definition for BlazorComponent, nothing to do.
|
||||
return;
|
||||
}
|
||||
|
||||
var types = new List<INamedTypeSymbol>();
|
||||
var visitor = new ComponentTypeVisitor(componentSymbol, types);
|
||||
|
||||
|
|
@ -67,11 +84,18 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
for (var i = 0; i < types.Count; i++)
|
||||
{
|
||||
var type = types[i];
|
||||
context.Results.Add(CreateDescriptor(type, parameterSymbol));
|
||||
context.Results.Add(CreateDescriptor(type, parameterSymbol, blazorComponentSymbol));
|
||||
}
|
||||
}
|
||||
|
||||
private TagHelperDescriptor CreateDescriptor(INamedTypeSymbol type, INamedTypeSymbol parameterSymbol)
|
||||
private Compilation WithMetadataImportOptionsAll(Compilation compilation)
|
||||
{
|
||||
var newCompilationOptions = (CSharpCompilationOptions)WithMetadataImportOptionsMethodInfo
|
||||
.Invoke(compilation.Options, new object[] { /* All */ (byte)2 });
|
||||
return compilation.WithOptions(newCompilationOptions);
|
||||
}
|
||||
|
||||
private TagHelperDescriptor CreateDescriptor(INamedTypeSymbol type, INamedTypeSymbol parameterSymbol, INamedTypeSymbol blazorComponentSymbol)
|
||||
{
|
||||
if (type == null)
|
||||
{
|
||||
|
|
@ -102,7 +126,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
// Components have very simple matching rules. The type name (short) matches the tag name.
|
||||
builder.TagMatchingRule(r => r.TagName = type.Name);
|
||||
|
||||
foreach (var property in GetProperties(type, parameterSymbol))
|
||||
foreach (var property in GetProperties(type, parameterSymbol, blazorComponentSymbol))
|
||||
{
|
||||
if (property.kind == PropertyKind.Ignored)
|
||||
{
|
||||
|
|
@ -138,19 +162,27 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
return descriptor;
|
||||
}
|
||||
|
||||
// Does a walk up the inheritance chain to determine the set of 'visible' properties by using
|
||||
// Does a walk up the inheritance chain to determine the set of parameters by using
|
||||
// a dictionary keyed on property name.
|
||||
//
|
||||
// Note that we're only interested in a property if all of the above are true:
|
||||
// - visible (not shadowed)
|
||||
// - has public getter
|
||||
// - has public setter
|
||||
// - is not an indexer
|
||||
private IEnumerable<(IPropertySymbol property, PropertyKind kind)> GetProperties(INamedTypeSymbol type, INamedTypeSymbol parameterSymbol)
|
||||
// We consider parameters to be defined by properties satisfying all of the following:
|
||||
// - are visible (not shadowed)
|
||||
// - have the [Parameter] attribute
|
||||
// - have a setter, even if private
|
||||
// - are not indexers
|
||||
private IEnumerable<(IPropertySymbol property, PropertyKind kind)> GetProperties(INamedTypeSymbol type, INamedTypeSymbol parameterSymbol, INamedTypeSymbol blazorComponentSymbol)
|
||||
{
|
||||
var properties = new Dictionary<string, (IPropertySymbol, PropertyKind)>(StringComparer.Ordinal);
|
||||
do
|
||||
{
|
||||
if (type == blazorComponentSymbol)
|
||||
{
|
||||
// The BlazorComponent base class doesn't have any [Parameter].
|
||||
// Bail out now to avoid walking through its many members, plus the members
|
||||
// of the System.Object base class.
|
||||
break;
|
||||
}
|
||||
|
||||
var members = type.GetMembers();
|
||||
for (var i = 0; i < members.Length; i++)
|
||||
{
|
||||
|
|
@ -174,15 +206,14 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
kind = PropertyKind.Ignored;
|
||||
}
|
||||
|
||||
if (property.GetMethod?.DeclaredAccessibility != Accessibility.Public)
|
||||
if (property.SetMethod == null)
|
||||
{
|
||||
// Non-public getter or no getter
|
||||
// No setter
|
||||
kind = PropertyKind.Ignored;
|
||||
}
|
||||
|
||||
if (property.SetMethod?.DeclaredAccessibility != Accessibility.Public)
|
||||
if (property.IsStatic)
|
||||
{
|
||||
// Non-public setter or no setter
|
||||
kind = PropertyKind.Ignored;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Blazor.Shared;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Blazor.Shared;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Razor;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Blazor.Shared;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
||||
|
|
|
|||
|
|
@ -38,4 +38,8 @@
|
|||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\shared\BlazorApi.cs" Link="shared\%(Filename)%(Extension)" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Blazor.Shared;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using System;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Blazor.Shared;
|
||||
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Blazor.Shared;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
|
||||
using System;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@implements ILayoutComponent
|
||||
@inherits BlazorLayoutComponent
|
||||
|
||||
<div class="sidebar">
|
||||
<NavMenu />
|
||||
|
|
@ -13,7 +13,3 @@
|
|||
@Body
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@functions {
|
||||
public RenderFragment Body { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,9 +11,7 @@
|
|||
and tell us what you think.
|
||||
</div>
|
||||
|
||||
@functions
|
||||
{
|
||||
// This is to demonstrate how a parent component can supply parameters
|
||||
@functions {
|
||||
[Parameter]
|
||||
public string Title { get; set; }
|
||||
string Title { get; set; } // Demonstrates how a parent component can supply parameters
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@implements ILayoutComponent
|
||||
@inherits BlazorLayoutComponent
|
||||
|
||||
<div class="sidebar">
|
||||
<NavMenu />
|
||||
|
|
@ -13,7 +13,3 @@
|
|||
@Body
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@functions {
|
||||
public RenderFragment Body { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,9 +11,7 @@
|
|||
and tell us what you think.
|
||||
</div>
|
||||
|
||||
@functions
|
||||
{
|
||||
// This is to demonstrate how a parent component can supply parameters
|
||||
@functions {
|
||||
[Parameter]
|
||||
public string Title { get; set; }
|
||||
string Title { get; set; } // Demonstrates how a parent component can supply parameters
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Blazor.Reflection;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
|
|
@ -56,26 +57,14 @@ namespace Microsoft.AspNetCore.Blazor.Components
|
|||
{
|
||||
// Do all the reflection up front
|
||||
var injectableProperties =
|
||||
GetPropertiesIncludingInherited(type, _injectablePropertyBindingFlags)
|
||||
MemberAssignment.GetPropertiesIncludingInherited(type, _injectablePropertyBindingFlags)
|
||||
.Where(p => p.GetCustomAttribute<InjectAttribute>() != null);
|
||||
var injectables = injectableProperties.Select(property =>
|
||||
{
|
||||
if (property.SetMethod == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot provide a value for property " +
|
||||
$"'{property.Name}' on type '{type.FullName}' because the property " +
|
||||
$"has no setter.");
|
||||
}
|
||||
|
||||
return
|
||||
(
|
||||
propertyName: property.Name,
|
||||
propertyType: property.PropertyType,
|
||||
setter: (IPropertySetter)Activator.CreateInstance(
|
||||
typeof(PropertySetter<,>).MakeGenericType(type, property.PropertyType),
|
||||
property.SetMethod)
|
||||
);
|
||||
}).ToArray();
|
||||
(
|
||||
propertyName: property.Name,
|
||||
propertyType: property.PropertyType,
|
||||
setter: MemberAssignment.CreatePropertySetter(type, property)
|
||||
)).ToArray();
|
||||
|
||||
// Return an action whose closure can write all the injected properties
|
||||
// without any further reflection calls (just typecasts)
|
||||
|
|
@ -95,40 +84,5 @@ namespace Microsoft.AspNetCore.Blazor.Components
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
private interface IPropertySetter
|
||||
{
|
||||
void SetValue(object target, object value);
|
||||
}
|
||||
|
||||
private class PropertySetter<TTarget, TValue> : IPropertySetter
|
||||
{
|
||||
private readonly Action<TTarget, TValue> _setterDelegate;
|
||||
|
||||
public PropertySetter(MethodInfo setMethod)
|
||||
{
|
||||
_setterDelegate = (Action<TTarget, TValue>)Delegate.CreateDelegate(
|
||||
typeof(Action<TTarget, TValue>), setMethod);
|
||||
}
|
||||
|
||||
public void SetValue(object target, object value)
|
||||
=> _setterDelegate((TTarget)target, (TValue)value);
|
||||
}
|
||||
|
||||
private static IEnumerable<PropertyInfo> GetPropertiesIncludingInherited(
|
||||
Type type, BindingFlags bindingFlags)
|
||||
{
|
||||
while (type != null)
|
||||
{
|
||||
var properties = type.GetProperties(bindingFlags)
|
||||
.Where(prop => prop.DeclaringType == type);
|
||||
foreach (var property in properties)
|
||||
{
|
||||
yield return property;
|
||||
}
|
||||
|
||||
type = type.BaseType;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,5 +31,11 @@ namespace Microsoft.AspNetCore.Blazor.Components
|
|||
/// </summary>
|
||||
public object Value
|
||||
=> _frames[_frameIndex].AttributeValue;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="RenderTreeFrame" /> that holds the parameter name and value.
|
||||
/// </summary>
|
||||
internal ref RenderTreeFrame Frame
|
||||
=> ref _frames[_frameIndex];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Blazor.Reflection;
|
||||
using Microsoft.AspNetCore.Blazor.RenderTree;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Components
|
||||
|
|
@ -11,6 +16,13 @@ namespace Microsoft.AspNetCore.Blazor.Components
|
|||
/// </summary>
|
||||
public static class ParameterCollectionExtensions
|
||||
{
|
||||
private const BindingFlags _bindablePropertyFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.IgnoreCase;
|
||||
|
||||
private delegate void WriteParameterAction(ref RenderTreeFrame frame, object target);
|
||||
|
||||
private readonly static IDictionary<Type, IDictionary<string, WriteParameterAction>> _cachedParameterWriters
|
||||
= new ConcurrentDictionary<Type, IDictionary<string, WriteParameterAction>>();
|
||||
|
||||
/// <summary>
|
||||
/// Iterates through the <see cref="ParameterCollection"/>, assigning each parameter
|
||||
/// to a property of the same name on <paramref name="target"/>.
|
||||
|
|
@ -26,54 +38,89 @@ namespace Microsoft.AspNetCore.Blazor.Components
|
|||
throw new ArgumentNullException(nameof(target));
|
||||
}
|
||||
|
||||
var targetType = target.GetType();
|
||||
if (!_cachedParameterWriters.TryGetValue(targetType, out var parameterWriters))
|
||||
{
|
||||
parameterWriters = CreateParameterWriters(targetType);
|
||||
_cachedParameterWriters[targetType] = parameterWriters;
|
||||
}
|
||||
|
||||
foreach (var parameter in parameterCollection)
|
||||
{
|
||||
AssignToProperty(target, parameter);
|
||||
var parameterName = parameter.Name;
|
||||
if (!parameterWriters.TryGetValue(parameterName, out var parameterWriter))
|
||||
{
|
||||
ThrowForUnknownIncomingParameterName(targetType, parameterName);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
parameterWriter(ref parameter.Frame, target);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Unable to set property '{parameterName}' on object of " +
|
||||
$"type '{target.GetType().FullName}'. The error was: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssignToProperty(object target, Parameter parameter)
|
||||
private static IDictionary<string, WriteParameterAction> CreateParameterWriters(Type targetType)
|
||||
{
|
||||
// TODO: Don't just use naive reflection like this. Possible ways to make it faster:
|
||||
// (a) Create and cache a property-assigning open delegate for each (target type,
|
||||
// property name) pair, e.g., using propertyInfo.GetSetMethod().CreateDelegate(...)
|
||||
// That's much faster than caching the PropertyInfo, at least on JIT-enabled platforms.
|
||||
// (b) Or possibly just code-gen an IComponent.SetParameters implementation for each
|
||||
// Razor component. However that might not work well with code-behind inheritance,
|
||||
// because the code-behind wouldn't be able to override it.
|
||||
var result = new Dictionary<string, WriteParameterAction>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var propertyInfo = GetPropertyInfo(target.GetType(), parameter.Name);
|
||||
try
|
||||
foreach (var propertyInfo in GetBindableProperties(targetType))
|
||||
{
|
||||
propertyInfo.SetValue(target, parameter.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Unable to set property '{parameter.Name}' on object of " +
|
||||
$"type '{target.GetType().FullName}'. The error was: {ex.Message}", ex);
|
||||
var propertySetter = MemberAssignment.CreatePropertySetter(targetType, propertyInfo);
|
||||
|
||||
var propertyName = propertyInfo.Name;
|
||||
if (result.ContainsKey(propertyName))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"The type '{targetType.FullName}' declares more than one parameter matching the " +
|
||||
$"name '{propertyName.ToLowerInvariant()}'. Parameter names are case-insensitive and must be unique.");
|
||||
}
|
||||
|
||||
result.Add(propertyName, (ref RenderTreeFrame frame, object target) =>
|
||||
{
|
||||
propertySetter.SetValue(target, frame.AttributeValue);
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static PropertyInfo GetPropertyInfo(Type targetType, string propertyName)
|
||||
private static IEnumerable<PropertyInfo> GetBindableProperties(Type targetType)
|
||||
=> MemberAssignment.GetPropertiesIncludingInherited(targetType, _bindablePropertyFlags)
|
||||
.Where(property => property.IsDefined(typeof(ParameterAttribute)));
|
||||
|
||||
private static void ThrowForUnknownIncomingParameterName(Type targetType, string parameterName)
|
||||
{
|
||||
var flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase;
|
||||
var property = targetType.GetProperty(propertyName, flags);
|
||||
if (property == null)
|
||||
// We know we're going to throw by this stage, so it doesn't matter that the following
|
||||
// reflection code will be slow. We're just trying to help developers see what they did wrong.
|
||||
var propertyInfo = targetType.GetProperty(parameterName, _bindablePropertyFlags);
|
||||
if (propertyInfo != null)
|
||||
{
|
||||
if (!propertyInfo.IsDefined(typeof(ParameterAttribute)))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Object of type '{targetType.FullName}' has a property matching the name '{parameterName}', " +
|
||||
$"but it does not have [{nameof(ParameterAttribute)}] applied.");
|
||||
}
|
||||
else
|
||||
{
|
||||
// This should not happen
|
||||
throw new InvalidOperationException(
|
||||
$"No writer was cached for the property '{propertyInfo.Name}' on type '{targetType.FullName}'.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Object of type '{targetType.FullName}' does not have a property " +
|
||||
$"matching the name '{propertyName}'.");
|
||||
$"matching the name '{parameterName}'.");
|
||||
}
|
||||
|
||||
if (!property.IsDefined(typeof(ParameterAttribute)))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Object of type '{targetType.FullName}' has a property matching the name '{propertyName}', " +
|
||||
$"but it does not have [{nameof(ParameterAttribute)}] applied.");
|
||||
}
|
||||
|
||||
return property;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Blazor.Components;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Layouts
|
||||
{
|
||||
/// <summary>
|
||||
/// Optional base class for components that represent a layout.
|
||||
/// Alternatively, Blazor components may implement <see cref="IComponent"/> directly
|
||||
/// and declare their own parameter named <see cref="BlazorLayoutComponent.Body"/>.
|
||||
/// </summary>
|
||||
public abstract class BlazorLayoutComponent : BlazorComponent
|
||||
{
|
||||
internal const string BodyPropertyName = nameof(Body);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the content to be rendered inside the layout.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
protected RenderFragment Body { get; private set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Blazor.Components;
|
||||
using Microsoft.AspNetCore.Blazor.RenderTree;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Layouts
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates that the type represents a layout.
|
||||
/// </summary>
|
||||
public interface ILayoutComponent : IComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the content to be rendered inside the layout.
|
||||
/// </summary>
|
||||
RenderFragment Body { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Blazor.Components;
|
||||
using System;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Layouts
|
||||
|
|
@ -12,23 +13,29 @@ namespace Microsoft.AspNetCore.Blazor.Layouts
|
|||
public class LayoutAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of the layout. The type always implements <see cref="ILayoutComponent"/>.
|
||||
/// The type of the layout. The type myst implement <see cref="IComponent"/>
|
||||
/// and must accept a parameter with the name 'Body'.
|
||||
/// </summary>
|
||||
public Type LayoutType { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructs an instance of <see cref="LayoutAttribute"/>.
|
||||
/// </summary>
|
||||
/// <param name="layoutType">The type of the layout. This must implement <see cref="ILayoutComponent"/>.</param>
|
||||
/// <param name="layoutType">The type of the layout.</param>
|
||||
public LayoutAttribute(Type layoutType)
|
||||
{
|
||||
LayoutType = layoutType ?? throw new ArgumentNullException(nameof(layoutType));
|
||||
|
||||
if (!typeof(ILayoutComponent).IsAssignableFrom(layoutType))
|
||||
if (!typeof(IComponent).IsAssignableFrom(layoutType))
|
||||
{
|
||||
throw new ArgumentException($"Invalid layout type: {layoutType.FullName} " +
|
||||
$"does not implement {typeof(ILayoutComponent).FullName}.");
|
||||
$"does not implement {typeof(IComponent).FullName}.");
|
||||
}
|
||||
|
||||
// Note that we can't validate its acceptance of a 'Body' parameter at this stage,
|
||||
// because the contract doesn't force them to be known statically. However it will
|
||||
// be a runtime error if the referenced component type rejects the 'Body' parameter
|
||||
// when it gets used.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ namespace Microsoft.AspNetCore.Blazor.Layouts
|
|||
/// </summary>
|
||||
public class LayoutDisplay : IComponent
|
||||
{
|
||||
internal const string NameOfPage = nameof(Page);
|
||||
internal const string NameOfPageParameters = nameof(PageParameters);
|
||||
|
||||
private RenderHandle _renderHandle;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -22,13 +25,13 @@ namespace Microsoft.AspNetCore.Blazor.Layouts
|
|||
/// The type must implement <see cref="IComponent"/>.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Type Page { get; set; }
|
||||
Type Page { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the parameters to pass to the page.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public IDictionary<string, object> PageParameters { get; set; }
|
||||
IDictionary<string, object> PageParameters { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Init(RenderHandle renderHandle)
|
||||
|
|
@ -64,7 +67,7 @@ namespace Microsoft.AspNetCore.Blazor.Layouts
|
|||
builder.OpenComponent(0, componentType);
|
||||
if (bodyParam != null)
|
||||
{
|
||||
builder.AddAttribute(1, nameof(ILayoutComponent.Body), bodyParam);
|
||||
builder.AddAttribute(1, BlazorLayoutComponent.BodyPropertyName, bodyParam);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Reflection
|
||||
{
|
||||
internal interface IPropertySetter
|
||||
{
|
||||
void SetValue(object target, object value);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Reflection
|
||||
{
|
||||
internal class MemberAssignment
|
||||
{
|
||||
public static IEnumerable<PropertyInfo> GetPropertiesIncludingInherited(
|
||||
Type type, BindingFlags bindingFlags)
|
||||
{
|
||||
while (type != null)
|
||||
{
|
||||
var properties = type.GetProperties(bindingFlags)
|
||||
.Where(prop => prop.DeclaringType == type);
|
||||
foreach (var property in properties)
|
||||
{
|
||||
yield return property;
|
||||
}
|
||||
|
||||
type = type.BaseType;
|
||||
}
|
||||
}
|
||||
|
||||
public static IPropertySetter CreatePropertySetter(Type targetType, PropertyInfo property)
|
||||
{
|
||||
if (property.SetMethod == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot provide a value for property " +
|
||||
$"'{property.Name}' on type '{targetType.FullName}' because the property " +
|
||||
$"has no setter.");
|
||||
}
|
||||
|
||||
return (IPropertySetter)Activator.CreateInstance(
|
||||
typeof(PropertySetter<,>).MakeGenericType(targetType, property.PropertyType),
|
||||
property.SetMethod);
|
||||
}
|
||||
|
||||
class PropertySetter<TTarget, TValue> : IPropertySetter
|
||||
{
|
||||
private readonly Action<TTarget, TValue> _setterDelegate;
|
||||
|
||||
public PropertySetter(MethodInfo setMethod)
|
||||
{
|
||||
_setterDelegate = (Action<TTarget, TValue>)Delegate.CreateDelegate(
|
||||
typeof(Action<TTarget, TValue>), setMethod);
|
||||
}
|
||||
|
||||
public void SetValue(object target, object value)
|
||||
=> _setterDelegate((TTarget)target, (TValue)value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -39,13 +39,13 @@ namespace Microsoft.AspNetCore.Blazor.Routing
|
|||
/// current route matches the NavLink href.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string ActiveClass { get; set; }
|
||||
string ActiveClass { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value representing the URL matching behavior.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public NavLinkMatch Match { get; set; }
|
||||
NavLinkMatch Match { get; set; }
|
||||
|
||||
[Inject] private IUriHelper UriHelper { get; set; }
|
||||
|
||||
|
|
|
|||
|
|
@ -29,8 +29,7 @@ namespace Microsoft.AspNetCore.Blazor.Routing
|
|||
/// Gets or sets the assembly that should be searched, along with its referenced
|
||||
/// assemblies, for components matching the URI.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Assembly AppAssembly { get; set; }
|
||||
[Parameter] private Assembly AppAssembly { get; set; }
|
||||
|
||||
private RouteTable Routes { get; set; }
|
||||
|
||||
|
|
@ -70,8 +69,8 @@ namespace Microsoft.AspNetCore.Blazor.Routing
|
|||
protected virtual void Render(RenderTreeBuilder builder, Type handler, IDictionary<string, object> parameters)
|
||||
{
|
||||
builder.OpenComponent(0, typeof(LayoutDisplay));
|
||||
builder.AddAttribute(1, nameof(LayoutDisplay.Page), handler);
|
||||
builder.AddAttribute(2, nameof(LayoutDisplay.PageParameters), parameters);
|
||||
builder.AddAttribute(1, LayoutDisplay.NameOfPage, handler);
|
||||
builder.AddAttribute(2, LayoutDisplay.NameOfPageParameters, parameters);
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Razor
|
||||
namespace Microsoft.AspNetCore.Blazor.Shared
|
||||
{
|
||||
// Constants for method names used in code-generation
|
||||
// Keep these in sync with the actual definitions
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Blazor.Components;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CodeFixes;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using TestHelper;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Analyzers.Test
|
||||
{
|
||||
public class ComponentParametersShouldNotBePublic : CodeFixVerifier
|
||||
{
|
||||
static string BlazorParameterSource = $@"
|
||||
namespace {typeof(ParameterAttribute).Namespace}
|
||||
{{
|
||||
public class {typeof(ParameterAttribute).Name} : System.Attribute
|
||||
{{
|
||||
}}
|
||||
}}
|
||||
";
|
||||
|
||||
[Fact]
|
||||
public void IgnoresPublicPropertiesWithoutParameterAttribute()
|
||||
{
|
||||
var test = @"
|
||||
namespace ConsoleApplication1
|
||||
{
|
||||
class TypeName
|
||||
{
|
||||
public string MyProperty { get; set; }
|
||||
}
|
||||
}" + BlazorParameterSource;
|
||||
|
||||
VerifyCSharpDiagnostic(test);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IgnoresNonpublicPropertiesWithParameterAttribute()
|
||||
{
|
||||
var test = @"
|
||||
namespace ConsoleApplication1
|
||||
{
|
||||
using " + typeof(ParameterAttribute).Namespace + @";
|
||||
|
||||
class TypeName
|
||||
{
|
||||
[Parameter] string MyPropertyNoModifer { get; set; }
|
||||
[Parameter] private string MyPropertyPrivate { get; set; }
|
||||
[Parameter] protected string MyPropertyProtected { get; set; }
|
||||
[Parameter] internal string MyPropertyInternal { get; set; }
|
||||
}
|
||||
}" + BlazorParameterSource;
|
||||
|
||||
VerifyCSharpDiagnostic(test);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddsDiagnosticAndFixForPublicPropertiesWithParameterAttribute()
|
||||
{
|
||||
var test = @"
|
||||
namespace ConsoleApplication1
|
||||
{
|
||||
using " + typeof(ParameterAttribute).Namespace + @";
|
||||
|
||||
class TypeName
|
||||
{
|
||||
[Parameter] public string BadProperty1 { get; set; }
|
||||
[Parameter] public object BadProperty2 { get; set; }
|
||||
}
|
||||
}" + BlazorParameterSource;
|
||||
|
||||
VerifyCSharpDiagnostic(test,
|
||||
new DiagnosticResult
|
||||
{
|
||||
Id = "BL9993",
|
||||
Message = "Component parameter 'BadProperty1' is marked public, but component parameters should not be public.",
|
||||
Severity = DiagnosticSeverity.Warning,
|
||||
Locations = new[]
|
||||
{
|
||||
new DiagnosticResultLocation("Test0.cs", 8, 13)
|
||||
}
|
||||
},
|
||||
new DiagnosticResult
|
||||
{
|
||||
Id = "BL9993",
|
||||
Message = "Component parameter 'BadProperty2' is marked public, but component parameters should not be public.",
|
||||
Severity = DiagnosticSeverity.Warning,
|
||||
Locations = new[]
|
||||
{
|
||||
new DiagnosticResultLocation("Test0.cs", 9, 13)
|
||||
}
|
||||
});
|
||||
|
||||
VerifyCSharpFix(test, @"
|
||||
namespace ConsoleApplication1
|
||||
{
|
||||
using " + typeof(ParameterAttribute).Namespace + @";
|
||||
|
||||
class TypeName
|
||||
{
|
||||
[Parameter] string BadProperty1 { get; set; }
|
||||
[Parameter] object BadProperty2 { get; set; }
|
||||
}
|
||||
}" + BlazorParameterSource);
|
||||
}
|
||||
|
||||
protected override CodeFixProvider GetCSharpCodeFixProvider()
|
||||
{
|
||||
return new ComponentParametersShouldNotBePublicCodeFixProvider();
|
||||
}
|
||||
|
||||
protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
|
||||
{
|
||||
return new ComponentParametersShouldNotBePublicAnalyzer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
// Most of the code in this file comes from the default Roslyn Analyzer project template
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CodeActions;
|
||||
using Microsoft.CodeAnalysis.Formatting;
|
||||
using Microsoft.CodeAnalysis.Simplification;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace TestHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Diagnostic Producer class with extra methods dealing with applying codefixes
|
||||
/// All methods are static
|
||||
/// </summary>
|
||||
public abstract partial class CodeFixVerifier : DiagnosticVerifier
|
||||
{
|
||||
/// <summary>
|
||||
/// Apply the inputted CodeAction to the inputted document.
|
||||
/// Meant to be used to apply codefixes.
|
||||
/// </summary>
|
||||
/// <param name="document">The Document to apply the fix on</param>
|
||||
/// <param name="codeAction">A CodeAction that will be applied to the Document.</param>
|
||||
/// <returns>A Document with the changes from the CodeAction</returns>
|
||||
private static Document ApplyFix(Document document, CodeAction codeAction)
|
||||
{
|
||||
var operations = codeAction.GetOperationsAsync(CancellationToken.None).Result;
|
||||
var solution = operations.OfType<ApplyChangesOperation>().Single().ChangedSolution;
|
||||
return solution.GetDocument(document.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compare two collections of Diagnostics,and return a list of any new diagnostics that appear only in the second collection.
|
||||
/// Note: Considers Diagnostics to be the same if they have the same Ids. In the case of multiple diagnostics with the same Id in a row,
|
||||
/// this method may not necessarily return the new one.
|
||||
/// </summary>
|
||||
/// <param name="diagnostics">The Diagnostics that existed in the code before the CodeFix was applied</param>
|
||||
/// <param name="newDiagnostics">The Diagnostics that exist in the code after the CodeFix was applied</param>
|
||||
/// <returns>A list of Diagnostics that only surfaced in the code after the CodeFix was applied</returns>
|
||||
private static IEnumerable<Diagnostic> GetNewDiagnostics(IEnumerable<Diagnostic> diagnostics, IEnumerable<Diagnostic> newDiagnostics)
|
||||
{
|
||||
var oldArray = diagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray();
|
||||
var newArray = newDiagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray();
|
||||
|
||||
int oldIndex = 0;
|
||||
int newIndex = 0;
|
||||
|
||||
while (newIndex < newArray.Length)
|
||||
{
|
||||
if (oldIndex < oldArray.Length && oldArray[oldIndex].Id == newArray[newIndex].Id)
|
||||
{
|
||||
++oldIndex;
|
||||
++newIndex;
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return newArray[newIndex++];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the existing compiler diagnostics on the inputted document.
|
||||
/// </summary>
|
||||
/// <param name="document">The Document to run the compiler diagnostic analyzers on</param>
|
||||
/// <returns>The compiler diagnostics that were found in the code</returns>
|
||||
private static IEnumerable<Diagnostic> GetCompilerDiagnostics(Document document)
|
||||
{
|
||||
return document.GetSemanticModelAsync().Result.GetDiagnostics();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a document, turn it into a string based on the syntax root
|
||||
/// </summary>
|
||||
/// <param name="document">The Document to be converted to a string</param>
|
||||
/// <returns>A string containing the syntax of the Document after formatting</returns>
|
||||
private static string GetStringFromDocument(Document document)
|
||||
{
|
||||
var simplifiedDoc = Simplifier.ReduceAsync(document, Simplifier.Annotation).Result;
|
||||
var root = simplifiedDoc.GetSyntaxRootAsync().Result;
|
||||
root = Formatter.Format(root, Formatter.Annotation, simplifiedDoc.Project.Solution.Workspace);
|
||||
return root.GetText().ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
// Most of the code in this file comes from the default Roslyn Analyzer project template
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using System;
|
||||
|
||||
namespace TestHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Location where the diagnostic appears, as determined by path, line number, and column number.
|
||||
/// </summary>
|
||||
public struct DiagnosticResultLocation
|
||||
{
|
||||
public DiagnosticResultLocation(string path, int line, int column)
|
||||
{
|
||||
if (line < -1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(line), "line must be >= -1");
|
||||
}
|
||||
|
||||
if (column < -1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(column), "column must be >= -1");
|
||||
}
|
||||
|
||||
this.Path = path;
|
||||
this.Line = line;
|
||||
this.Column = column;
|
||||
}
|
||||
|
||||
public string Path { get; }
|
||||
public int Line { get; }
|
||||
public int Column { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Struct that stores information about a Diagnostic appearing in a source
|
||||
/// </summary>
|
||||
public struct DiagnosticResult
|
||||
{
|
||||
private DiagnosticResultLocation[] locations;
|
||||
|
||||
public DiagnosticResultLocation[] Locations
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this.locations == null)
|
||||
{
|
||||
this.locations = new DiagnosticResultLocation[] { };
|
||||
}
|
||||
return this.locations;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
this.locations = value;
|
||||
}
|
||||
}
|
||||
|
||||
public DiagnosticSeverity Severity { get; set; }
|
||||
|
||||
public string Id { get; set; }
|
||||
|
||||
public string Message { get; set; }
|
||||
|
||||
public string Path
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.Locations.Length > 0 ? this.Locations[0].Path : "";
|
||||
}
|
||||
}
|
||||
|
||||
public int Line
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.Locations.Length > 0 ? this.Locations[0].Line : -1;
|
||||
}
|
||||
}
|
||||
|
||||
public int Column
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.Locations.Length > 0 ? this.Locations[0].Column : -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
// Most of the code in this file comes from the default Roslyn Analyzer project template
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
namespace TestHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Class for turning strings into documents and getting the diagnostics on them
|
||||
/// All methods are static
|
||||
/// </summary>
|
||||
public abstract partial class DiagnosticVerifier
|
||||
{
|
||||
private static readonly MetadataReference CorlibReference = MetadataReference.CreateFromFile(typeof(object).Assembly.Location);
|
||||
private static readonly MetadataReference SystemCoreReference = MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location);
|
||||
private static readonly MetadataReference CSharpSymbolsReference = MetadataReference.CreateFromFile(typeof(CSharpCompilation).Assembly.Location);
|
||||
private static readonly MetadataReference CodeAnalysisReference = MetadataReference.CreateFromFile(typeof(Compilation).Assembly.Location);
|
||||
|
||||
internal static string DefaultFilePathPrefix = "Test";
|
||||
internal static string CSharpDefaultFileExt = "cs";
|
||||
internal static string VisualBasicDefaultExt = "vb";
|
||||
internal static string TestProjectName = "TestProject";
|
||||
|
||||
#region Get Diagnostics
|
||||
|
||||
/// <summary>
|
||||
/// Given classes in the form of strings, their language, and an IDiagnosticAnalyzer to apply to it, return the diagnostics found in the string after converting it to a document.
|
||||
/// </summary>
|
||||
/// <param name="sources">Classes in the form of strings</param>
|
||||
/// <param name="language">The language the source classes are in</param>
|
||||
/// <param name="analyzer">The analyzer to be run on the sources</param>
|
||||
/// <returns>An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location</returns>
|
||||
private static Diagnostic[] GetSortedDiagnostics(string[] sources, string language, DiagnosticAnalyzer analyzer)
|
||||
{
|
||||
return GetSortedDiagnosticsFromDocuments(analyzer, GetDocuments(sources, language));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given an analyzer and a document to apply it to, run the analyzer and gather an array of diagnostics found in it.
|
||||
/// The returned diagnostics are then ordered by location in the source document.
|
||||
/// </summary>
|
||||
/// <param name="analyzer">The analyzer to run on the documents</param>
|
||||
/// <param name="documents">The Documents that the analyzer will be run on</param>
|
||||
/// <returns>An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location</returns>
|
||||
protected static Diagnostic[] GetSortedDiagnosticsFromDocuments(DiagnosticAnalyzer analyzer, Document[] documents)
|
||||
{
|
||||
var projects = new HashSet<Project>();
|
||||
foreach (var document in documents)
|
||||
{
|
||||
projects.Add(document.Project);
|
||||
}
|
||||
|
||||
var diagnostics = new List<Diagnostic>();
|
||||
foreach (var project in projects)
|
||||
{
|
||||
var compilationWithAnalyzers = project.GetCompilationAsync().Result.WithAnalyzers(ImmutableArray.Create(analyzer));
|
||||
var diags = compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().Result;
|
||||
foreach (var diag in diags)
|
||||
{
|
||||
if (diag.Location == Location.None || diag.Location.IsInMetadata)
|
||||
{
|
||||
diagnostics.Add(diag);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < documents.Length; i++)
|
||||
{
|
||||
var document = documents[i];
|
||||
var tree = document.GetSyntaxTreeAsync().Result;
|
||||
if (tree == diag.Location.SourceTree)
|
||||
{
|
||||
diagnostics.Add(diag);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var results = SortDiagnostics(diagnostics);
|
||||
diagnostics.Clear();
|
||||
return results;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sort diagnostics by location in source document
|
||||
/// </summary>
|
||||
/// <param name="diagnostics">The list of Diagnostics to be sorted</param>
|
||||
/// <returns>An IEnumerable containing the Diagnostics in order of Location</returns>
|
||||
private static Diagnostic[] SortDiagnostics(IEnumerable<Diagnostic> diagnostics)
|
||||
{
|
||||
return diagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Set up compilation and documents
|
||||
/// <summary>
|
||||
/// Given an array of strings as sources and a language, turn them into a project and return the documents and spans of it.
|
||||
/// </summary>
|
||||
/// <param name="sources">Classes in the form of strings</param>
|
||||
/// <param name="language">The language the source code is in</param>
|
||||
/// <returns>A Tuple containing the Documents produced from the sources and their TextSpans if relevant</returns>
|
||||
private static Document[] GetDocuments(string[] sources, string language)
|
||||
{
|
||||
if (language != LanguageNames.CSharp && language != LanguageNames.VisualBasic)
|
||||
{
|
||||
throw new ArgumentException("Unsupported Language");
|
||||
}
|
||||
|
||||
var project = CreateProject(sources, language);
|
||||
var documents = project.Documents.ToArray();
|
||||
|
||||
if (sources.Length != documents.Length)
|
||||
{
|
||||
throw new InvalidOperationException("Amount of sources did not match amount of Documents created");
|
||||
}
|
||||
|
||||
return documents;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a Document from a string through creating a project that contains it.
|
||||
/// </summary>
|
||||
/// <param name="source">Classes in the form of a string</param>
|
||||
/// <param name="language">The language the source code is in</param>
|
||||
/// <returns>A Document created from the source string</returns>
|
||||
protected static Document CreateDocument(string source, string language = LanguageNames.CSharp)
|
||||
{
|
||||
return CreateProject(new[] { source }, language).Documents.First();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a project using the inputted strings as sources.
|
||||
/// </summary>
|
||||
/// <param name="sources">Classes in the form of strings</param>
|
||||
/// <param name="language">The language the source code is in</param>
|
||||
/// <returns>A Project created out of the Documents created from the source strings</returns>
|
||||
private static Project CreateProject(string[] sources, string language = LanguageNames.CSharp)
|
||||
{
|
||||
string fileNamePrefix = DefaultFilePathPrefix;
|
||||
string fileExt = language == LanguageNames.CSharp ? CSharpDefaultFileExt : VisualBasicDefaultExt;
|
||||
|
||||
var projectId = ProjectId.CreateNewId(debugName: TestProjectName);
|
||||
|
||||
var solution = new AdhocWorkspace()
|
||||
.CurrentSolution
|
||||
.AddProject(projectId, TestProjectName, TestProjectName, language)
|
||||
.AddMetadataReference(projectId, CorlibReference)
|
||||
.AddMetadataReference(projectId, SystemCoreReference)
|
||||
.AddMetadataReference(projectId, CSharpSymbolsReference)
|
||||
.AddMetadataReference(projectId, CodeAnalysisReference);
|
||||
|
||||
int count = 0;
|
||||
foreach (var source in sources)
|
||||
{
|
||||
var newFileName = fileNamePrefix + count + "." + fileExt;
|
||||
var documentId = DocumentId.CreateNewId(projectId, debugName: newFileName);
|
||||
solution = solution.AddDocument(documentId, newFileName, SourceText.From(source));
|
||||
count++;
|
||||
}
|
||||
return solution.GetProject(projectId);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="2.4.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0" />
|
||||
<PackageReference Include="xunit" Version="2.3.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Blazor.Analyzers\Microsoft.AspNetCore.Blazor.Analyzers.csproj" />
|
||||
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Blazor\Microsoft.AspNetCore.Blazor.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
// Most of the code in this file comes from the default Roslyn Analyzer project template
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CodeActions;
|
||||
using Microsoft.CodeAnalysis.CodeFixes;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Microsoft.CodeAnalysis.Formatting;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Xunit;
|
||||
|
||||
namespace TestHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Superclass of all Unit tests made for diagnostics with codefixes.
|
||||
/// Contains methods used to verify correctness of codefixes
|
||||
/// </summary>
|
||||
public abstract partial class CodeFixVerifier : DiagnosticVerifier
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the codefix being tested (C#) - to be implemented in non-abstract class
|
||||
/// </summary>
|
||||
/// <returns>The CodeFixProvider to be used for CSharp code</returns>
|
||||
protected virtual CodeFixProvider GetCSharpCodeFixProvider()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the codefix being tested (VB) - to be implemented in non-abstract class
|
||||
/// </summary>
|
||||
/// <returns>The CodeFixProvider to be used for VisualBasic code</returns>
|
||||
protected virtual CodeFixProvider GetBasicCodeFixProvider()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called to test a C# codefix when applied on the inputted string as a source
|
||||
/// </summary>
|
||||
/// <param name="oldSource">A class in the form of a string before the CodeFix was applied to it</param>
|
||||
/// <param name="newSource">A class in the form of a string after the CodeFix was applied to it</param>
|
||||
/// <param name="codeFixIndex">Index determining which codefix to apply if there are multiple</param>
|
||||
/// <param name="allowNewCompilerDiagnostics">A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied</param>
|
||||
protected void VerifyCSharpFix(string oldSource, string newSource, int? codeFixIndex = null, bool allowNewCompilerDiagnostics = false)
|
||||
{
|
||||
VerifyFix(LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), GetCSharpCodeFixProvider(), oldSource, newSource, codeFixIndex, allowNewCompilerDiagnostics);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called to test a VB codefix when applied on the inputted string as a source
|
||||
/// </summary>
|
||||
/// <param name="oldSource">A class in the form of a string before the CodeFix was applied to it</param>
|
||||
/// <param name="newSource">A class in the form of a string after the CodeFix was applied to it</param>
|
||||
/// <param name="codeFixIndex">Index determining which codefix to apply if there are multiple</param>
|
||||
/// <param name="allowNewCompilerDiagnostics">A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied</param>
|
||||
protected void VerifyBasicFix(string oldSource, string newSource, int? codeFixIndex = null, bool allowNewCompilerDiagnostics = false)
|
||||
{
|
||||
VerifyFix(LanguageNames.VisualBasic, GetBasicDiagnosticAnalyzer(), GetBasicCodeFixProvider(), oldSource, newSource, codeFixIndex, allowNewCompilerDiagnostics);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// General verifier for codefixes.
|
||||
/// Creates a Document from the source string, then gets diagnostics on it and applies the relevant codefixes.
|
||||
/// Then gets the string after the codefix is applied and compares it with the expected result.
|
||||
/// Note: If any codefix causes new diagnostics to show up, the test fails unless allowNewCompilerDiagnostics is set to true.
|
||||
/// </summary>
|
||||
/// <param name="language">The language the source code is in</param>
|
||||
/// <param name="analyzer">The analyzer to be applied to the source code</param>
|
||||
/// <param name="codeFixProvider">The codefix to be applied to the code wherever the relevant Diagnostic is found</param>
|
||||
/// <param name="oldSource">A class in the form of a string before the CodeFix was applied to it</param>
|
||||
/// <param name="newSource">A class in the form of a string after the CodeFix was applied to it</param>
|
||||
/// <param name="codeFixIndex">Index determining which codefix to apply if there are multiple</param>
|
||||
/// <param name="allowNewCompilerDiagnostics">A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied</param>
|
||||
private void VerifyFix(string language, DiagnosticAnalyzer analyzer, CodeFixProvider codeFixProvider, string oldSource, string newSource, int? codeFixIndex, bool allowNewCompilerDiagnostics)
|
||||
{
|
||||
var document = CreateDocument(oldSource, language);
|
||||
var analyzerDiagnostics = GetSortedDiagnosticsFromDocuments(analyzer, new[] { document });
|
||||
var compilerDiagnostics = GetCompilerDiagnostics(document);
|
||||
var attempts = analyzerDiagnostics.Length;
|
||||
|
||||
for (int i = 0; i < attempts; ++i)
|
||||
{
|
||||
var actions = new List<CodeAction>();
|
||||
var context = new CodeFixContext(document, analyzerDiagnostics[0], (a, d) => actions.Add(a), CancellationToken.None);
|
||||
codeFixProvider.RegisterCodeFixesAsync(context).Wait();
|
||||
|
||||
if (!actions.Any())
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (codeFixIndex != null)
|
||||
{
|
||||
document = ApplyFix(document, actions.ElementAt((int)codeFixIndex));
|
||||
break;
|
||||
}
|
||||
|
||||
document = ApplyFix(document, actions.ElementAt(0));
|
||||
analyzerDiagnostics = GetSortedDiagnosticsFromDocuments(analyzer, new[] { document });
|
||||
|
||||
var newCompilerDiagnostics = GetNewDiagnostics(compilerDiagnostics, GetCompilerDiagnostics(document));
|
||||
|
||||
//check if applying the code fix introduced any new compiler diagnostics
|
||||
if (!allowNewCompilerDiagnostics && newCompilerDiagnostics.Any())
|
||||
{
|
||||
// Format and get the compiler diagnostics again so that the locations make sense in the output
|
||||
document = document.WithSyntaxRoot(Formatter.Format(document.GetSyntaxRootAsync().Result, Formatter.Annotation, document.Project.Solution.Workspace));
|
||||
newCompilerDiagnostics = GetNewDiagnostics(compilerDiagnostics, GetCompilerDiagnostics(document));
|
||||
|
||||
Assert.True(false,
|
||||
string.Format("Fix introduced new compiler diagnostics:\r\n{0}\r\n\r\nNew document:\r\n{1}\r\n",
|
||||
string.Join("\r\n", newCompilerDiagnostics.Select(d => d.ToString())),
|
||||
document.GetSyntaxRootAsync().Result.ToFullString()));
|
||||
}
|
||||
|
||||
//check if there are analyzer diagnostics left after the code fix
|
||||
if (!analyzerDiagnostics.Any())
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//after applying all of the code fixes, compare the resulting string to the inputted one
|
||||
var actual = GetStringFromDocument(document);
|
||||
Assert.Equal(newSource, actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,274 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
// Most of the code in this file comes from the default Roslyn Analyzer project template
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Xunit;
|
||||
|
||||
namespace TestHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Superclass of all Unit Tests for DiagnosticAnalyzers
|
||||
/// </summary>
|
||||
public abstract partial class DiagnosticVerifier
|
||||
{
|
||||
#region To be implemented by Test classes
|
||||
/// <summary>
|
||||
/// Get the CSharp analyzer being tested - to be implemented in non-abstract class
|
||||
/// </summary>
|
||||
protected virtual DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the Visual Basic analyzer being tested (C#) - to be implemented in non-abstract class
|
||||
/// </summary>
|
||||
protected virtual DiagnosticAnalyzer GetBasicDiagnosticAnalyzer()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Verifier wrappers
|
||||
|
||||
/// <summary>
|
||||
/// Called to test a C# DiagnosticAnalyzer when applied on the single inputted string as a source
|
||||
/// Note: input a DiagnosticResult for each Diagnostic expected
|
||||
/// </summary>
|
||||
/// <param name="source">A class in the form of a string to run the analyzer on</param>
|
||||
/// <param name="expected"> DiagnosticResults that should appear after the analyzer is run on the source</param>
|
||||
protected void VerifyCSharpDiagnostic(string source, params DiagnosticResult[] expected)
|
||||
{
|
||||
VerifyDiagnostics(new[] { source }, LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), expected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called to test a VB DiagnosticAnalyzer when applied on the single inputted string as a source
|
||||
/// Note: input a DiagnosticResult for each Diagnostic expected
|
||||
/// </summary>
|
||||
/// <param name="source">A class in the form of a string to run the analyzer on</param>
|
||||
/// <param name="expected">DiagnosticResults that should appear after the analyzer is run on the source</param>
|
||||
protected void VerifyBasicDiagnostic(string source, params DiagnosticResult[] expected)
|
||||
{
|
||||
VerifyDiagnostics(new[] { source }, LanguageNames.VisualBasic, GetBasicDiagnosticAnalyzer(), expected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called to test a C# DiagnosticAnalyzer when applied on the inputted strings as a source
|
||||
/// Note: input a DiagnosticResult for each Diagnostic expected
|
||||
/// </summary>
|
||||
/// <param name="sources">An array of strings to create source documents from to run the analyzers on</param>
|
||||
/// <param name="expected">DiagnosticResults that should appear after the analyzer is run on the sources</param>
|
||||
protected void VerifyCSharpDiagnostic(string[] sources, params DiagnosticResult[] expected)
|
||||
{
|
||||
VerifyDiagnostics(sources, LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), expected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called to test a VB DiagnosticAnalyzer when applied on the inputted strings as a source
|
||||
/// Note: input a DiagnosticResult for each Diagnostic expected
|
||||
/// </summary>
|
||||
/// <param name="sources">An array of strings to create source documents from to run the analyzers on</param>
|
||||
/// <param name="expected">DiagnosticResults that should appear after the analyzer is run on the sources</param>
|
||||
protected void VerifyBasicDiagnostic(string[] sources, params DiagnosticResult[] expected)
|
||||
{
|
||||
VerifyDiagnostics(sources, LanguageNames.VisualBasic, GetBasicDiagnosticAnalyzer(), expected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// General method that gets a collection of actual diagnostics found in the source after the analyzer is run,
|
||||
/// then verifies each of them.
|
||||
/// </summary>
|
||||
/// <param name="sources">An array of strings to create source documents from to run the analyzers on</param>
|
||||
/// <param name="language">The language of the classes represented by the source strings</param>
|
||||
/// <param name="analyzer">The analyzer to be run on the source code</param>
|
||||
/// <param name="expected">DiagnosticResults that should appear after the analyzer is run on the sources</param>
|
||||
private void VerifyDiagnostics(string[] sources, string language, DiagnosticAnalyzer analyzer, params DiagnosticResult[] expected)
|
||||
{
|
||||
var diagnostics = GetSortedDiagnostics(sources, language, analyzer);
|
||||
VerifyDiagnosticResults(diagnostics, analyzer, expected);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Actual comparisons and verifications
|
||||
/// <summary>
|
||||
/// Checks each of the actual Diagnostics found and compares them with the corresponding DiagnosticResult in the array of expected results.
|
||||
/// Diagnostics are considered equal only if the DiagnosticResultLocation, Id, Severity, and Message of the DiagnosticResult match the actual diagnostic.
|
||||
/// </summary>
|
||||
/// <param name="actualResults">The Diagnostics found by the compiler after running the analyzer on the source code</param>
|
||||
/// <param name="analyzer">The analyzer that was being run on the sources</param>
|
||||
/// <param name="expectedResults">Diagnostic Results that should have appeared in the code</param>
|
||||
private static void VerifyDiagnosticResults(IEnumerable<Diagnostic> actualResults, DiagnosticAnalyzer analyzer, params DiagnosticResult[] expectedResults)
|
||||
{
|
||||
int expectedCount = expectedResults.Count();
|
||||
int actualCount = actualResults.Count();
|
||||
|
||||
if (expectedCount != actualCount)
|
||||
{
|
||||
string diagnosticsOutput = actualResults.Any() ? FormatDiagnostics(analyzer, actualResults.ToArray()) : " NONE.";
|
||||
|
||||
Assert.True(false,
|
||||
string.Format("Mismatch between number of diagnostics returned, expected \"{0}\" actual \"{1}\"\r\n\r\nDiagnostics:\r\n{2}\r\n", expectedCount, actualCount, diagnosticsOutput));
|
||||
}
|
||||
|
||||
for (int i = 0; i < expectedResults.Length; i++)
|
||||
{
|
||||
var actual = actualResults.ElementAt(i);
|
||||
var expected = expectedResults[i];
|
||||
|
||||
if (expected.Line == -1 && expected.Column == -1)
|
||||
{
|
||||
if (actual.Location != Location.None)
|
||||
{
|
||||
Assert.True(false,
|
||||
string.Format("Expected:\nA project diagnostic with No location\nActual:\n{0}",
|
||||
FormatDiagnostics(analyzer, actual)));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
VerifyDiagnosticLocation(analyzer, actual, actual.Location, expected.Locations.First());
|
||||
var additionalLocations = actual.AdditionalLocations.ToArray();
|
||||
|
||||
if (additionalLocations.Length != expected.Locations.Length - 1)
|
||||
{
|
||||
Assert.True(false,
|
||||
string.Format("Expected {0} additional locations but got {1} for Diagnostic:\r\n {2}\r\n",
|
||||
expected.Locations.Length - 1, additionalLocations.Length,
|
||||
FormatDiagnostics(analyzer, actual)));
|
||||
}
|
||||
|
||||
for (int j = 0; j < additionalLocations.Length; ++j)
|
||||
{
|
||||
VerifyDiagnosticLocation(analyzer, actual, additionalLocations[j], expected.Locations[j + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
if (actual.Id != expected.Id)
|
||||
{
|
||||
Assert.True(false,
|
||||
string.Format("Expected diagnostic id to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
|
||||
expected.Id, actual.Id, FormatDiagnostics(analyzer, actual)));
|
||||
}
|
||||
|
||||
if (actual.Severity != expected.Severity)
|
||||
{
|
||||
Assert.True(false,
|
||||
string.Format("Expected diagnostic severity to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
|
||||
expected.Severity, actual.Severity, FormatDiagnostics(analyzer, actual)));
|
||||
}
|
||||
|
||||
if (actual.GetMessage() != expected.Message)
|
||||
{
|
||||
Assert.True(false,
|
||||
string.Format("Expected diagnostic message to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
|
||||
expected.Message, actual.GetMessage(), FormatDiagnostics(analyzer, actual)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to VerifyDiagnosticResult that checks the location of a diagnostic and compares it with the location in the expected DiagnosticResult.
|
||||
/// </summary>
|
||||
/// <param name="analyzer">The analyzer that was being run on the sources</param>
|
||||
/// <param name="diagnostic">The diagnostic that was found in the code</param>
|
||||
/// <param name="actual">The Location of the Diagnostic found in the code</param>
|
||||
/// <param name="expected">The DiagnosticResultLocation that should have been found</param>
|
||||
private static void VerifyDiagnosticLocation(DiagnosticAnalyzer analyzer, Diagnostic diagnostic, Location actual, DiagnosticResultLocation expected)
|
||||
{
|
||||
var actualSpan = actual.GetLineSpan();
|
||||
|
||||
Assert.True(actualSpan.Path == expected.Path || (actualSpan.Path != null && actualSpan.Path.Contains("Test0.") && expected.Path.Contains("Test.")),
|
||||
string.Format("Expected diagnostic to be in file \"{0}\" was actually in file \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
|
||||
expected.Path, actualSpan.Path, FormatDiagnostics(analyzer, diagnostic)));
|
||||
|
||||
var actualLinePosition = actualSpan.StartLinePosition;
|
||||
|
||||
// Only check line position if there is an actual line in the real diagnostic
|
||||
if (actualLinePosition.Line > 0)
|
||||
{
|
||||
if (actualLinePosition.Line + 1 != expected.Line)
|
||||
{
|
||||
Assert.True(false,
|
||||
string.Format("Expected diagnostic to be on line \"{0}\" was actually on line \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
|
||||
expected.Line, actualLinePosition.Line + 1, FormatDiagnostics(analyzer, diagnostic)));
|
||||
}
|
||||
}
|
||||
|
||||
// Only check column position if there is an actual column position in the real diagnostic
|
||||
if (actualLinePosition.Character > 0)
|
||||
{
|
||||
if (actualLinePosition.Character + 1 != expected.Column)
|
||||
{
|
||||
Assert.True(false,
|
||||
string.Format("Expected diagnostic to start at column \"{0}\" was actually at column \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
|
||||
expected.Column, actualLinePosition.Character + 1, FormatDiagnostics(analyzer, diagnostic)));
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Formatting Diagnostics
|
||||
/// <summary>
|
||||
/// Helper method to format a Diagnostic into an easily readable string
|
||||
/// </summary>
|
||||
/// <param name="analyzer">The analyzer that this verifier tests</param>
|
||||
/// <param name="diagnostics">The Diagnostics to be formatted</param>
|
||||
/// <returns>The Diagnostics formatted as a string</returns>
|
||||
private static string FormatDiagnostics(DiagnosticAnalyzer analyzer, params Diagnostic[] diagnostics)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
for (int i = 0; i < diagnostics.Length; ++i)
|
||||
{
|
||||
builder.AppendLine("// " + diagnostics[i].ToString());
|
||||
|
||||
var analyzerType = analyzer.GetType();
|
||||
var rules = analyzer.SupportedDiagnostics;
|
||||
|
||||
foreach (var rule in rules)
|
||||
{
|
||||
if (rule != null && rule.Id == diagnostics[i].Id)
|
||||
{
|
||||
var location = diagnostics[i].Location;
|
||||
if (location == Location.None)
|
||||
{
|
||||
builder.AppendFormat("GetGlobalResult({0}.{1})", analyzerType.Name, rule.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.True(location.IsInSource,
|
||||
$"Test base does not currently handle diagnostics in metadata locations. Diagnostic in metadata: {diagnostics[i]}\r\n");
|
||||
|
||||
string resultMethodName = diagnostics[i].Location.SourceTree.FilePath.EndsWith(".cs") ? "GetCSharpResultAt" : "GetBasicResultAt";
|
||||
var linePosition = diagnostics[i].Location.GetLineSpan().StartLinePosition;
|
||||
|
||||
builder.AppendFormat("{0}({1}, {2}, {3}.{4})",
|
||||
resultMethodName,
|
||||
linePosition.Line + 1,
|
||||
linePosition.Character + 1,
|
||||
analyzerType.Name,
|
||||
rule.Id);
|
||||
}
|
||||
|
||||
if (i != diagnostics.Length - 1)
|
||||
{
|
||||
builder.Append(',');
|
||||
}
|
||||
|
||||
builder.AppendLine();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
@ -25,10 +25,10 @@ namespace Test
|
|||
public class MyComponent : BlazorComponent
|
||||
{
|
||||
[Parameter]
|
||||
public int Value { get; set; }
|
||||
int Value { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Action<int> ValueChanged { get; set; }
|
||||
Action<int> ValueChanged { get; set; }
|
||||
}
|
||||
}"));
|
||||
|
||||
|
|
@ -99,10 +99,10 @@ namespace Test
|
|||
public class MyComponent : BlazorComponent
|
||||
{
|
||||
[Parameter]
|
||||
public int Value { get; set; }
|
||||
int Value { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Action<int> OnChanged { get; set; }
|
||||
Action<int> OnChanged { get; set; }
|
||||
}
|
||||
}"));
|
||||
|
||||
|
|
|
|||
|
|
@ -57,10 +57,10 @@ namespace Test
|
|||
|
||||
public class MyComponent : BlazorComponent
|
||||
{
|
||||
[Parameter] public int IntProperty { get; set; }
|
||||
[Parameter] public bool BoolProperty { get; set; }
|
||||
[Parameter] public string StringProperty { get; set; }
|
||||
[Parameter] public SomeType ObjectProperty { get; set; }
|
||||
[Parameter] int IntProperty { get; set; }
|
||||
[Parameter] bool BoolProperty { get; set; }
|
||||
[Parameter] string StringProperty { get; set; }
|
||||
[Parameter] SomeType ObjectProperty { get; set; }
|
||||
}
|
||||
}
|
||||
"));
|
||||
|
|
@ -132,7 +132,7 @@ namespace Test
|
|||
public class MyComponent : BlazorComponent
|
||||
{
|
||||
[Parameter]
|
||||
public string StringProperty { get; set; }
|
||||
string StringProperty { get; set; }
|
||||
}
|
||||
}
|
||||
"));
|
||||
|
|
@ -206,7 +206,7 @@ namespace Test
|
|||
public class MyComponent : BlazorComponent
|
||||
{
|
||||
[Parameter]
|
||||
public Action<UIMouseEventArgs> OnClick { get; set; }
|
||||
Action<UIMouseEventArgs> OnClick { get; set; }
|
||||
}
|
||||
}
|
||||
"));
|
||||
|
|
@ -254,7 +254,7 @@ namespace Test
|
|||
public class MyComponent : BlazorComponent
|
||||
{
|
||||
[Parameter]
|
||||
public Action<UIEventArgs> OnClick { get; set; }
|
||||
Action<UIEventArgs> OnClick { get; set; }
|
||||
}
|
||||
}
|
||||
"));
|
||||
|
|
@ -301,7 +301,7 @@ namespace Test
|
|||
public class MyComponent : BlazorComponent
|
||||
{
|
||||
[Parameter]
|
||||
public bool BoolProperty { get; set; }
|
||||
bool BoolProperty { get; set; }
|
||||
}
|
||||
}"));
|
||||
|
||||
|
|
@ -331,10 +331,10 @@ namespace Test
|
|||
public class MyComponent : BlazorComponent
|
||||
{
|
||||
[Parameter]
|
||||
public string MyAttr { get; set; }
|
||||
string MyAttr { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment ChildContent { get; set; }
|
||||
RenderFragment ChildContent { get; set; }
|
||||
}
|
||||
}
|
||||
"));
|
||||
|
|
@ -376,7 +376,7 @@ namespace Test
|
|||
public class MyComponent : BlazorComponent
|
||||
{
|
||||
[Parameter]
|
||||
public RenderFragment ChildContent { get; set; }
|
||||
RenderFragment ChildContent { get; set; }
|
||||
}
|
||||
}
|
||||
"));
|
||||
|
|
|
|||
|
|
@ -26,10 +26,10 @@ namespace Test
|
|||
|
||||
public class MyComponent : BlazorComponent
|
||||
{
|
||||
[Parameter] public int IntProperty { get; set; }
|
||||
[Parameter] public bool BoolProperty { get; set; }
|
||||
[Parameter] public string StringProperty { get; set; }
|
||||
[Parameter] public SomeType ObjectProperty { get; set; }
|
||||
[Parameter] int IntProperty { get; set; }
|
||||
[Parameter] bool BoolProperty { get; set; }
|
||||
[Parameter] string StringProperty { get; set; }
|
||||
[Parameter] SomeType ObjectProperty { get; set; }
|
||||
}
|
||||
}
|
||||
"));
|
||||
|
|
@ -61,7 +61,7 @@ namespace Test
|
|||
public class MyComponent : BlazorComponent
|
||||
{
|
||||
[Parameter]
|
||||
public string StringProperty { get; set; }
|
||||
string StringProperty { get; set; }
|
||||
}
|
||||
}
|
||||
"));
|
||||
|
|
@ -117,7 +117,7 @@ namespace Test
|
|||
public class MyComponent : BlazorComponent
|
||||
{
|
||||
[Parameter]
|
||||
public Action<UIEventArgs> OnClick { get; set; }
|
||||
Action<UIEventArgs> OnClick { get; set; }
|
||||
}
|
||||
}
|
||||
"));
|
||||
|
|
@ -154,7 +154,7 @@ namespace Test
|
|||
public class MyComponent : BlazorComponent
|
||||
{
|
||||
[Parameter]
|
||||
public Action<UIEventArgs> OnClick { get; set; }
|
||||
Action<UIEventArgs> OnClick { get; set; }
|
||||
}
|
||||
}
|
||||
"));
|
||||
|
|
@ -191,10 +191,10 @@ namespace Test
|
|||
public class MyComponent : BlazorComponent
|
||||
{
|
||||
[Parameter]
|
||||
public string MyAttr { get; set; }
|
||||
string MyAttr { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment ChildContent { get; set; }
|
||||
RenderFragment ChildContent { get; set; }
|
||||
}
|
||||
}
|
||||
"));
|
||||
|
|
@ -387,10 +387,10 @@ namespace Test
|
|||
public class MyComponent : BlazorComponent
|
||||
{
|
||||
[Parameter]
|
||||
public int Value { get; set; }
|
||||
int Value { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Action<int> ValueChanged { get; set; }
|
||||
Action<int> ValueChanged { get; set; }
|
||||
}
|
||||
}"));
|
||||
|
||||
|
|
@ -453,10 +453,10 @@ namespace Test
|
|||
public class MyComponent : BlazorComponent
|
||||
{
|
||||
[Parameter]
|
||||
public int Value { get; set; }
|
||||
int Value { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Action<int> OnChanged { get; set; }
|
||||
Action<int> OnChanged { get; set; }
|
||||
}
|
||||
}"));
|
||||
// Act
|
||||
|
|
|
|||
|
|
@ -119,10 +119,10 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
|
|||
frame => AssertFrame.Text(frame, typeof(MyService2Impl).FullName));
|
||||
}
|
||||
|
||||
public class TestLayout : ILayoutComponent
|
||||
public class TestLayout : IComponent
|
||||
{
|
||||
[Parameter]
|
||||
public RenderFragment Body { get; set; }
|
||||
RenderFragment Body { get; set; }
|
||||
|
||||
public void Init(RenderHandle renderHandle)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -51,10 +51,10 @@ namespace Test
|
|||
|
||||
public class MyComponent : BlazorComponent
|
||||
{
|
||||
[Parameter] public int IntProperty { get; set; }
|
||||
[Parameter] public bool BoolProperty { get; set; }
|
||||
[Parameter] public string StringProperty { get; set; }
|
||||
[Parameter] public SomeType ObjectProperty { get; set; }
|
||||
[Parameter] int IntProperty { get; set; }
|
||||
[Parameter] bool BoolProperty { get; set; }
|
||||
[Parameter] string StringProperty { get; set; }
|
||||
[Parameter] SomeType ObjectProperty { get; set; }
|
||||
}
|
||||
}
|
||||
"));
|
||||
|
|
@ -86,7 +86,7 @@ namespace Test
|
|||
public class MyComponent : BlazorComponent
|
||||
{
|
||||
[Parameter]
|
||||
public string StringProperty { get; set; }
|
||||
string StringProperty { get; set; }
|
||||
}
|
||||
}
|
||||
"));
|
||||
|
|
@ -143,7 +143,7 @@ namespace Test
|
|||
public class MyComponent : BlazorComponent
|
||||
{
|
||||
[Parameter]
|
||||
public Action<UIEventArgs> OnClick { get; set; }
|
||||
Action<UIEventArgs> OnClick { get; set; }
|
||||
}
|
||||
}
|
||||
"));
|
||||
|
|
@ -180,7 +180,7 @@ namespace Test
|
|||
public class MyComponent : BlazorComponent
|
||||
{
|
||||
[Parameter]
|
||||
public Action<UIEventArgs> OnClick { get; set; }
|
||||
Action<UIEventArgs> OnClick { get; set; }
|
||||
}
|
||||
}
|
||||
"));
|
||||
|
|
@ -217,10 +217,10 @@ namespace Test
|
|||
public class MyComponent : BlazorComponent
|
||||
{
|
||||
[Parameter]
|
||||
public string MyAttr { get; set; }
|
||||
string MyAttr { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment ChildContent { get; set; }
|
||||
RenderFragment ChildContent { get; set; }
|
||||
}
|
||||
}
|
||||
"));
|
||||
|
|
@ -641,10 +641,10 @@ namespace Test
|
|||
public class MyComponent : BlazorComponent
|
||||
{
|
||||
[Parameter]
|
||||
public int Value { get; set; }
|
||||
int Value { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Action<int> ValueChanged { get; set; }
|
||||
Action<int> ValueChanged { get; set; }
|
||||
}
|
||||
}"));
|
||||
|
||||
|
|
@ -707,10 +707,10 @@ namespace Test
|
|||
public class MyComponent : BlazorComponent
|
||||
{
|
||||
[Parameter]
|
||||
public int Value { get; set; }
|
||||
int Value { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Action<int> OnChanged { get; set; }
|
||||
Action<int> OnChanged { get; set; }
|
||||
}
|
||||
}"));
|
||||
// Act
|
||||
|
|
|
|||
|
|
@ -28,10 +28,10 @@ namespace Test
|
|||
public void SetParameters(ParameterCollection parameters) { }
|
||||
|
||||
[Parameter]
|
||||
public string MyProperty { get; set; }
|
||||
string MyProperty { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Action<string> MyPropertyChanged { get; set; }
|
||||
Action<string> MyPropertyChanged { get; set; }
|
||||
}
|
||||
}
|
||||
"));
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ namespace Test
|
|||
public void SetParameters(ParameterCollection parameters) { }
|
||||
|
||||
[Parameter]
|
||||
public string MyProperty { get; set; }
|
||||
private string MyProperty { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -138,7 +138,7 @@ namespace Test
|
|||
public class MyComponent : BlazorComponent
|
||||
{
|
||||
[Parameter]
|
||||
public string MyProperty { get; set; }
|
||||
string MyProperty { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -179,7 +179,7 @@ namespace Test
|
|||
public class MyComponent : BlazorComponent
|
||||
{
|
||||
[Parameter]
|
||||
public bool MyProperty { get; set; }
|
||||
bool MyProperty { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -231,7 +231,7 @@ namespace Test
|
|||
public class MyComponent : BlazorComponent
|
||||
{
|
||||
[Parameter]
|
||||
public MyEnum MyProperty { get; set; }
|
||||
MyEnum MyProperty { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -279,7 +279,7 @@ namespace Test
|
|||
public class MyComponent : BlazorComponent
|
||||
{
|
||||
[Parameter]
|
||||
public Action<UIMouseEventArgs> OnClick { get; set; }
|
||||
Action<UIMouseEventArgs> OnClick { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -326,18 +326,21 @@ namespace Test
|
|||
public abstract class MyBase : BlazorComponent
|
||||
{
|
||||
[Parameter]
|
||||
public string Hidden { get; set; }
|
||||
protected string Hidden { get; set; }
|
||||
}
|
||||
|
||||
public class MyComponent : MyBase
|
||||
{
|
||||
[Parameter]
|
||||
public string NoPublicGetter { private get; set; }
|
||||
string NoSetter { get; }
|
||||
|
||||
[Parameter]
|
||||
static string StaticProperty { get; set; }
|
||||
|
||||
public string NoParameterAttribute { get; set; }
|
||||
|
||||
// No attribute here, hides base-class property of the same name.
|
||||
public new int Hidden { get; set; }
|
||||
protected new int Hidden { get; set; }
|
||||
|
||||
public string this[int i]
|
||||
{
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
|
|||
// Arrange/Act
|
||||
_layoutDisplayComponent.SetParameters(new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(LayoutDisplay.Page), typeof(ComponentWithLayout) }
|
||||
{ LayoutDisplay.NameOfPage, typeof(ComponentWithLayout) }
|
||||
});
|
||||
|
||||
// Assert
|
||||
|
|
@ -87,7 +87,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
|
|||
// Arrange/Act
|
||||
_layoutDisplayComponent.SetParameters(new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(LayoutDisplay.Page), typeof(ComponentWithNestedLayout) }
|
||||
{ LayoutDisplay.NameOfPage, typeof(ComponentWithNestedLayout) }
|
||||
});
|
||||
|
||||
// Assert
|
||||
|
|
@ -114,13 +114,13 @@ namespace Microsoft.AspNetCore.Blazor.Test
|
|||
// Arrange
|
||||
_layoutDisplayComponent.SetParameters(new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(LayoutDisplay.Page), typeof(ComponentWithLayout) }
|
||||
{ LayoutDisplay.NameOfPage, typeof(ComponentWithLayout) }
|
||||
});
|
||||
|
||||
// Act
|
||||
_layoutDisplayComponent.SetParameters(new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(LayoutDisplay.Page), typeof(DifferentComponentWithLayout) }
|
||||
{ LayoutDisplay.NameOfPage, typeof(DifferentComponentWithLayout) }
|
||||
});
|
||||
|
||||
// Assert
|
||||
|
|
@ -165,13 +165,13 @@ namespace Microsoft.AspNetCore.Blazor.Test
|
|||
// Arrange
|
||||
_layoutDisplayComponent.SetParameters(new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(LayoutDisplay.Page), typeof(ComponentWithLayout) }
|
||||
{ LayoutDisplay.NameOfPage, typeof(ComponentWithLayout) }
|
||||
});
|
||||
|
||||
// Act
|
||||
_layoutDisplayComponent.SetParameters(new Dictionary<string, object>
|
||||
{
|
||||
{ nameof(LayoutDisplay.Page), typeof(ComponentWithNestedLayout) }
|
||||
{ LayoutDisplay.NameOfPage, typeof(ComponentWithNestedLayout) }
|
||||
});
|
||||
|
||||
// Assert
|
||||
|
|
@ -218,10 +218,10 @@ namespace Microsoft.AspNetCore.Blazor.Test
|
|||
});
|
||||
}
|
||||
|
||||
private class RootLayout : AutoRenderComponent, ILayoutComponent
|
||||
private class RootLayout : AutoRenderComponent
|
||||
{
|
||||
[Parameter]
|
||||
public RenderFragment Body { get; set; }
|
||||
RenderFragment Body { get; set; }
|
||||
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
{
|
||||
|
|
@ -232,10 +232,10 @@ namespace Microsoft.AspNetCore.Blazor.Test
|
|||
}
|
||||
|
||||
[Layout(typeof(RootLayout))]
|
||||
private class NestedLayout : AutoRenderComponent, ILayoutComponent
|
||||
private class NestedLayout : AutoRenderComponent
|
||||
{
|
||||
[Parameter]
|
||||
public RenderFragment Body { get; set; }
|
||||
RenderFragment Body { get; set; }
|
||||
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,308 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Blazor.Components;
|
||||
using Microsoft.AspNetCore.Blazor.RenderTree;
|
||||
using Microsoft.AspNetCore.Blazor.Test.Helpers;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Test
|
||||
{
|
||||
public class ParameterCollectionAssignmentExtensionsTest
|
||||
{
|
||||
[Fact]
|
||||
public void IncomingParameterMatchesAnnotatedPrivateProperty_SetsValue()
|
||||
{
|
||||
// Arrange
|
||||
var someObject = new object();
|
||||
var parameterCollection = new ParameterCollectionBuilder
|
||||
{
|
||||
{ nameof(HasInstanceProperties.IntProp), 123 },
|
||||
{ nameof(HasInstanceProperties.StringProp), "Hello" },
|
||||
{ HasInstanceProperties.ObjectPropName, someObject },
|
||||
}.Build();
|
||||
var target = new HasInstanceProperties();
|
||||
|
||||
// Act
|
||||
parameterCollection.AssignToProperties(target);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(123, target.IntProp);
|
||||
Assert.Equal("Hello", target.StringProp);
|
||||
Assert.Same(someObject, target.ObjectPropCurrentValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IncomingParameterMatchesDeclaredParameterCaseInsensitively_SetsValue()
|
||||
{
|
||||
// Arrange
|
||||
var parameterCollection = new ParameterCollectionBuilder
|
||||
{
|
||||
{ nameof(HasInstanceProperties.IntProp).ToLowerInvariant(), 123 }
|
||||
}.Build();
|
||||
var target = new HasInstanceProperties();
|
||||
|
||||
// Act
|
||||
parameterCollection.AssignToProperties(target);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(123, target.IntProp);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IncomingParameterMatchesInheritedDeclaredParameter_SetsValue()
|
||||
{
|
||||
// Arrange
|
||||
var parameterCollection = new ParameterCollectionBuilder
|
||||
{
|
||||
{ nameof(HasInheritedProperties.IntProp), 123 },
|
||||
{ nameof(HasInheritedProperties.DerivedClassIntProp), 456 },
|
||||
}.Build();
|
||||
var target = new HasInheritedProperties();
|
||||
|
||||
// Act
|
||||
parameterCollection.AssignToProperties(target);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(123, target.IntProp);
|
||||
Assert.Equal(456, target.DerivedClassIntProp);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NoIncomingParameterMatchesDeclaredParameter_LeavesValueUnchanged()
|
||||
{
|
||||
// Arrange
|
||||
var existingObjectValue = new object();
|
||||
var target = new HasInstanceProperties
|
||||
{
|
||||
IntProp = 456,
|
||||
StringProp = "Existing value",
|
||||
ObjectPropCurrentValue = existingObjectValue
|
||||
};
|
||||
|
||||
var parameterCollection = new ParameterCollectionBuilder().Build();
|
||||
|
||||
// Act
|
||||
parameterCollection.AssignToProperties(target);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(456, target.IntProp);
|
||||
Assert.Equal("Existing value", target.StringProp);
|
||||
Assert.Same(existingObjectValue, target.ObjectPropCurrentValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IncomingParameterMatchesNoDeclaredParameter_Throws()
|
||||
{
|
||||
// Arrange
|
||||
var target = new HasPropertyWithoutParameterAttribute();
|
||||
var parameterCollection = new ParameterCollectionBuilder
|
||||
{
|
||||
{ "AnyOtherKey", 123 },
|
||||
}.Build();
|
||||
|
||||
// Act
|
||||
var ex = Assert.Throws<InvalidOperationException>(
|
||||
() => parameterCollection.AssignToProperties(target));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(
|
||||
$"Object of type '{typeof(HasPropertyWithoutParameterAttribute).FullName}' does not have a property " +
|
||||
$"matching the name 'AnyOtherKey'.",
|
||||
ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IncomingParameterMatchesPropertyNotDeclaredAsParameter_Throws()
|
||||
{
|
||||
// Arrange
|
||||
var target = new HasPropertyWithoutParameterAttribute();
|
||||
var parameterCollection = new ParameterCollectionBuilder
|
||||
{
|
||||
{ nameof(HasPropertyWithoutParameterAttribute.IntProp), 123 },
|
||||
}.Build();
|
||||
|
||||
// Act
|
||||
var ex = Assert.Throws<InvalidOperationException>(
|
||||
() => parameterCollection.AssignToProperties(target));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(default, target.IntProp);
|
||||
Assert.Equal(
|
||||
$"Object of type '{typeof(HasPropertyWithoutParameterAttribute).FullName}' has a property matching the name '{nameof(HasPropertyWithoutParameterAttribute.IntProp)}', " +
|
||||
$"but it does not have [{nameof(ParameterAttribute)}] applied.",
|
||||
ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IncomingParameterValueMismatchesDeclaredParameterType_Throws()
|
||||
{
|
||||
// Arrange
|
||||
var someObject = new object();
|
||||
var parameterCollection = new ParameterCollectionBuilder
|
||||
{
|
||||
{ nameof(HasInstanceProperties.IntProp), "string value" },
|
||||
}.Build();
|
||||
var target = new HasInstanceProperties();
|
||||
|
||||
// Act
|
||||
var ex = Assert.Throws<InvalidOperationException>(
|
||||
() => parameterCollection.AssignToProperties(target));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(
|
||||
$"Unable to set property '{nameof(HasInstanceProperties.IntProp)}' on object of " +
|
||||
$"type '{typeof(HasInstanceProperties).FullName}'. The error was: {ex.InnerException.Message}",
|
||||
ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PropertyExplicitSetterException_Throws()
|
||||
{
|
||||
// Arrange
|
||||
var target = new HasPropertyWhoseSetterThrows();
|
||||
var parameterCollection = new ParameterCollectionBuilder
|
||||
{
|
||||
{ nameof(HasPropertyWhoseSetterThrows.StringProp), "anything" },
|
||||
}.Build();
|
||||
|
||||
// Act
|
||||
var ex = Assert.Throws<InvalidOperationException>(
|
||||
() => parameterCollection.AssignToProperties(target));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(
|
||||
$"Unable to set property '{nameof(HasPropertyWhoseSetterThrows.StringProp)}' on object of " +
|
||||
$"type '{typeof(HasPropertyWhoseSetterThrows).FullName}'. The error was: {ex.InnerException.Message}",
|
||||
ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeclaredParametersVaryOnlyByCase_Throws()
|
||||
{
|
||||
// Arrange
|
||||
var parameterCollection = new ParameterCollectionBuilder().Build();
|
||||
var target = new HasParametersVaryingOnlyByCase();
|
||||
|
||||
// Act
|
||||
var ex = Assert.Throws<InvalidOperationException>(() =>
|
||||
parameterCollection.AssignToProperties(target));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(
|
||||
$"The type '{typeof(HasParametersVaryingOnlyByCase).FullName}' declares more than one parameter matching the " +
|
||||
$"name '{nameof(HasParametersVaryingOnlyByCase.MyValue).ToLowerInvariant()}'. Parameter names are case-insensitive and must be unique.",
|
||||
ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeclaredParameterClashesWithInheritedParameter_Throws()
|
||||
{
|
||||
// Even when the developer uses 'new' to shadow an inherited property, this is not
|
||||
// an allowed scenario because there would be no way for the consumer to specify
|
||||
// both property values, and it's no good leaving the shadowed one unset because the
|
||||
// base class can legitimately depend on it for correct functioning.
|
||||
|
||||
// Arrange
|
||||
var parameterCollection = new ParameterCollectionBuilder().Build();
|
||||
var target = new HasParameterClashingWithInherited();
|
||||
|
||||
// Act
|
||||
var ex = Assert.Throws<InvalidOperationException>(() =>
|
||||
parameterCollection.AssignToProperties(target));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(
|
||||
$"The type '{typeof(HasParameterClashingWithInherited).FullName}' declares more than one parameter matching the " +
|
||||
$"name '{nameof(HasParameterClashingWithInherited.IntProp).ToLowerInvariant()}'. Parameter names are case-insensitive and must be unique.",
|
||||
ex.Message);
|
||||
}
|
||||
|
||||
|
||||
class HasInstanceProperties
|
||||
{
|
||||
// "internal" to show we're not requiring public accessors, but also
|
||||
// to keep the assertions simple in the tests
|
||||
|
||||
[Parameter] internal int IntProp { get; set; }
|
||||
[Parameter] internal string StringProp { get; set; }
|
||||
|
||||
// Also a truly private one to show there's nothing special about 'internal'
|
||||
[Parameter] private object ObjectProp { get; set; }
|
||||
|
||||
public static string ObjectPropName => nameof(ObjectProp);
|
||||
public object ObjectPropCurrentValue
|
||||
{
|
||||
get => ObjectProp;
|
||||
set => ObjectProp = value;
|
||||
}
|
||||
}
|
||||
|
||||
class HasPropertyWithoutParameterAttribute
|
||||
{
|
||||
internal int IntProp { get; set; }
|
||||
}
|
||||
|
||||
class HasPropertyWhoseSetterThrows
|
||||
{
|
||||
[Parameter]
|
||||
internal string StringProp
|
||||
{
|
||||
get => string.Empty;
|
||||
set => throw new InvalidOperationException("This setter throws");
|
||||
}
|
||||
}
|
||||
|
||||
class HasInheritedProperties : HasInstanceProperties
|
||||
{
|
||||
[Parameter] internal int DerivedClassIntProp { get; set; }
|
||||
}
|
||||
|
||||
class HasParametersVaryingOnlyByCase
|
||||
{
|
||||
[Parameter] internal object MyValue { get; set; }
|
||||
[Parameter] internal object Myvalue { get; set; }
|
||||
}
|
||||
|
||||
class HasParameterClashingWithInherited : HasInstanceProperties
|
||||
{
|
||||
[Parameter] new int IntProp { get; set; }
|
||||
}
|
||||
|
||||
class ParameterCollectionBuilder : IEnumerable
|
||||
{
|
||||
private readonly List<(string Name, object Value)> _keyValuePairs
|
||||
= new List<(string, object)>();
|
||||
|
||||
public void Add(string name, object value)
|
||||
=> _keyValuePairs.Add((name, value));
|
||||
|
||||
public IEnumerator GetEnumerator()
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public ParameterCollection Build()
|
||||
{
|
||||
var builder = new RenderTreeBuilder(new TestRenderer());
|
||||
builder.OpenComponent<FakeComponent>(0);
|
||||
foreach (var kvp in _keyValuePairs)
|
||||
{
|
||||
builder.AddAttribute(1, kvp.Name, kvp.Value);
|
||||
}
|
||||
builder.CloseComponent();
|
||||
return new ParameterCollection(builder.GetFrames().Array, ownerIndex: 0);
|
||||
}
|
||||
}
|
||||
|
||||
class FakeComponent : IComponent
|
||||
{
|
||||
public void Init(RenderHandle renderHandle)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public void SetParameters(ParameterCollection parameters)
|
||||
=> throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1504,19 +1504,19 @@ namespace Microsoft.AspNetCore.Blazor.Test
|
|||
private class FakeComponent : IComponent
|
||||
{
|
||||
[Parameter]
|
||||
public int IntProperty { get; set; }
|
||||
internal int IntProperty { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string StringProperty { get; set; }
|
||||
internal string StringProperty { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public object ObjectProperty { get; set; }
|
||||
internal object ObjectProperty { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string ReadonlyProperty { get; private set; }
|
||||
internal string ReadonlyProperty { get; private set; }
|
||||
|
||||
[Parameter]
|
||||
private string PrivateProperty { get; set; }
|
||||
string PrivateProperty { get; set; }
|
||||
|
||||
public string NonParameterProperty { get; set; }
|
||||
|
||||
|
|
|
|||
|
|
@ -1110,7 +1110,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
|
|||
private class MessageComponent : AutoRenderComponent
|
||||
{
|
||||
[Parameter]
|
||||
public string Message { get; set; }
|
||||
internal string Message { get; set; }
|
||||
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
{
|
||||
|
|
@ -1121,13 +1121,13 @@ namespace Microsoft.AspNetCore.Blazor.Test
|
|||
private class FakeComponent : IComponent
|
||||
{
|
||||
[Parameter]
|
||||
public int IntProperty { get; set; }
|
||||
internal int IntProperty { get; private set; }
|
||||
|
||||
[Parameter]
|
||||
public string StringProperty { get; set; }
|
||||
internal string StringProperty { get; private set; }
|
||||
|
||||
[Parameter]
|
||||
public object ObjectProperty { get; set; }
|
||||
internal object ObjectProperty { get; set; }
|
||||
|
||||
public RenderHandle RenderHandle { get; private set; }
|
||||
|
||||
|
|
@ -1141,13 +1141,13 @@ namespace Microsoft.AspNetCore.Blazor.Test
|
|||
private class EventComponent : AutoRenderComponent, IComponent, IHandleEvent
|
||||
{
|
||||
[Parameter]
|
||||
public Action<UIEventArgs> OnTest { get; set; }
|
||||
internal Action<UIEventArgs> OnTest { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Action<UIMouseEventArgs> OnClick { get; set; }
|
||||
internal Action<UIMouseEventArgs> OnClick { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Action OnClickAction { get; set; }
|
||||
internal Action OnClickAction { get; set; }
|
||||
|
||||
public bool SkipElement { get; set; }
|
||||
private int renderCount = 0;
|
||||
|
|
@ -1187,10 +1187,10 @@ namespace Microsoft.AspNetCore.Blazor.Test
|
|||
private class ConditionalParentComponent<T> : AutoRenderComponent where T : IComponent
|
||||
{
|
||||
[Parameter]
|
||||
public bool IncludeChild { get; set; }
|
||||
internal bool IncludeChild { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public IDictionary<string, object> ChildParameters { get; set; }
|
||||
internal IDictionary<string, object> ChildParameters { get; set; }
|
||||
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
{
|
||||
|
|
@ -1215,7 +1215,8 @@ namespace Microsoft.AspNetCore.Blazor.Test
|
|||
private class ReRendersParentComponent : AutoRenderComponent
|
||||
{
|
||||
[Parameter]
|
||||
public TestComponent Parent { get; set; }
|
||||
internal TestComponent Parent { get; private set; }
|
||||
|
||||
private bool _isFirstTime = true;
|
||||
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
|
|
@ -1233,7 +1234,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
|
|||
private class RendersSelfAfterEventComponent : IComponent, IHandleEvent
|
||||
{
|
||||
[Parameter]
|
||||
public Action<object> OnClick { get; set; }
|
||||
Action<object> OnClick { get; set; }
|
||||
|
||||
private RenderHandle _renderHandle;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
@using Microsoft.AspNetCore.Blazor.Components
|
||||
<span style="color: red" class="message">@Message</span>
|
||||
@functions {
|
||||
[Parameter] public string Message { get; set; }
|
||||
[Parameter] string Message { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,5 +5,5 @@
|
|||
// Note: The lack of any whitespace or other output besides @ChildContent is important for
|
||||
// what scenarios this component is used for in E2E tests.
|
||||
|
||||
[Parameter] public RenderFragment ChildContent { get; set; }
|
||||
[Parameter] RenderFragment ChildContent { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
@functions {
|
||||
[Parameter]
|
||||
public int SuppliedValue { get; set; }
|
||||
int SuppliedValue { get; set; }
|
||||
|
||||
private int computedValue;
|
||||
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@
|
|||
@functions
|
||||
{
|
||||
[Parameter]
|
||||
public string FirstName { get; set; }
|
||||
string FirstName { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string LastName { get ; set; }
|
||||
string LastName { get ; set; }
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue