-
Couldn't load subscription status.
- Fork 2.1k
Create an Analyzer to warn customers to not user Required attribute opn top-level parameters #7415 #8290
Create an Analyzer to warn customers to not user Required attribute opn top-level parameters #7415 #8290
Changes from all commits
5899cab ad2bcd7 8730e6c File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| | @@ -52,5 +52,14 @@ public static class DiagnosticDescriptors | |
| DiagnosticSeverity.Warning, | ||
| isEnabledByDefault: true, | ||
| helpLinkUri: "https://aka.ms/AA20pbc"); | ||
| | ||
| public static readonly DiagnosticDescriptor MVC1005_AttributeAvoidUsingRequiredAndBindRequired = | ||
| new DiagnosticDescriptor( | ||
| "MVC1005", | ||
| "Use of '{0}' should be avoided on collections.", | ||
| "Use of '{0}' should be avoided. This always made model validation is true. Consider using '{1}'.", | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "made" -> "makes" | ||
| "Usage", | ||
| DiagnosticSeverity.Warning, | ||
| isEnabledByDefault: true); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Collections.Immutable; | ||
| using System.Text; | ||
| using Microsoft.CodeAnalysis; | ||
| using Microsoft.CodeAnalysis.CSharp; | ||
| using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
| using Microsoft.CodeAnalysis.Diagnostics; | ||
| | ||
| namespace Microsoft.AspNetCore.Mvc.Analyzers | ||
| { | ||
| [DiagnosticAnalyzer(LanguageNames.CSharp)] | ||
| public class ModelPropertyRequiredAttribueBindRequiredAttributeAnalyzer : DiagnosticAnalyzer | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggest creating a small base or helpers class that inspects an individual top-level parameter or property symbol. This would reduce the duplication between As a smaller detail, | ||
| { | ||
| public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create( | ||
| DiagnosticDescriptors.MVC1005_AttributeAvoidUsingRequiredAndBindRequired); | ||
| | ||
| public override void Initialize(AnalysisContext context) | ||
| { | ||
| context.EnableConcurrentExecution(); | ||
| context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); | ||
| | ||
| context.RegisterCompilationStartAction(compilationStartAnalysisContext => | ||
| { | ||
| InitializeWorker(compilationStartAnalysisContext); | ||
| }); | ||
| } | ||
| | ||
| private void InitializeWorker(CompilationStartAnalysisContext compilationStartAnalysisContext) | ||
| { | ||
| compilationStartAnalysisContext.RegisterSymbolAction(symbolAnalysisContext => | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While #7415 mentions users continue to confuse | ||
| { | ||
| var property = (IPropertySymbol)symbolAnalysisContext.Symbol; | ||
| | ||
| if (property.DeclaredAccessibility != Accessibility.Public || | ||
| property.IsStatic ) | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Odd indentation here and in a few other places. Please avoid over-indentation because it is just extra work to maintain. (In this particular case, the whole condition fits easily on one line.) | ||
| { | ||
| return; | ||
| } | ||
| | ||
| if (property.GetAttributes().Length == 0) | ||
| { | ||
| return; | ||
| } | ||
| | ||
| var typeCache = new SymbolCache(symbolAnalysisContext.Compilation); | ||
| var location = property.Locations.Length != 0 ? | ||
| property.Locations[0] : | ||
| Location.None; | ||
| | ||
| if (IsNonNullableValueType(property.Type, typeCache.SystemNullableType) && property.HasAttribute(typeCache.RequiredAttribute, inherit: false)) | ||
| { | ||
| { | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this statement wrapped in an additional block? | ||
| symbolAnalysisContext.ReportDiagnostic( | ||
| Diagnostic.Create( | ||
| DiagnosticDescriptors.MVC1005_AttributeAvoidUsingRequiredAndBindRequired, | ||
| location, | ||
| "RequiredAttribute", "BindRequiredAttribute")); | ||
| } | ||
| } | ||
| else if (IsComplexType(property.Type) && property.HasAttribute(typeCache.BindRequiredAttribute, inherit: false)) | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should handle | ||
| { | ||
| symbolAnalysisContext.ReportDiagnostic( | ||
| Diagnostic.Create( | ||
| DiagnosticDescriptors.MVC1005_AttributeAvoidUsingRequiredAndBindRequired, | ||
| location, | ||
| "BindRequiredAttribute", "RequiredAttribute")); | ||
| } | ||
| | ||
| | ||
| }, SymbolKind.Property); | ||
| } | ||
| | ||
| public static bool IsComplexType(ITypeSymbol type) => type.SpecialType == SpecialType.None; | ||
| | ||
| public static bool IsNonNullableValueType(ITypeSymbol type, INamedTypeSymbol systemNullableType) | ||
| { | ||
| return !type.IsReferenceType && !(type.OriginalDefinition.Equals(systemNullableType)); | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Banish the excess parentheses: … | ||
| } | ||
| | ||
| internal readonly struct SymbolCache | ||
| { | ||
| public SymbolCache(Compilation compilation) | ||
| { | ||
| RequiredAttribute = compilation.GetTypeByMetadataName(SymbolNames.RequiredAttribute); | ||
| BindRequiredAttribute = compilation.GetTypeByMetadataName(SymbolNames.BindRequiredAttribute); | ||
| SystemNullableType = compilation.GetSpecialType(SpecialType.System_Nullable_T); | ||
| } | ||
| | ||
| public ITypeSymbol BindRequiredAttribute { get; } | ||
| public INamedTypeSymbol SystemNullableType { get; } | ||
| public INamedTypeSymbol RequiredAttribute { get; } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -0,0 +1,126 @@ | ||||
| using System; | ||||
| using System.Collections.Immutable; | ||||
| using System.Linq; | ||||
| using Microsoft.CodeAnalysis; | ||||
| using Microsoft.CodeAnalysis.Diagnostics; | ||||
| | ||||
| namespace Microsoft.AspNetCore.Mvc.Analyzers | ||||
| { | ||||
| [DiagnosticAnalyzer(LanguageNames.CSharp)] | ||||
| public class TopLevelParameterRequiredAttributeAnalyzer : DiagnosticAnalyzer | ||||
| { | ||||
| public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create( | ||||
| DiagnosticDescriptors.MVC1005_AttributeAvoidUsingRequiredAndBindRequired); | ||||
| | ||||
| public override void Initialize(AnalysisContext context) | ||||
| { | ||||
| context.EnableConcurrentExecution(); | ||||
| context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); | ||||
| | ||||
| context.RegisterCompilationStartAction(compilationStartAnalysisContext => | ||||
| { | ||||
| var typeCache = new SymbolCache(compilationStartAnalysisContext.Compilation); | ||||
| if (typeCache.ControllerAttribute == null || typeCache.ControllerAttribute.TypeKind == TypeKind.Error) | ||||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I may be missing something. Why doesn't this case exist in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here we're checking type is the Controller, in my understanding whether it is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
As far as I know, this checks the
No, #7415 is specifically about top-level parameters and properties. Specifically
Again, ignore my optional "might be worthwhile warning" point in #7415 at least until the core work is done. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. then it should has this there as well. i will add. | ||||
| { | ||||
| // No-op if we can't find types we care about. | ||||
| return; | ||||
| } | ||||
| | ||||
| InitializeWorker(compilationStartAnalysisContext, typeCache); | ||||
| }); | ||||
| } | ||||
| | ||||
| private void InitializeWorker(CompilationStartAnalysisContext compilationStartAnalysisContext, SymbolCache symbolCache) | ||||
| { | ||||
| compilationStartAnalysisContext.RegisterSymbolAction(symbolAnalysisContext => | ||||
| { | ||||
| | ||||
| var method = (IMethodSymbol)symbolAnalysisContext.Symbol; | ||||
| if (method.MethodKind != MethodKind.Ordinary) | ||||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is public IActionResult<int> First([Required] int[] numbers) => numbers.FirstOrDefault();There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. add this case as well in the next commit, with current implementation this test case got success. I didn't find Please correct If am wrong, up to my understanding following both are same .
and
And both are There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes, that was a typo.
Please test this assumption. I'm not sure. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sure | ||||
| { | ||||
| return; | ||||
| } | ||||
| | ||||
| if (method.Parameters.Length == 0) | ||||
| { | ||||
| return; | ||||
| } | ||||
| | ||||
| if (!MvcFacts.IsController(method.ContainingType, symbolCache.ControllerAttribute, symbolCache.NonControllerAttribute) || | ||||
| !MvcFacts.IsControllerAction(method, symbolCache.NonActionAttribute, symbolCache.IDisposableDispose)) | ||||
| { | ||||
| return; | ||||
| } | ||||
| | ||||
| for (var i = 0; i < method.Parameters.Length; i++) | ||||
| { | ||||
| var parameter = method.Parameters[i]; | ||||
| | ||||
| if (IsEnumerable(parameter.Type, symbolCache.IEnumerableInterface)) | ||||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need to warn about Separately, MVC treats all types with conversions from
| ||||
| { | ||||
| if (IsProblematicParameter(parameter, symbolCache.RequiredAttribute)) | ||||
| { | ||||
| var location = parameter.Locations.Length != 0 ? | ||||
| parameter.Locations[0] : | ||||
| Location.None; | ||||
| | ||||
| symbolAnalysisContext.ReportDiagnostic( | ||||
| Diagnostic.Create( | ||||
| DiagnosticDescriptors.MVC1005_AttributeAvoidUsingRequiredAndBindRequired, | ||||
| location, | ||||
| "RequiredAttribute","MinLength(1)")); | ||||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
| ||||
| } | ||||
| } | ||||
| } | ||||
| | ||||
| | ||||
| }, SymbolKind.Method); | ||||
| } | ||||
| | ||||
| public static bool IsEnumerable(ITypeSymbol source, INamedTypeSymbol iEnumerableInterface) | ||||
| { | ||||
| if (source.Kind == SymbolKind.ArrayType) | ||||
| { | ||||
| return true; | ||||
| } | ||||
| foreach (var @interface in source.AllInterfaces) | ||||
| { | ||||
| if (@interface == iEnumerableInterface) | ||||
| { | ||||
| return true; | ||||
| } | ||||
| } | ||||
| | ||||
| return false; | ||||
| } | ||||
| | ||||
| private bool IsProblematicParameter(IParameterSymbol parameter, INamedTypeSymbol requiredAttribute) | ||||
| { | ||||
| return parameter.GetAttributes(requiredAttribute).Any(); | ||||
| } | ||||
| | ||||
| internal readonly struct SymbolCache | ||||
| { | ||||
| public SymbolCache(Compilation compilation) | ||||
| { | ||||
| ControllerAttribute = compilation.GetTypeByMetadataName(SymbolNames.ControllerAttribute); | ||||
| NonControllerAttribute = compilation.GetTypeByMetadataName(SymbolNames.NonControllerAttribute); | ||||
| NonActionAttribute = compilation.GetTypeByMetadataName(SymbolNames.NonActionAttribute); | ||||
| | ||||
| IEnumerableInterface = compilation.GetSpecialType(SpecialType.System_Collections_IEnumerable); | ||||
| RequiredAttribute = compilation.GetTypeByMetadataName(SymbolNames.RequiredAttribute); | ||||
| | ||||
| var disposable = compilation.GetSpecialType(SpecialType.System_IDisposable); | ||||
| var members = disposable.GetMembers(nameof(IDisposable.Dispose)); | ||||
| IDisposableDispose = members.Length == 1 ? (IMethodSymbol)members[0] : null; | ||||
| } | ||||
| | ||||
| public INamedTypeSymbol ControllerAttribute { get; } | ||||
| public INamedTypeSymbol NonControllerAttribute { get; } | ||||
| public INamedTypeSymbol NonActionAttribute { get; } | ||||
| public INamedTypeSymbol IEnumerableInterface { get; } | ||||
| public INamedTypeSymbol RequiredAttribute { get; } | ||||
| public IMethodSymbol IDisposableDispose { get; } | ||||
| } | ||||
| } | ||||
| } | ||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| using System.Runtime.CompilerServices; | ||
| using System.Threading.Tasks; | ||
| using Microsoft.AspNetCore.Analyzer.Testing; | ||
| using Xunit; | ||
| | ||
| namespace Microsoft.AspNetCore.Mvc.Analyzers | ||
| { | ||
| public class ModelPropertyRequiredAttribueBindRequiredAttributeAnalyzerTest | ||
| { | ||
| private MvcDiagnosticAnalyzerRunner Runner { get; } = new MvcDiagnosticAnalyzerRunner(new ModelPropertyRequiredAttribueBindRequiredAttributeAnalyzer()); | ||
| | ||
| [Fact] | ||
| public Task DiagnosticsAreReturned_ForNonNullableTypeProperty() | ||
| => RunTest(new[] { "RequiredAttribute", "BindRequiredAttribute" }); | ||
| | ||
| [Fact] | ||
| public Task DiagnosticsAreReturned_ForComplexTypeProperty() | ||
| => RunTest(new[] {"BindRequiredAttribute", "RequiredAttribute" }); | ||
| | ||
| private async Task RunTest(string[] attributes,[CallerMemberName] string testMethod = "") | ||
| { | ||
| // Arrange | ||
| var descriptor = DiagnosticDescriptors.MVC1005_AttributeAvoidUsingRequiredAndBindRequired; | ||
| var testSource = MvcTestSource.Read(GetType().Name, testMethod); | ||
| var expectedLocation = testSource.DefaultMarkerLocation; | ||
| | ||
| // Act | ||
| var result = await Runner.GetDiagnosticsAsync(testSource.Source); | ||
| | ||
| // Assert | ||
| Assert.Collection( | ||
| result, | ||
| diagnostic => | ||
| { | ||
| Assert.Equal(descriptor.Id, diagnostic.Id); | ||
| Assert.Same(descriptor, diagnostic.Descriptor); | ||
| AnalyzerAssert.DiagnosticLocation(expectedLocation, diagnostic.Location); | ||
| Assert.Equal(string.Format(descriptor.MessageFormat.ToString(), attributes[0], attributes[1]), diagnostic.GetMessage()); | ||
| }); | ||
| } | ||
| | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Text; | ||
| using Microsoft.AspNetCore.Mvc.ModelBinding; | ||
| | ||
| namespace Microsoft.AspNetCore.Mvc.Analyzers.TestFiles.ModelPropertyRequiredAttribueBindRequiredAttributeAnalyzerTest | ||
| { | ||
| public class DiagnosticsAreReturned_ForComplexTypeProperty | ||
| { | ||
| [BindRequired] | ||
| public DiagnosticsAreReturned_ForComplexTypeProperty /*MM*/MyProperty { get; set; } | ||
| | ||
| [BindRequired] | ||
| public string name { get; set; } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.ComponentModel.DataAnnotations; | ||
| using System.Text; | ||
| | ||
| namespace Microsoft.AspNetCore.Mvc.Analyzers.TestFiles.ModelPropertyRequiredAttribueBindRequiredAttributeAnalyzerTest | ||
| { | ||
| public class DiagnosticsAreReturned_ForNonNullableTypeProperty | ||
| { | ||
| [Required] | ||
| public int /*MM*/id { get; set; } | ||
| | ||
| protected static int static_x { get; set; } | ||
| | ||
| protected int x { get; set; } | ||
| | ||
| [Required] | ||
| public int? y { get; set; } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| namespace Microsoft.AspNetCore.Mvc.Analyzers.TestFiles.TopLevelParameterAttributeAnalyzerTest | ||
| { | ||
| [Controller] | ||
| public class DiagnosticsAreReturned_ForControllerActionsWithParametersWithOutRequiredAttribute | ||
| { | ||
| public IActionResult Index(int[] collection) => null; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| using System.ComponentModel.DataAnnotations; | ||
| | ||
| namespace Microsoft.AspNetCore.Mvc.Analyzers.TestFiles.TopLevelParameterAttributeAnalyzerTest | ||
| { | ||
| public class DiagnosticsAreReturned_ForControllerActionsWithParametersWithRequiredAttributeForArray: Controller | ||
| { | ||
| public IActionResult Index([Required]int[] /*MM*/collection) => null; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.ComponentModel.DataAnnotations; | ||
| using System.Text; | ||
| | ||
| namespace Microsoft.AspNetCore.Mvc.Analyzers.TestFiles.TopLevelParameterAttributeAnalyzerTest | ||
| { | ||
| public class DiagnosticsAreReturned_ForControllerActionsWithParametersWithRequiredAttributeForIEnumerable : Controller | ||
| { | ||
| public IActionResult Index([Required]IEnumerable<int> /*MM*/collection) => null; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| using System.ComponentModel.DataAnnotations; | ||
| | ||
| namespace Microsoft.AspNetCore.Mvc.Analyzers.TestFiles.TopLevelParameterAttributeAnalyzerTest | ||
| { | ||
| public class RunNoDiagnosticsAreReturnedForNonCollectionsParameterWithRequired : Controller | ||
| { | ||
| public IActionResult Index([Required] int id) => null; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this file changing at all?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we keep this method here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What method? I see only two new blank lines in this file.