PA0005: Check that classes marked with [SiteType] inherits SiteContent.
This commit is contained in:
Родитель
f4d3f2e337
Коммит
1aa677cd94
|
@ -3,3 +3,4 @@ Rule ID | Category | Severity | Notes
|
|||
--------|----------|----------|-------
|
||||
PA0003 | Usage | Error | PostTypeAttributeOnlyForClassesInheritingPostAnalyzer, [Documentation](PA0003/README.md)
|
||||
PA0004 | Usage | Error | PageTypeAttributeOnlyForClassesInheritingPageAnalyzer, [Documentation](PA0004/README.md)
|
||||
PA0005 | Usage | Error | SiteTypeAttributeOnlyForClassesInheritingSiteContentAnalyzer, [Documentation](PA0005/README.md)
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
# PA0005 - Classes marked with the SiteTypeAttribute should inherit SiteContent
|
||||
|
||||
The analyzer will check classes marked with the `Piranha.AttributeBuilder.SiteTypeAttribute` and mark the class as violating if it does not inherit `Piranha.Models.SiteContent<T>`.
|
||||
|
||||
| Severity | Category |
|
||||
|----------|----------|
|
||||
| Error | Usage |
|
||||
|
||||
## Available code fixes
|
||||
None currently.
|
|
@ -21,6 +21,7 @@ namespace Piranha.Analyzers
|
|||
{
|
||||
internal const string PiranhaAttributeBuilderPageTypeAttribute = "Piranha.AttributeBuilder.PageTypeAttribute";
|
||||
internal const string PiranhaAttributeBuilderPostTypeAttribute = "Piranha.AttributeBuilder.PostTypeAttribute";
|
||||
internal const string PiranhaAttributeBuilderSiteTypeAttribute = "Piranha.AttributeBuilder.SiteTypeAttribute";
|
||||
|
||||
internal const string PiranhaExtendFieldAttribute = "Piranha.Extend.FieldAttribute";
|
||||
internal const string PiranhaExtendRegionAttribute = "Piranha.Extend.RegionAttribute";
|
||||
|
@ -39,6 +40,7 @@ namespace Piranha.Analyzers
|
|||
|
||||
internal const string PiranhaModelsPage = "Piranha.Models.Page";
|
||||
internal const string PiranhaModelsPost = "Piranha.Models.Post";
|
||||
internal const string PiranhaModelsSiteContent = "Piranha.Models.SiteContent";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -167,5 +167,32 @@ namespace Piranha.Analyzers {
|
|||
return ResourceManager.GetString("PostTypeAttributeOnlyForClassesInheritingPostAnalyzerTitle", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to [SiteType] should only be applied to classes inheriting SiteContent..
|
||||
/// </summary>
|
||||
internal static string SiteTypeAttributeOnlyForClassesInheritingSiteAnalyzerDescription {
|
||||
get {
|
||||
return ResourceManager.GetString("SiteTypeAttributeOnlyForClassesInheritingSiteAnalyzerDescription", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to {0} does not extend SiteContent, but is marked with [SiteType].
|
||||
/// </summary>
|
||||
internal static string SiteTypeAttributeOnlyForClassesInheritingSiteAnalyzerMessageFormat {
|
||||
get {
|
||||
return ResourceManager.GetString("SiteTypeAttributeOnlyForClassesInheritingSiteAnalyzerMessageFormat", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to [SiteType] should only be applied to classes inheriting SiteContent..
|
||||
/// </summary>
|
||||
internal static string SiteTypeAttributeOnlyForClassesInheritingSiteAnalyzerTitle {
|
||||
get {
|
||||
return ResourceManager.GetString("SiteTypeAttributeOnlyForClassesInheritingSiteAnalyzerTitle", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -159,4 +159,13 @@
|
|||
<data name="PageTypeAttributeOnlyForClassesInheritingPageAnalyzerTitle" xml:space="preserve">
|
||||
<value>[PageType] should only be applied to classes inheriting Page.</value>
|
||||
</data>
|
||||
<data name="SiteTypeAttributeOnlyForClassesInheritingSiteAnalyzerDescription" xml:space="preserve">
|
||||
<value>[SiteType] should only be applied to classes inheriting SiteContent.</value>
|
||||
</data>
|
||||
<data name="SiteTypeAttributeOnlyForClassesInheritingSiteAnalyzerMessageFormat" xml:space="preserve">
|
||||
<value>{0} does not extend SiteContent, but is marked with [SiteType]</value>
|
||||
</data>
|
||||
<data name="SiteTypeAttributeOnlyForClassesInheritingSiteAnalyzerTitle" xml:space="preserve">
|
||||
<value>[SiteType] should only be applied to classes inheriting SiteContent.</value>
|
||||
</data>
|
||||
</root>
|
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Mikael Lindemann
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*
|
||||
* https://github.com/piranhacms/piranha.core.analyzers
|
||||
*
|
||||
*/
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
namespace Piranha.Analyzers
|
||||
{
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
public class SiteTypeAttributeOnlyForClassesInheritingSiteContentAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
public const string DiagnosticId = "PA0005";
|
||||
private const string Category = "Usage";
|
||||
private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.SiteTypeAttributeOnlyForClassesInheritingSiteAnalyzerTitle), Resources.ResourceManager, typeof(Resources));
|
||||
private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.SiteTypeAttributeOnlyForClassesInheritingSiteAnalyzerMessageFormat), Resources.ResourceManager, typeof(Resources));
|
||||
private static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.SiteTypeAttributeOnlyForClassesInheritingSiteAnalyzerDescription), Resources.ResourceManager, typeof(Resources));
|
||||
private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: Description);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.EnableConcurrentExecution();
|
||||
|
||||
context.RegisterSyntaxNodeAction(AnalyzeSyntaxNode, SyntaxKind.Attribute);
|
||||
}
|
||||
|
||||
private static void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
if (!(context.Node is AttributeSyntax attribute))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var regionAttributeType = context.Compilation.GetTypeByMetadataName(Constants.Types.PiranhaAttributeBuilderSiteTypeAttribute);
|
||||
|
||||
if (regionAttributeType == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var attributeType = context.SemanticModel.GetTypeInfo(attribute, context.CancellationToken);
|
||||
|
||||
if (!regionAttributeType.Equals(attributeType.ConvertedType, SymbolEqualityComparer.IncludeNullability))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(attribute.Parent is AttributeListSyntax attributeList))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(attributeList.Parent is ClassDeclarationSyntax @class))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var siteType = context.Compilation.GetTypeByMetadataName($"{Constants.Types.PiranhaModelsSiteContent}`1");
|
||||
|
||||
if (siteType == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var unboundSiteType = siteType.ConstructUnboundGenericType();
|
||||
|
||||
if (@class.BaseList != null && @class.BaseList.Types.Count != 0)
|
||||
{
|
||||
foreach (var type in @class.BaseList.Types)
|
||||
{
|
||||
if (!(context.SemanticModel.GetTypeInfo(type.Type, context.CancellationToken).ConvertedType is INamedTypeSymbol convertedType))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (InheritsSite(convertedType, siteType, unboundSiteType))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.ReportDiagnostic(Diagnostic.Create(Rule, @class.GetLocation(), @class.Identifier.ValueText));
|
||||
}
|
||||
|
||||
private static bool InheritsSite(INamedTypeSymbol type, INamedTypeSymbol siteType, INamedTypeSymbol unboundSiteType)
|
||||
{
|
||||
if (type == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (type.SpecialType == SpecialType.System_Object)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!type.IsGenericType || !unboundSiteType.Equals(type.ConstructUnboundGenericType(), SymbolEqualityComparer.IncludeNullability))
|
||||
{
|
||||
var baseType = type.BaseType;
|
||||
if (baseType != null && InheritsSite(baseType, siteType, unboundSiteType))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (type.TypeArguments.Length != 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var constructedType = siteType.Construct(type.TypeArguments.First());
|
||||
|
||||
if (constructedType.Equals(type, SymbolEqualityComparer.IncludeNullability))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,209 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Mikael Lindemann
|
||||
*
|
||||
* This software may be modified and distributed under the terms
|
||||
* of the MIT license. See the LICENSE file for details.
|
||||
*
|
||||
* https://github.com/piranhacms/piranha.core.analyzers
|
||||
*
|
||||
*/
|
||||
|
||||
using System;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CodeFixes;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using TestHelper;
|
||||
using Xunit;
|
||||
|
||||
namespace Piranha.Analyzers.Test
|
||||
{
|
||||
public class SiteTypeAttributeOnlyForClassesInheritingSiteContentAnalyzerTests : CodeFixVerifier
|
||||
{
|
||||
[Fact]
|
||||
public async Task ClassWithSiteTypeAttributeDirectlyInheritingSiteContent()
|
||||
{
|
||||
var test = string.Join(Environment.NewLine,
|
||||
"using Piranha.AttributeBuilder;",
|
||||
"using Piranha.Extend;",
|
||||
"using Piranha.Extend.Fields;",
|
||||
"using Piranha.Models;",
|
||||
"",
|
||||
"namespace ConsoleApplication1",
|
||||
"{",
|
||||
" [SiteType]",
|
||||
" class TypeName : SiteContent<TypeName>",
|
||||
" {",
|
||||
" [Region]",
|
||||
" public HeroRegion Hero { get; set; }",
|
||||
" }",
|
||||
" class HeroRegion",
|
||||
" {",
|
||||
" [Field]",
|
||||
" public AudioField Audio { get; set; }",
|
||||
" [Field]",
|
||||
" public AudioField Audio2 { get; set; }",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
await VerifyCSharpDiagnosticAsync(test);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClassWithSiteTypeAttributeIndirectlyInheritingSiteContent()
|
||||
{
|
||||
var test = string.Join(Environment.NewLine,
|
||||
"using Piranha.AttributeBuilder;",
|
||||
"using Piranha.Extend;",
|
||||
"using Piranha.Extend.Fields;",
|
||||
"using Piranha.Models;",
|
||||
"",
|
||||
"namespace ConsoleApplication1",
|
||||
"{",
|
||||
" [SiteType]",
|
||||
" class TypeName : MySiteContent<TypeName>",
|
||||
" {",
|
||||
" [Region]",
|
||||
" public HeroRegion Hero { get; set; }",
|
||||
" }",
|
||||
" class MySiteContent<T> : SiteContent<T>",
|
||||
" {}",
|
||||
" class HeroRegion",
|
||||
" {",
|
||||
" [Field]",
|
||||
" public AudioField Audio { get; set; }",
|
||||
" [Field]",
|
||||
" public AudioField Audio2 { get; set; }",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
await VerifyCSharpDiagnosticAsync(test);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClassWithSiteTypeAttributeIndirectlyInheritingSiteContent2()
|
||||
{
|
||||
var test = string.Join(Environment.NewLine,
|
||||
"using Piranha.AttributeBuilder;",
|
||||
"using Piranha.Extend;",
|
||||
"using Piranha.Extend.Fields;",
|
||||
"using Piranha.Models;",
|
||||
"",
|
||||
"namespace ConsoleApplication1",
|
||||
"{",
|
||||
" [SiteType]",
|
||||
" class TypeName : MySiteContent2",
|
||||
" {",
|
||||
" [Region]",
|
||||
" public HeroRegion Hero { get; set; }",
|
||||
" }",
|
||||
" abstract class MySiteContent : SiteContent<TypeName>",
|
||||
" {}",
|
||||
" abstract class MySiteContent2 : MySiteContent",
|
||||
" {}",
|
||||
" class HeroRegion",
|
||||
" {",
|
||||
" [Field]",
|
||||
" public AudioField Audio { get; set; }",
|
||||
" [Field]",
|
||||
" public AudioField Audio2 { get; set; }",
|
||||
" }",
|
||||
"}");
|
||||
|
||||
await VerifyCSharpDiagnosticAsync(test);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClassWithSiteTypeAttributeWithoutSiteContentAsBaseClass()
|
||||
{
|
||||
var test = string.Join(Environment.NewLine,
|
||||
"using Piranha.AttributeBuilder;",
|
||||
"using Piranha.Extend;",
|
||||
"using Piranha.Extend.Fields;",
|
||||
"using Piranha.Models;",
|
||||
"",
|
||||
"namespace ConsoleApplication1",
|
||||
"{",
|
||||
" [SiteType]",
|
||||
" class TypeName",
|
||||
" {",
|
||||
" [Region]",
|
||||
" public HeroRegion Hero { get; set; }",
|
||||
" }",
|
||||
" class HeroRegion",
|
||||
" {",
|
||||
" [Field]",
|
||||
" public AudioField Audio { get; set; }",
|
||||
" [Field]",
|
||||
" public AudioField Audio2 { get; set; }",
|
||||
" }",
|
||||
"}");
|
||||
var expected = new DiagnosticResult
|
||||
{
|
||||
Id = "PA0005",
|
||||
Message = "TypeName does not extend SiteContent, but is marked with [SiteType]",
|
||||
Severity = DiagnosticSeverity.Error,
|
||||
Locations = new[]
|
||||
{
|
||||
new DiagnosticResultLocation("Test0.cs", 8, 5)
|
||||
}
|
||||
};
|
||||
|
||||
await VerifyCSharpDiagnosticAsync(test, expected);
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Fact]
|
||||
public async Task ClassWithSiteTypeAttributeWithCustomSiteContentAsBaseClass()
|
||||
{
|
||||
var test = string.Join(Environment.NewLine,
|
||||
"using Piranha.AttributeBuilder;",
|
||||
"using Piranha.Extend;",
|
||||
"using Piranha.Extend.Fields;",
|
||||
"using Piranha.Models;",
|
||||
"",
|
||||
"namespace ConsoleApplication1",
|
||||
"{",
|
||||
" [SiteType]",
|
||||
" class TypeName : SiteContent<TypeName>",
|
||||
" {",
|
||||
" [Region]",
|
||||
" public HeroRegion Hero { get; set; }",
|
||||
" }",
|
||||
" class SiteContent<T> where T : SiteContent<T>",
|
||||
" {}",
|
||||
" class HeroRegion",
|
||||
" {",
|
||||
" [Field]",
|
||||
" public AudioField Audio { get; set; }",
|
||||
" [Field]",
|
||||
" public AudioField Audio2 { get; set; }",
|
||||
" }",
|
||||
"}");
|
||||
var expected = new DiagnosticResult
|
||||
{
|
||||
Id = "PA0005",
|
||||
Message = "TypeName does not extend SiteContent, but is marked with [SiteType]",
|
||||
Severity = DiagnosticSeverity.Error,
|
||||
Locations = new[]
|
||||
{
|
||||
new DiagnosticResultLocation("Test0.cs", 8, 5)
|
||||
}
|
||||
};
|
||||
|
||||
await VerifyCSharpDiagnosticAsync(test, expected);
|
||||
}
|
||||
|
||||
|
||||
protected override CodeFixProvider GetCSharpCodeFixProvider()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
|
||||
{
|
||||
return new SiteTypeAttributeOnlyForClassesInheritingSiteContentAnalyzer();
|
||||
}
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче