Add analyzer to expect public properties for component parameters.

- If we find a parameter that is non-public we will create a warning for that situation and notify the user.
- Updated the naming of existing properties and classes that referred to public/private required properties to be specific to those properties setters (that's what they verified before these changes).
- Updated test expectations and names.
- Changed the code fix provider to no longer worry about the property setters and instead inspect the properties declared accessibility. Did not re-add the property setter code fix provider because these code fix providers do nothing in practice (Razor light bulbs aren't enabled in the editor).
- Added new tests.

#8825
This commit is contained in:
N. Taylor Mullen 2019-07-03 16:07:26 -07:00
parent e31813b9cd
commit b647a223f7
10 changed files with 404 additions and 119 deletions

View File

@ -18,7 +18,8 @@ namespace Microsoft.AspNetCore.Components.Analyzers
{ {
SupportedDiagnostics = ImmutableArray.Create(new[] SupportedDiagnostics = ImmutableArray.Create(new[]
{ {
DiagnosticDescriptors.ComponentParametersShouldNotBePublic, DiagnosticDescriptors.ComponentParametersShouldBePublic,
DiagnosticDescriptors.ComponentParameterSettersShouldBePublic,
DiagnosticDescriptors.ComponentParameterCaptureUnmatchedValuesMustBeUnique, DiagnosticDescriptors.ComponentParameterCaptureUnmatchedValuesMustBeUnique,
DiagnosticDescriptors.ComponentParameterCaptureUnmatchedValuesHasWrongType, DiagnosticDescriptors.ComponentParameterCaptureUnmatchedValuesHasWrongType,
}); });
@ -61,14 +62,27 @@ namespace Microsoft.AspNetCore.Components.Analyzers
{ {
var captureUnmatchedValuesParameters = new List<IPropertySymbol>(); var captureUnmatchedValuesParameters = new List<IPropertySymbol>();
// Per-property validations // Per-property validations
foreach (var property in properties) foreach (var property in properties)
{ {
if (property.SetMethod?.DeclaredAccessibility == Accessibility.Public) var propertyLocation = property.Locations.FirstOrDefault();
if (propertyLocation == null)
{
continue;
}
if (property.DeclaredAccessibility != Accessibility.Public)
{ {
context.ReportDiagnostic(Diagnostic.Create( context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.ComponentParametersShouldNotBePublic, DiagnosticDescriptors.ComponentParametersShouldBePublic,
property.Locations[0], propertyLocation,
property.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)));
}
else if (property.SetMethod?.DeclaredAccessibility != Accessibility.Public)
{
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.ComponentParameterSettersShouldBePublic,
propertyLocation,
property.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat))); property.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)));
} }
@ -76,13 +90,13 @@ namespace Microsoft.AspNetCore.Components.Analyzers
{ {
captureUnmatchedValuesParameters.Add(property); captureUnmatchedValuesParameters.Add(property);
// Check the type, we need to be able to assign a Dictionary<string, object> // Check the type, we need to be able to assign a Dictionary<string, object>
var conversion = context.Compilation.ClassifyConversion(symbols.ParameterCaptureUnmatchedValuesRuntimeType, property.Type); var conversion = context.Compilation.ClassifyConversion(symbols.ParameterCaptureUnmatchedValuesRuntimeType, property.Type);
if (!conversion.Exists || conversion.IsExplicit) if (!conversion.Exists || conversion.IsExplicit)
{ {
context.ReportDiagnostic(Diagnostic.Create( context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.ComponentParameterCaptureUnmatchedValuesHasWrongType, DiagnosticDescriptors.ComponentParameterCaptureUnmatchedValuesHasWrongType,
property.Locations[0], propertyLocation,
property.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat), property.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat),
property.Type.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat), property.Type.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat),
symbols.ParameterCaptureUnmatchedValuesRuntimeType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat))); symbols.ParameterCaptureUnmatchedValuesRuntimeType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)));
@ -90,9 +104,9 @@ namespace Microsoft.AspNetCore.Components.Analyzers
} }
} }
// Check if the type defines multiple CaptureUnmatchedValues parameters. Doing this outside the loop means we place the // Check if the type defines multiple CaptureUnmatchedValues parameters. Doing this outside the loop means we place the
// errors on the type. // errors on the type.
if (captureUnmatchedValuesParameters.Count > 1) if (captureUnmatchedValuesParameters.Count > 1)
{ {
context.ReportDiagnostic(Diagnostic.Create( context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.ComponentParameterCaptureUnmatchedValuesMustBeUnique, DiagnosticDescriptors.ComponentParameterCaptureUnmatchedValuesMustBeUnique,

View File

@ -13,13 +13,13 @@ using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Microsoft.AspNetCore.Components.Analyzers namespace Microsoft.AspNetCore.Components.Analyzers
{ {
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ComponentParametersShouldNotBePublicCodeFixProvider)), Shared] [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ComponentParametersShouldBePublicCodeFixProvider)), Shared]
public class ComponentParametersShouldNotBePublicCodeFixProvider : CodeFixProvider public class ComponentParametersShouldBePublicCodeFixProvider : CodeFixProvider
{ {
private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.ComponentParametersShouldNotBePublic_FixTitle), Resources.ResourceManager, typeof(Resources)); private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.ComponentParametersShouldBePublic_FixTitle), Resources.ResourceManager, typeof(Resources));
public override ImmutableArray<string> FixableDiagnosticIds public override ImmutableArray<string> FixableDiagnosticIds
=> ImmutableArray.Create(DiagnosticDescriptors.ComponentParametersShouldNotBePublic.Id); => ImmutableArray.Create(DiagnosticDescriptors.ComponentParametersShouldBePublic.Id);
public sealed override FixAllProvider GetFixAllProvider() public sealed override FixAllProvider GetFixAllProvider()
{ {
@ -64,9 +64,24 @@ namespace Microsoft.AspNetCore.Components.Analyzers
return null; return null;
} }
var publicModifier = node.Modifiers.FirstOrDefault(m => m.IsKind(SyntaxKind.PublicKeyword)); var newModifiers = node.Modifiers;
node = node.WithModifiers( for (var i = 0; i < node.Modifiers.Count; i++)
node.Modifiers.Remove(publicModifier)); {
var modifier = node.Modifiers[i];
if (modifier.IsKind(SyntaxKind.PrivateKeyword) ||
modifier.IsKind(SyntaxKind.ProtectedKeyword) ||
modifier.IsKind(SyntaxKind.InternalKeyword) ||
// We also remove public in case the user has written something totally backwards such as private public protected Foo
modifier.IsKind(SyntaxKind.PublicKeyword))
{
newModifiers = newModifiers.Remove(modifier);
}
}
var publicModifier = SyntaxFactory.Token(SyntaxKind.PublicKeyword);
newModifiers = newModifiers.Insert(0, publicModifier);
node = node.WithModifiers(newModifiers);
return node; return node;
} }
} }

View File

@ -11,14 +11,14 @@ namespace Microsoft.AspNetCore.Components.Analyzers
// no change of clashing between that and the BL prefix used here. // no change of clashing between that and the BL prefix used here.
// //
// Tracking https://github.com/aspnet/AspNetCore/issues/10382 to rationalize this // Tracking https://github.com/aspnet/AspNetCore/issues/10382 to rationalize this
public static readonly DiagnosticDescriptor ComponentParametersShouldNotBePublic = new DiagnosticDescriptor( public static readonly DiagnosticDescriptor ComponentParameterSettersShouldBePublic = new DiagnosticDescriptor(
"BL0001", "BL0001",
new LocalizableResourceString(nameof(Resources.ComponentParametersShouldNotBePublic_Title), Resources.ResourceManager, typeof(Resources)), new LocalizableResourceString(nameof(Resources.ComponentParameterSettersShouldBePublic_Title), Resources.ResourceManager, typeof(Resources)),
new LocalizableResourceString(nameof(Resources.ComponentParametersShouldNotBePublic_Format), Resources.ResourceManager, typeof(Resources)), new LocalizableResourceString(nameof(Resources.ComponentParameterSettersShouldBePublic_Format), Resources.ResourceManager, typeof(Resources)),
"Encapsulation", "Encapsulation",
DiagnosticSeverity.Warning, DiagnosticSeverity.Warning,
isEnabledByDefault: true, isEnabledByDefault: true,
description: new LocalizableResourceString(nameof(Resources.ComponentParametersShouldNotBePublic_Description), Resources.ResourceManager, typeof(Resources))); description: new LocalizableResourceString(nameof(Resources.ComponentParameterSettersShouldBePublic_Description), Resources.ResourceManager, typeof(Resources)));
public static readonly DiagnosticDescriptor ComponentParameterCaptureUnmatchedValuesMustBeUnique = new DiagnosticDescriptor( public static readonly DiagnosticDescriptor ComponentParameterCaptureUnmatchedValuesMustBeUnique = new DiagnosticDescriptor(
"BL0002", "BL0002",
@ -37,5 +37,14 @@ namespace Microsoft.AspNetCore.Components.Analyzers
DiagnosticSeverity.Warning, DiagnosticSeverity.Warning,
isEnabledByDefault: true, isEnabledByDefault: true,
description: new LocalizableResourceString(nameof(Resources.ComponentParameterCaptureUnmatchedValuesHasWrongType_Description), Resources.ResourceManager, typeof(Resources))); description: new LocalizableResourceString(nameof(Resources.ComponentParameterCaptureUnmatchedValuesHasWrongType_Description), Resources.ResourceManager, typeof(Resources)));
public static readonly DiagnosticDescriptor ComponentParametersShouldBePublic = new DiagnosticDescriptor(
"BL0004",
new LocalizableResourceString(nameof(Resources.ComponentParameterShouldBePublic_Title), Resources.ResourceManager, typeof(Resources)),
new LocalizableResourceString(nameof(Resources.ComponentParameterShouldBePublic_Format), Resources.ResourceManager, typeof(Resources)),
"Encapsulation",
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: new LocalizableResourceString(nameof(Resources.ComponentParametersShouldBePublic_Description), Resources.ResourceManager, typeof(Resources)));
} }
} }

View File

@ -61,7 +61,7 @@ namespace Microsoft.AspNetCore.Components.Analyzers {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Component parameters with CaptureUnmatchedValuess must be a correct type.. /// Looks up a localized string similar to Component parameters with CaptureUnmatchedValues must be a correct type..
/// </summary> /// </summary>
internal static string ComponentParameterCaptureUnmatchedValuesHasWrongType_Description { internal static string ComponentParameterCaptureUnmatchedValuesHasWrongType_Description {
get { get {
@ -70,7 +70,7 @@ namespace Microsoft.AspNetCore.Components.Analyzers {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Component parameter &apos;{0}&apos; defines CaptureUnmatchedValuess but has an unsupported type &apos;{1}&apos;. Use a type assignable from &apos;{2}&apos;.. /// Looks up a localized string similar to Component parameter &apos;{0}&apos; defines CaptureUnmatchedValues but has an unsupported type &apos;{1}&apos;. Use a type assignable from &apos;{2}&apos;..
/// </summary> /// </summary>
internal static string ComponentParameterCaptureUnmatchedValuesHasWrongType_Format { internal static string ComponentParameterCaptureUnmatchedValuesHasWrongType_Format {
get { get {
@ -115,38 +115,65 @@ namespace Microsoft.AspNetCore.Components.Analyzers {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Component parameters should not have public setters.. /// Looks up a localized string similar to Component parameters should have public setters..
/// </summary> /// </summary>
internal static string ComponentParametersShouldNotBePublic_Description { internal static string ComponentParameterSettersShouldBePublic_Description {
get { get {
return ResourceManager.GetString("ComponentParametersShouldNotBePublic_Description", resourceCulture); return ResourceManager.GetString("ComponentParameterSettersShouldBePublic_Description", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Make component parameter private. /// Looks up a localized string similar to Component parameter &apos;{0}&apos; should have a public setter..
/// </summary> /// </summary>
internal static string ComponentParametersShouldNotBePublic_FixTitle { internal static string ComponentParameterSettersShouldBePublic_Format {
get { get {
return ResourceManager.GetString("ComponentParametersShouldNotBePublic_FixTitle", resourceCulture); return ResourceManager.GetString("ComponentParameterSettersShouldBePublic_Format", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Component parameter &apos;{0}&apos; has a public setter, but component parameters should not be publicly settable.. /// Looks up a localized string similar to Component parameter should have public setters..
/// </summary> /// </summary>
internal static string ComponentParametersShouldNotBePublic_Format { internal static string ComponentParameterSettersShouldBePublic_Title {
get { get {
return ResourceManager.GetString("ComponentParametersShouldNotBePublic_Format", resourceCulture); return ResourceManager.GetString("ComponentParameterSettersShouldBePublic_Title", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Component parameter has public setter. /// Looks up a localized string similar to Component parameter &apos;{0}&apos; should be public..
/// </summary> /// </summary>
internal static string ComponentParametersShouldNotBePublic_Title { internal static string ComponentParameterShouldBePublic_Format {
get { get {
return ResourceManager.GetString("ComponentParametersShouldNotBePublic_Title", resourceCulture); return ResourceManager.GetString("ComponentParameterShouldBePublic_Format", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Component parameter should be public..
/// </summary>
internal static string ComponentParameterShouldBePublic_Title {
get {
return ResourceManager.GetString("ComponentParameterShouldBePublic_Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Component parameters should be public..
/// </summary>
internal static string ComponentParametersShouldBePublic_Description {
get {
return ResourceManager.GetString("ComponentParametersShouldBePublic_Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Make component parameters public..
/// </summary>
internal static string ComponentParametersShouldBePublic_FixTitle {
get {
return ResourceManager.GetString("ComponentParametersShouldBePublic_FixTitle", resourceCulture);
} }
} }
} }

View File

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
Version 2.0 Version 2.0
The primary goals of this format is to allow a simple XML format The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes various data types are done through the TypeConverter classes
associated with the data types. associated with the data types.
Example: Example:
... ado.net/XML headers & schema ... ... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader> <resheader name="version">2.0</resheader>
@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment> <comment>This is a comment</comment>
</data> </data>
There are any number of "resheader" rows that contain simple There are any number of "resheader" rows that contain simple
name/value pairs. name/value pairs.
Each data row contains a name, and value. The row also contains a Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture. text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the Classes that don't support this are serialized and stored with the
mimetype set. mimetype set.
The mimetype is used for serialized objects, and tells the The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly: extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below. read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64 mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64 mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64 mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter : using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
--> -->
@ -117,17 +117,14 @@
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="ComponentParametersShouldNotBePublic_Description" xml:space="preserve"> <data name="ComponentParameterSettersShouldBePublic_Description" xml:space="preserve">
<value>Component parameters should not have public setters.</value> <value>Component parameters should have public setters.</value>
</data> </data>
<data name="ComponentParametersShouldNotBePublic_FixTitle" xml:space="preserve"> <data name="ComponentParameterSettersShouldBePublic_Format" xml:space="preserve">
<value>Make component parameter private</value> <value>Component parameter '{0}' should have a public setter.</value>
</data> </data>
<data name="ComponentParametersShouldNotBePublic_Format" xml:space="preserve"> <data name="ComponentParameterSettersShouldBePublic_Title" xml:space="preserve">
<value>Component parameter '{0}' has a public setter, but component parameters should not be publicly settable.</value> <value>Component parameter should have public setters.</value>
</data>
<data name="ComponentParametersShouldNotBePublic_Title" xml:space="preserve">
<value>Component parameter has public setter</value>
</data> </data>
<data name="ComponentParameterCaptureUnmatchedValuesMustBeUnique_Description" xml:space="preserve"> <data name="ComponentParameterCaptureUnmatchedValuesMustBeUnique_Description" xml:space="preserve">
<value>Components may only define a single parameter with CaptureUnmatchedValues.</value> <value>Components may only define a single parameter with CaptureUnmatchedValues.</value>
@ -147,4 +144,16 @@
<data name="ComponentParameterCaptureUnmatchedValuesHasWrongType_Title" xml:space="preserve"> <data name="ComponentParameterCaptureUnmatchedValuesHasWrongType_Title" xml:space="preserve">
<value>Component parameter with CaptureUnmatchedValues has the wrong type</value> <value>Component parameter with CaptureUnmatchedValues has the wrong type</value>
</data> </data>
</root> <data name="ComponentParameterShouldBePublic_Format" xml:space="preserve">
<value>Component parameter '{0}' should be public.</value>
</data>
<data name="ComponentParameterShouldBePublic_Title" xml:space="preserve">
<value>Component parameter should be public.</value>
</data>
<data name="ComponentParametersShouldBePublic_Description" xml:space="preserve">
<value>Component parameters should be public.</value>
</data>
<data name="ComponentParametersShouldBePublic_FixTitle" xml:space="preserve">
<value>Make component parameters public.</value>
</data>
</root>

View File

@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Components.Analyzers.Test
using {typeof(ParameterAttribute).Namespace}; using {typeof(ParameterAttribute).Namespace};
class TypeName class TypeName
{{ {{
[Parameter(CaptureUnmatchedValues = true)] {propertyType} MyProperty {{ get; set; }} [Parameter(CaptureUnmatchedValues = true)] public {propertyType} MyProperty {{ get; set; }}
}} }}
}}" + ComponentsTestDeclarations.Source; }}" + ComponentsTestDeclarations.Source;
@ -39,7 +39,7 @@ namespace Microsoft.AspNetCore.Components.Analyzers.Test
using {typeof(ParameterAttribute).Namespace}; using {typeof(ParameterAttribute).Namespace};
class TypeName class TypeName
{{ {{
[Parameter(CaptureUnmatchedValues = false)] string MyProperty {{ get; set; }} [Parameter(CaptureUnmatchedValues = false)] public string MyProperty {{ get; set; }}
}} }}
}}" + ComponentsTestDeclarations.Source; }}" + ComponentsTestDeclarations.Source;
@ -55,7 +55,7 @@ namespace Microsoft.AspNetCore.Components.Analyzers.Test
using {typeof(ParameterAttribute).Namespace}; using {typeof(ParameterAttribute).Namespace};
class TypeName class TypeName
{{ {{
[Parameter(CaptureUnmatchedValues = true)] string MyProperty {{ get; set; }} [Parameter(CaptureUnmatchedValues = true)] public string MyProperty {{ get; set; }}
}} }}
}}" + ComponentsTestDeclarations.Source; }}" + ComponentsTestDeclarations.Source;
@ -67,7 +67,7 @@ namespace Microsoft.AspNetCore.Components.Analyzers.Test
Severity = DiagnosticSeverity.Warning, Severity = DiagnosticSeverity.Warning,
Locations = new[] Locations = new[]
{ {
new DiagnosticResultLocation("Test0.cs", 7, 63) new DiagnosticResultLocation("Test0.cs", 7, 70)
} }
}); });
} }

View File

@ -21,8 +21,8 @@ namespace Microsoft.AspNetCore.Components.Analyzers.Test
using {typeof(ParameterAttribute).Namespace}; using {typeof(ParameterAttribute).Namespace};
class TypeName class TypeName
{{ {{
[Parameter(CaptureUnmatchedValues = false)] string MyProperty {{ get; set; }} [Parameter(CaptureUnmatchedValues = false)] public string MyProperty {{ get; set; }}
[Parameter(CaptureUnmatchedValues = true)] Dictionary<string, object> MyOtherProperty {{ get; set; }} [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object> MyOtherProperty {{ get; set; }}
}} }}
}}" + ComponentsTestDeclarations.Source; }}" + ComponentsTestDeclarations.Source;
@ -39,8 +39,8 @@ namespace Microsoft.AspNetCore.Components.Analyzers.Test
using {typeof(ParameterAttribute).Namespace}; using {typeof(ParameterAttribute).Namespace};
class TypeName class TypeName
{{ {{
[Parameter(CaptureUnmatchedValues = true)] Dictionary<string, object> MyProperty {{ get; set; }} [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object> MyProperty {{ get; set; }}
[Parameter(CaptureUnmatchedValues = true)] Dictionary<string, object> MyOtherProperty {{ get; set; }} [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object> MyOtherProperty {{ get; set; }}
}} }}
}}" + ComponentsTestDeclarations.Source; }}" + ComponentsTestDeclarations.Source;

View File

@ -0,0 +1,95 @@
// 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.Diagnostics;
using TestHelper;
using Xunit;
namespace Microsoft.AspNetCore.Components.Analyzers
{
public class ComponentParameterSettersShouldBePublicTest : DiagnosticVerifier
{
[Fact]
public void IgnoresPublicSettersProperties()
{
var test = $@"
namespace ConsoleApplication1
{{
using {typeof(ParameterAttribute).Namespace};
class TypeName
{{
[Parameter] public string MyProperty {{ get; set; }}
}}
}}" + ComponentsTestDeclarations.Source;
VerifyCSharpDiagnostic(test);
}
[Fact]
public void IgnoresPrivateSettersNonParameterProperties()
{
var test = $@"
namespace ConsoleApplication1
{{
using {typeof(ParameterAttribute).Namespace};
class TypeName
{{
private string MyProperty {{ get; private set; }}
}}
}}" + ComponentsTestDeclarations.Source;
VerifyCSharpDiagnostic(test);
}
[Fact]
public void ErrorsForNonPublicSetterParameters()
{
var test = $@"
namespace ConsoleApplication1
{{
using {typeof(ParameterAttribute).Namespace};
class TypeName
{{
[Parameter] public string MyProperty1 {{ get; private set; }}
[Parameter] public string MyProperty2 {{ get; protected set; }}
[Parameter] public string MyProperty3 {{ get; internal set; }}
}}
}}" + ComponentsTestDeclarations.Source;
VerifyCSharpDiagnostic(test,
new DiagnosticResult
{
Id = DiagnosticDescriptors.ComponentParameterSettersShouldBePublic.Id,
Message = "Component parameter 'ConsoleApplication1.TypeName.MyProperty1' should have a public setter.",
Severity = DiagnosticSeverity.Warning,
Locations = new[]
{
new DiagnosticResultLocation("Test0.cs", 7, 39)
}
},
new DiagnosticResult
{
Id = DiagnosticDescriptors.ComponentParameterSettersShouldBePublic.Id,
Message = "Component parameter 'ConsoleApplication1.TypeName.MyProperty2' should have a public setter.",
Severity = DiagnosticSeverity.Warning,
Locations = new[]
{
new DiagnosticResultLocation("Test0.cs", 8, 39)
}
},
new DiagnosticResult
{
Id = DiagnosticDescriptors.ComponentParameterSettersShouldBePublic.Id,
Message = "Component parameter 'ConsoleApplication1.TypeName.MyProperty3' should have a public setter.",
Severity = DiagnosticSeverity.Warning,
Locations = new[]
{
new DiagnosticResultLocation("Test0.cs", 9, 39)
}
});
}
protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() => new ComponentParameterAnalyzer();
}
}

View File

@ -9,17 +9,17 @@ using Xunit;
namespace Microsoft.AspNetCore.Components.Analyzers.Test namespace Microsoft.AspNetCore.Components.Analyzers.Test
{ {
public class ComponentParametersShouldNotBePublic : CodeFixVerifier public class ComponentParametersShouldBePublicCodeFixProviderTest : CodeFixVerifier
{ {
[Fact] [Fact]
public void IgnoresPublicPropertiesWithoutParameterAttribute() public void IgnoresPrivatePropertiesWithoutParameterAttribute()
{ {
var test = @" var test = @"
namespace ConsoleApplication1 namespace ConsoleApplication1
{ {
class TypeName class TypeName
{ {
public string MyProperty { get; set; } private string MyProperty { get; set; }
} }
}" + ComponentsTestDeclarations.Source; }" + ComponentsTestDeclarations.Source;
@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.Components.Analyzers.Test
} }
[Fact] [Fact]
public void IgnoresNonpublicPropertiesWithParameterAttribute() public void AddsDiagnosticAndFixForPrivatePropertiesWithParameterAttribute()
{ {
var test = @" var test = @"
namespace ConsoleApplication1 namespace ConsoleApplication1
@ -36,50 +36,30 @@ namespace Microsoft.AspNetCore.Components.Analyzers.Test
class TypeName class TypeName
{ {
[Parameter] string MyPropertyNoModifer { get; set; } [Parameter] private string BadProperty1 { get; set; }
[Parameter] private string MyPropertyPrivate { get; set; } [CascadingParameter] private object BadProperty2 { get; set; }
[CascadingParameter] protected string MyPropertyProtected { get; set; }
[CascadingParameter] internal string MyPropertyInternal { get; set; }
}
}" + ComponentsTestDeclarations.Source;
VerifyCSharpDiagnostic(test);
}
[Fact]
public void AddsDiagnosticAndFixForPublicPropertiesWithParameterAttribute()
{
var test = @"
namespace ConsoleApplication1
{
using " + typeof(ParameterAttribute).Namespace + @";
class TypeName
{
[Parameter] public string BadProperty1 { get; set; }
[CascadingParameter] public object BadProperty2 { get; set; }
} }
}" + ComponentsTestDeclarations.Source; }" + ComponentsTestDeclarations.Source;
VerifyCSharpDiagnostic(test, VerifyCSharpDiagnostic(test,
new DiagnosticResult new DiagnosticResult
{ {
Id = DiagnosticDescriptors.ComponentParametersShouldNotBePublic.Id, Id = DiagnosticDescriptors.ComponentParametersShouldBePublic.Id,
Message = "Component parameter 'ConsoleApplication1.TypeName.BadProperty1' has a public setter, but component parameters should not be publicly settable.", Message = "Component parameter 'ConsoleApplication1.TypeName.BadProperty1' should be public.",
Severity = DiagnosticSeverity.Warning, Severity = DiagnosticSeverity.Warning,
Locations = new[] Locations = new[]
{ {
new DiagnosticResultLocation("Test0.cs", 8, 39) new DiagnosticResultLocation("Test0.cs", 8, 40)
} }
}, },
new DiagnosticResult new DiagnosticResult
{ {
Id = DiagnosticDescriptors.ComponentParametersShouldNotBePublic.Id, Id = DiagnosticDescriptors.ComponentParametersShouldBePublic.Id,
Message = "Component parameter 'ConsoleApplication1.TypeName.BadProperty2' has a public setter, but component parameters should not be publicly settable.", Message = "Component parameter 'ConsoleApplication1.TypeName.BadProperty2' should be public.",
Severity = DiagnosticSeverity.Warning, Severity = DiagnosticSeverity.Warning,
Locations = new[] Locations = new[]
{ {
new DiagnosticResultLocation("Test0.cs", 9, 48) new DiagnosticResultLocation("Test0.cs", 9, 49)
} }
}); });
@ -90,8 +70,8 @@ namespace Microsoft.AspNetCore.Components.Analyzers.Test
class TypeName class TypeName
{ {
[Parameter] string BadProperty1 { get; set; } [Parameter] public string BadProperty1 { get; set; }
[CascadingParameter] object BadProperty2 { get; set; } [CascadingParameter] public object BadProperty2 { get; set; }
} }
}" + ComponentsTestDeclarations.Source); }" + ComponentsTestDeclarations.Source);
} }
@ -108,16 +88,46 @@ namespace Microsoft.AspNetCore.Components.Analyzers.Test
{ {
[Parameter] public string MyProperty1 { get; private set; } [Parameter] public string MyProperty1 { get; private set; }
[Parameter] public object MyProperty2 { get; protected set; } [Parameter] public object MyProperty2 { get; protected set; }
[Parameter] public object MyProperty2 { get; internal set; } [Parameter] public object MyProperty3 { get; internal set; }
} }
}" + ComponentsTestDeclarations.Source; }" + ComponentsTestDeclarations.Source;
VerifyCSharpDiagnostic(test); VerifyCSharpDiagnostic(test,
new DiagnosticResult
{
Id = DiagnosticDescriptors.ComponentParameterSettersShouldBePublic.Id,
Message = "Component parameter 'ConsoleApplication1.TypeName.MyProperty1' should have a public setter.",
Severity = DiagnosticSeverity.Warning,
Locations = new[]
{
new DiagnosticResultLocation("Test0.cs", 8, 39)
}
},
new DiagnosticResult
{
Id = DiagnosticDescriptors.ComponentParameterSettersShouldBePublic.Id,
Message = "Component parameter 'ConsoleApplication1.TypeName.MyProperty2' should have a public setter.",
Severity = DiagnosticSeverity.Warning,
Locations = new[]
{
new DiagnosticResultLocation("Test0.cs", 9, 39)
}
},
new DiagnosticResult
{
Id = DiagnosticDescriptors.ComponentParameterSettersShouldBePublic.Id,
Message = "Component parameter 'ConsoleApplication1.TypeName.MyProperty3' should have a public setter.",
Severity = DiagnosticSeverity.Warning,
Locations = new[]
{
new DiagnosticResultLocation("Test0.cs", 10, 39)
}
});
} }
protected override CodeFixProvider GetCSharpCodeFixProvider() protected override CodeFixProvider GetCSharpCodeFixProvider()
{ {
return new ComponentParametersShouldNotBePublicCodeFixProvider(); return new ComponentParametersShouldBePublicCodeFixProvider();
} }
protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()

View File

@ -0,0 +1,106 @@
// 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.Diagnostics;
using TestHelper;
using Xunit;
namespace Microsoft.AspNetCore.Components.Analyzers
{
public class ComponentParametersShouldBePublicTest : DiagnosticVerifier
{
[Fact]
public void IgnoresPublicProperties()
{
var test = $@"
namespace ConsoleApplication1
{{
using {typeof(ParameterAttribute).Namespace};
class TypeName
{{
[Parameter] public string MyProperty {{ get; set; }}
}}
}}" + ComponentsTestDeclarations.Source;
VerifyCSharpDiagnostic(test);
}
[Fact]
public void IgnoresPrivateNonParameterProperties()
{
var test = $@"
namespace ConsoleApplication1
{{
using {typeof(ParameterAttribute).Namespace};
class TypeName
{{
private string MyProperty {{ get; set; }}
}}
}}" + ComponentsTestDeclarations.Source;
VerifyCSharpDiagnostic(test);
}
[Fact]
public void ErrorsForNonPublicParameters()
{
var test = $@"
namespace ConsoleApplication1
{{
using {typeof(ParameterAttribute).Namespace};
class TypeName
{{
[Parameter] string MyProperty1 {{ get; set; }}
[Parameter] private string MyProperty2 {{ get; set; }}
[Parameter] protected string MyProperty3 {{ get; set; }}
[Parameter] internal string MyProperty4 {{ get; set; }}
}}
}}" + ComponentsTestDeclarations.Source;
VerifyCSharpDiagnostic(test,
new DiagnosticResult
{
Id = DiagnosticDescriptors.ComponentParametersShouldBePublic.Id,
Message = "Component parameter 'ConsoleApplication1.TypeName.MyProperty1' should be public.",
Severity = DiagnosticSeverity.Warning,
Locations = new[]
{
new DiagnosticResultLocation("Test0.cs", 7, 32)
}
},
new DiagnosticResult
{
Id = DiagnosticDescriptors.ComponentParametersShouldBePublic.Id,
Message = "Component parameter 'ConsoleApplication1.TypeName.MyProperty2' should be public.",
Severity = DiagnosticSeverity.Warning,
Locations = new[]
{
new DiagnosticResultLocation("Test0.cs", 8, 40)
}
},
new DiagnosticResult
{
Id = DiagnosticDescriptors.ComponentParametersShouldBePublic.Id,
Message = "Component parameter 'ConsoleApplication1.TypeName.MyProperty3' should be public.",
Severity = DiagnosticSeverity.Warning,
Locations = new[]
{
new DiagnosticResultLocation("Test0.cs", 9, 42)
}
},
new DiagnosticResult
{
Id = DiagnosticDescriptors.ComponentParametersShouldBePublic.Id,
Message = "Component parameter 'ConsoleApplication1.TypeName.MyProperty4' should be public.",
Severity = DiagnosticSeverity.Warning,
Locations = new[]
{
new DiagnosticResultLocation("Test0.cs", 10, 41)
}
});
}
protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() => new ComponentParameterAnalyzer();
}
}