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 2 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
19 changes: 19 additions & 0 deletions src/Microsoft.AspNetCore.Mvc.Analyzers/CodeAnalysisExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,24 @@ public static bool IsAssignableFrom(this ITypeSymbol source, ITypeSymbol target)
return false;
}

public static bool IsCollection(this ITypeSymbol source)
Copy link
Contributor

Choose a reason for hiding this comment

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

IsEnumerable would be a better name. May also need IsCollection and IsArray to distinguish the cases where current MVC1005_ParameterAttributeAvoidUsingRequiredOncollections advice is actionable under .NET Core and .NET Framework (respectively).

Copy link
Contributor

Choose a reason for hiding this comment

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

Could we move this method to the analyzer? The methods on this type are meant to be more general purpose.

{
if (source.Kind == SymbolKind.ArrayType)
{
return true;
}

foreach (var @interface in source.AllInterfaces)
{
if (@interface.MetadataName == "IEnumerable")
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd recommend checking the actual type @interface == symbolCache.IEnumerable. You can get to IEnumerable using
Compilation.GetSpecialType(SpecialType.System_Collections_IEnumerable)

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 true;
}
}

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 +163,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_ParameterAttributeAvoidUsingRequiredOncollections =
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 [MinLength(1)].",
"Usage",
DiagnosticSeverity.Warning,
isEnabledByDefault: true);
}
}
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 = "RequiredAttribute";
Copy link
Contributor

Choose a reason for hiding this comment

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

Fully qualify this and use that to look up the type.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will change


public const string CollectionType= "SourceComplexParameterSymbol";
Copy link
Contributor

Choose a reason for hiding this comment

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

Doesn't this symbol both exclude SourceComplexParameterSymbolWithCustomModifiers and indicate only that the parameter is not simple e.g. that it has associated attributes? That is, what about this type indicates a collection?

@pranavkm

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think you're using this anymore.

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
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 TopLevelParameterAttributeAnalyzer : DiagnosticAnalyzer
Copy link
Contributor

Choose a reason for hiding this comment

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

Are you planning a separate analyzer or analyzer pair for [Required] on complex types? #7415 covers that case too.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes I'm planning seperate analyzer for that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We'll writer analyzer as well in this issue. If it require please update issue.

Copy link
Contributor

Choose a reason for hiding this comment

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

@kishanAnem #7415 is already clear complex types must be covered. See https://github.com/aspnet/Mvc/issues/7415#issuecomment-369413264

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, i mean codefixer.

Copy link
Contributor

Choose a reason for hiding this comment

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

Not doing fixers is fine for this PR. As I said earlier,

Initial priority is covering #7415 in full, especially covering all top-level models and providing correct advice. Well, the "might be worthwhile" point is optional.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK, ☺️

Copy link
Contributor

Choose a reason for hiding this comment

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

Could we call this RequiredAttributeAnalyzer? There's already an analyzer by the same name in the Api.Analyzers package

{
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(
DiagnosticDescriptors.MVC1005_ParameterAttributeAvoidUsingRequiredOncollections);

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)
{
// 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)
{
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 (parameter.Type.IsCollection())
{
if (IsProblematicParameter(parameter))
{
var location = parameter.Locations.Length != 0 ?
parameter.Locations[0] :
Location.None;
var descriptor = DiagnosticDescriptors.MVC1005_ParameterAttributeAvoidUsingRequiredOncollections;
Copy link
Contributor

Choose a reason for hiding this comment

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

unused?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. I will remove that.


symbolAnalysisContext.ReportDiagnostic(
Diagnostic.Create(
DiagnosticDescriptors.MVC1005_ParameterAttributeAvoidUsingRequiredOncollections,
location,
SymbolNames.RequiredAttribute));
}
}
}


}, SymbolKind.Method);
}

private bool IsProblematicParameter(IParameterSymbol parameter)
{
return parameter.GetAttributes().Any(attribute => Equals(attribute.AttributeClass.MetadataName, SymbolNames.RequiredAttribute));
Copy link
Contributor

Choose a reason for hiding this comment

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

parameter.GetAttributes(symbolCache.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);

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 IMethodSymbol IDisposableDispose { get; }
}
}
}
5 changes: 5 additions & 0 deletions test/Mvc.Analyzers.Test/Mvc.Analyzers.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
</PropertyGroup>

<ItemGroup>
<Compile Remove="TestFiles\TopLevelParameterAttributeAnalyzerTest\DiagnosticsAreReturned_ForControllerActionsWithParametersWithOutRequiredAttributeForIEnumerable.cs" />
Copy link
Contributor

Choose a reason for hiding this comment

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

Undo changes to the file

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

</ItemGroup>

<ItemGroup>
<None Include="TestFiles\TopLevelParameterAttributeAnalyzerTest\DiagnosticsAreReturned_ForControllerActionsWithParametersWithOutRequiredAttributeForIEnumerable.cs" />
<None Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

Expand Down
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,11 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace Microsoft.AspNetCore.Mvc.Analyzers.TestFiles.TopLevelParameterAttributeAnalyzerTest
{
public class DiagnosticsAreReturned_ForControllerActionsWithParametersWithOutRequiredAttributeForIEnumerable : Controller
{
public IActionResult Index(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 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;
Copy link
Contributor

Choose a reason for hiding this comment

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

IEnumerable<T> does not inherit from ICollection. So, I suspect the analyzer would also complain about a [Required] int nonCollection parameter.

Copy link
Contributor Author

@kishanAnem kishanAnem Aug 19, 2018

Choose a reason for hiding this comment

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

So, I suspect the analyzer would also complain about a [Required] int nonCollection parameter.

Yes, you're right @dougbu will fix this.

}
}
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;
}
}
76 changes: 76 additions & 0 deletions test/Mvc.Analyzers.Test/TopLevelParameterAttributeAnalyzerTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Analyzer.Testing;
using Xunit;

namespace Microsoft.AspNetCore.Mvc.Analyzers
{
public class TopLevelParameterAttributeAnalyzerTest
{
private MvcDiagnosticAnalyzerRunner Runner { get; } = new MvcDiagnosticAnalyzerRunner(new TopLevelParameterAttributeAnalyzer());

[Fact]
public Task DiagnosticsAreReturned_ForControllerActionsWithParametersWithRequiredAttributeForArray()
=> RunTest();

[Fact]
public Task DiagnosticsAreReturned_ForControllerActionsWithParametersWithRequiredAttributeForIEnumerable()
=> RunTest();

[Fact]
public Task DiagnosticsAreReturned_ForControllerActionsWithParametersWithOutRequiredAttribute()
=> RunNoDiagnosticsAreReturned(nameof(TopLevelParameterAttributeAnalyzerTest));


[Fact]
public Task NoDiagnosticsAreReturnedIfParameterIsRenamedUsingBindingAttribute()
=> RunNoDiagnosticsAreReturned(nameof(TopLevelParameterNameAnalyzerTest));

[Fact]
public Task NoDiagnosticsAreReturnedForNonActions()
=> RunNoDiagnosticsAreReturned(nameof(TopLevelParameterNameAnalyzerTest));

[Fact]
public Task RunNoDiagnosticsAreReturnedForNonCollectionsParameterWithRequired()
=> RunNoDiagnosticsAreReturned(nameof(TopLevelParameterAttributeAnalyzerTest));

[Fact]
public Task DiagnosticsAreReturned_ForControllerActionsWithParametersWithOutRequiredAttributeForIEnumerable()
=> RunNoDiagnosticsAreReturned(nameof(TopLevelParameterAttributeAnalyzerTest));

private async Task RunNoDiagnosticsAreReturned(string typename, [CallerMemberName] string testMethod = "")
{
// Arrange
var testSource = MvcTestSource.Read(typename, testMethod);
var expectedLocation = testSource.DefaultMarkerLocation;

// Act
var result = await Runner.GetDiagnosticsAsync(testSource.Source);

// Assert
Assert.Empty(result);
}

private async Task RunTest([CallerMemberName] string testMethod = "")
{
// Arrange
var descriptor = DiagnosticDescriptors.MVC1005_ParameterAttributeAvoidUsingRequiredOncollections;
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(), SymbolNames.RequiredAttribute), diagnostic.GetMessage());
});
}
}
}