Skip to content
This repository was archived by the owner on Dec 14, 2018. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ public static bool IsAssignableFrom(this ITypeSymbol source, ITypeSymbol target)
return false;
}


Copy link
Contributor

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?

Copy link
Contributor Author

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?

Copy link
Contributor

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.

private static bool HasAttribute(this ISymbol symbol, ITypeSymbol attribute)
{
foreach (var declaredAttribute in symbol.GetAttributes())
Expand All @@ -145,5 +146,6 @@ private static IEnumerable<ITypeSymbol> GetTypeHierarchy(this ITypeSymbol typeSy
typeSymbol = typeSymbol.BaseType;
}
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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}'.",
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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 TopLevelParameterRequiredAttributeAnalyzer and TopLevelPropertyRequiredAttributeAnalyzer. (Yes, I suggest renaming this analyzer and narrowing its behaviour to match TopLevelParameterRequiredAttributeAnalyzer.)

As a smaller detail, Attribue -> Attribute.

{
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 =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While #7415 mentions users continue to confuse [BindRequired] and [Required] on int and other value types, let's focus on top-level properties (and parameters) in this PR.

{
var property = (IPropertySymbol)symbolAnalysisContext.Symbol;

if (property.DeclaredAccessibility != Accessibility.Public ||
property.IsStatic )
Copy link
Contributor

Choose a reason for hiding this comment

The 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))
{
{
Copy link
Contributor

Choose a reason for hiding this comment

The 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))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should handle SpecialType.System_Collections_Generic_ICollection_T through SpecialType.System_Collections_IEnumerable and SpecialType.System_Array separately for top-level properties. Of course, it's better to inspect the implemented interfaces as you've done in TopLevelParameterRequiredAttributeAnalyzer.

{
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));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Banish the excess parentheses: … !type.OriginalDefinition.Equals(systemNullableType);.

}

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; }
}
}
}
4 changes: 4 additions & 0 deletions src/Microsoft.AspNetCore.Mvc.Analyzers/SymbolNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,9 @@ internal static class SymbolNames
public const string ProducesResponseTypeAttribute = "Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute";

public const string RenderPartialMethod = "RenderPartial";

public const string RequiredAttribute = "System.ComponentModel.DataAnnotations.RequiredAttribute";
public const string BindRequiredAttribute = "Microsoft.AspNetCore.Mvc.ModelBinding.BindRequiredAttribute";
public const string NullableType = "System.Nullable<T>";
}
}
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may be missing something. Why doesn't this case exist in ModelPropertyRequiredAttribueBindRequiredAttributeAnalyzer.Initialize?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 controller or Model on properties we should advice.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we're checking type is the Controller

As far as I know, this checks the SymbolCache, not the specific symbol. My question was why this check doesn't exist in ModelPropertyRequiredAttribueBindRequiredAttributeAnalyzer because that also has a SymbolCache.

whether it is controller or Model on properties we should advice.

No, #7415 is specifically about top-level parameters and properties. Specifically

  • model-bound properties in a controller
  • parameters to an action
  • model-bound properties in a page model
  • parameters to a page handler

Again, ignore my optional "might be worthwhile warning" point in #7415 at least until the core work is done.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is MethodKind.LambdaMethod unsupported? Customers often write actions as lambda methods e.g.

public IActionResult<int> First([Required] int[] numbers) => numbers.FirstOrDefault();
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 IActionResult<int>, got ActionResult<int>.

Please correct If am wrong, up to my understanding following both are same .

public ActionResult<int> First([Required] int[] numbers) => numbers.FirstOrDefault();

and

public ActionResult<int> First([Required] int[] numbers) { return numbers.FirstOrDefault(); }

And both are MethodKind.Ordinary

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't find IActionResult<int>, got ActionResult<int>

Yes, that was a typo.

both are MethodKind.Ordinary

Please test this assumption. I'm not sure.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to warn about [Required] on non-Nullable<T> value types and on complex types too.

Separately, MVC treats all types with conversions from string as "simple" types. Treating all classes as "complex" may result in false warnings. And, user-defined value types will not necessarily have conversions from string -- leading to missed warnings. @pranavkm does Roslyn support anything similar to the following (in ModelMetadata?

IsComplexType = !TypeDescriptor.GetConverter(ModelType).CanConvertFrom(typeof(string));

{
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)"));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MinLength(1)will only work in a subset of cases where a problem is detected i.e. arrays in the .NET Framework case and ICollection implementations in .NET Core. Should avoid giving incorrect advice otherwise.

}
}
}


}, 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;
}
}
Loading