PA0005: Check that classes marked with [SiteType] inherits SiteContent.

This commit is contained in:
Mikael Lindemann 2020-05-13 12:06:01 +02:00
Родитель f4d3f2e337
Коммит 1aa677cd94
7 изменённых файлов: 394 добавлений и 0 удалений

Просмотреть файл

@ -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)

10
docs/PA0005/README.md Normal file
Просмотреть файл

@ -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";
}
}
}

27
src/Piranha.Analyzers/Resources.Designer.cs сгенерированный
Просмотреть файл

@ -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();
}
}
}