Analyzer to warn against fields not intended for single field regions.

Also contains a code fix provider that will suggest to move the field into a complex region.
This commit is contained in:
Mikael Lindemann 2020-02-08 20:21:59 +01:00
Родитель ad43a89fcc
Коммит e319e51a75
19 изменённых файлов: 1894 добавлений и 0 удалений

70
Piranha.Analyzers.sln Normal file
Просмотреть файл

@ -0,0 +1,70 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29709.97
MinimumVisualStudioVersion = 15.0.26124.0
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{BD012B1D-F0E1-4029-81BE-04D66BBD5AFE}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Piranha.Analyzers", "src\Piranha.Analyzers\Piranha.Analyzers.csproj", "{3F3E3590-CB01-4A9C-8628-BC7A23C0B64D}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{9FA21F26-134E-462C-AA04-2134731A6CFB}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Piranha.Analyzers.Test", "test\Piranha.Analyzers.Test\Piranha.Analyzers.Test.csproj", "{09015A43-EBE4-4102-A54E-FF58ADF94969}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuspec", ".nuspec", "{89374D21-B221-4B56-99EC-569DBE295AF1}"
ProjectSection(SolutionItems) = preProject
nuspec\Piranha.Analyzers.nuspec = nuspec\Piranha.Analyzers.nuspec
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".global", ".global", "{D571F145-B946-4998-80D0-D93B9FBF24E9}"
ProjectSection(SolutionItems) = preProject
LICENSE = LICENSE
README.md = README.md
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{3F3E3590-CB01-4A9C-8628-BC7A23C0B64D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3F3E3590-CB01-4A9C-8628-BC7A23C0B64D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3F3E3590-CB01-4A9C-8628-BC7A23C0B64D}.Debug|x64.ActiveCfg = Debug|Any CPU
{3F3E3590-CB01-4A9C-8628-BC7A23C0B64D}.Debug|x64.Build.0 = Debug|Any CPU
{3F3E3590-CB01-4A9C-8628-BC7A23C0B64D}.Debug|x86.ActiveCfg = Debug|Any CPU
{3F3E3590-CB01-4A9C-8628-BC7A23C0B64D}.Debug|x86.Build.0 = Debug|Any CPU
{3F3E3590-CB01-4A9C-8628-BC7A23C0B64D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3F3E3590-CB01-4A9C-8628-BC7A23C0B64D}.Release|Any CPU.Build.0 = Release|Any CPU
{3F3E3590-CB01-4A9C-8628-BC7A23C0B64D}.Release|x64.ActiveCfg = Release|Any CPU
{3F3E3590-CB01-4A9C-8628-BC7A23C0B64D}.Release|x64.Build.0 = Release|Any CPU
{3F3E3590-CB01-4A9C-8628-BC7A23C0B64D}.Release|x86.ActiveCfg = Release|Any CPU
{3F3E3590-CB01-4A9C-8628-BC7A23C0B64D}.Release|x86.Build.0 = Release|Any CPU
{09015A43-EBE4-4102-A54E-FF58ADF94969}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{09015A43-EBE4-4102-A54E-FF58ADF94969}.Debug|Any CPU.Build.0 = Debug|Any CPU
{09015A43-EBE4-4102-A54E-FF58ADF94969}.Debug|x64.ActiveCfg = Debug|Any CPU
{09015A43-EBE4-4102-A54E-FF58ADF94969}.Debug|x64.Build.0 = Debug|Any CPU
{09015A43-EBE4-4102-A54E-FF58ADF94969}.Debug|x86.ActiveCfg = Debug|Any CPU
{09015A43-EBE4-4102-A54E-FF58ADF94969}.Debug|x86.Build.0 = Debug|Any CPU
{09015A43-EBE4-4102-A54E-FF58ADF94969}.Release|Any CPU.ActiveCfg = Release|Any CPU
{09015A43-EBE4-4102-A54E-FF58ADF94969}.Release|Any CPU.Build.0 = Release|Any CPU
{09015A43-EBE4-4102-A54E-FF58ADF94969}.Release|x64.ActiveCfg = Release|Any CPU
{09015A43-EBE4-4102-A54E-FF58ADF94969}.Release|x64.Build.0 = Release|Any CPU
{09015A43-EBE4-4102-A54E-FF58ADF94969}.Release|x86.ActiveCfg = Release|Any CPU
{09015A43-EBE4-4102-A54E-FF58ADF94969}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{3F3E3590-CB01-4A9C-8628-BC7A23C0B64D} = {BD012B1D-F0E1-4029-81BE-04D66BBD5AFE}
{09015A43-EBE4-4102-A54E-FF58ADF94969} = {9FA21F26-134E-462C-AA04-2134731A6CFB}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {DF23C474-6EF4-409F-9DB2-B3FF38F5BAF2}
EndGlobalSection
EndGlobal

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

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd">
<metadata>
<id>Piranha.Analyzers</id>
<title>Piranha.Analyzers</title>
<version>8.0.2</version>
<authors>Mikael Lindemann</authors>
<owners>Mikael Lindemann, Håkan Edling</owners>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>Analyzers for Piranha CMS for .NET Core.</description>
<copyright>Copyright 2020 (c) Mikael Lindemann</copyright>
<tags>cms mvc aspnetcore netstandard</tags>
<license type="expression">MIT</license>
<projectUrl>https://github.com/piranhacms/piranha.core.analyzers</projectUrl>
<iconUrl>http://piranhacms.org/assets/twitter-shield.png</iconUrl>
<developmentDependency>true</developmentDependency>
<releaseNotes></releaseNotes>
</metadata>
<files>
<file src="..\src\Piranha.Analyzers\bin\Release\netstandard2.0\Piranha.Analyzers.dll" target="analyzers\dotnet\cs" />
<file src="..\src\Piranha.Analyzers\tools\*.ps1" target="tools\" />
</files>
</package>

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

@ -0,0 +1,33 @@
/*
* 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
*
*/
namespace Piranha.Analyzers
{
internal static class Constants
{
internal static class Types
{
internal const string PiranhaExtendFieldAttribute = "Piranha.Extend.FieldAttribute";
internal const string PiranhaExtendRegionAttribute = "Piranha.Extend.RegionAttribute";
internal const string PiranhaExtendFieldsAudioField = "Piranha.Extend.Fields.AudioField";
internal const string PiranhaExtendFieldsCheckBoxField = "Piranha.Extend.Fields.CheckBoxField";
internal const string PiranhaExtendFieldsDateField = "Piranha.Extend.Fields.DateField";
internal const string PiranhaExtendFieldsDocumentField = "Piranha.Extend.Fields.DocumentField";
internal const string PiranhaExtendFieldsImageField = "Piranha.Extend.Fields.ImageField";
internal const string PiranhaExtendFieldsMediaField = "Piranha.Extend.Fields.MediaField";
internal const string PiranhaExtendFieldsNumberField = "Piranha.Extend.Fields.NumberField";
internal const string PiranhaExtendFieldsPageField = "Piranha.Extend.Fields.PageField";
internal const string PiranhaExtendFieldsPostField = "Piranha.Extend.Fields.PostField";
internal const string PiranhaExtendFieldsStringField = "Piranha.Extend.Fields.StringField";
internal const string PiranhaExtendFieldsVideoField = "Piranha.Extend.Fields.VideoField";
}
}
}

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

@ -0,0 +1,120 @@
/*
* 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.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Simplification;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
namespace Piranha.Analyzers
{
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(IntroduceComplexRegionCodeFixProvider)), Shared]
public class IntroduceComplexRegionCodeFixProvider : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(NonSingleFieldRegionAnalyzer.DiagnosticId);
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
// Find the faulty property and its containing class
var faultyProperty = root.FindNode(context.Span).FirstAncestorOrSelf<PropertyDeclarationSyntax>();
var contentClass = faultyProperty.FirstAncestorOrSelf<ClassDeclarationSyntax>();
var complexRegionName = ComplexRegionName(contentClass);
if (complexRegionName == null)
{
// Could not find a complex region name.
return;
}
context.RegisterCodeFix(CodeAction.Create(
"Replace with complex region",
ct =>
{
// Create complex region with the faulty region as a field.
var complexRegionClass = CreateComplexRegionClass(
complexRegionName,
CreateAutoPropertyWithAttributes(faultyProperty.Type, faultyProperty.Identifier.ValueText, attributeNames: Constants.Types.PiranhaExtendFieldAttribute)
);
// Create a new region property of the new complex region type.
var regionProperty = CreateAutoPropertyWithAttributes(SyntaxFactory.ParseTypeName(complexRegionClass.Identifier.ValueText), "MyRegion", attributeNames: Constants.Types.PiranhaExtendRegionAttribute)
.WithTrailingTrivia(faultyProperty.GetTrailingTrivia());
// Replace the faulty region property with the proper one, add the region class as inner class.
var newContentClass = contentClass
.WithMembers(contentClass.Members.Replace(faultyProperty, regionProperty))
.AddMembers(complexRegionClass);
return Task.FromResult(context.Document.WithSyntaxRoot(root.ReplaceNode(contentClass, newContentClass)));
},
"Replace with complex region"), context.Diagnostics);
}
private static string ComplexRegionName(ClassDeclarationSyntax @class)
{
string Name(int number)
{
if (number <= 0)
{
return "ComplexRegion";
}
return $"ComplexRegion{number}";
}
for (var number = 0; number < 10; number++)
{
var name = Name(number);
if (!@class.Members.Any(m => (m.GetType().GetProperty("Identifier", BindingFlags.Public | BindingFlags.Instance)?.GetValue(m) as SyntaxToken?)?.ValueText == name))
{
return name;
}
}
// Give up on finding a class name.
return null;
}
private static PropertyDeclarationSyntax CreateAutoPropertyWithAttributes(TypeSyntax type, string name, params string[] attributeNames)
{
return SyntaxFactory
.PropertyDeclaration(type, name)
.AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword))
.AddAccessorListAccessors(
SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration).WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)),
SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration).WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken))
)
.AddAttributeLists(SyntaxFactory.AttributeList(new SeparatedSyntaxList<AttributeSyntax>().AddRange(attributeNames.Select(attr => SyntaxFactory.Attribute(SyntaxFactory.ParseName(attr))))))
.WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation);
}
private static ClassDeclarationSyntax CreateComplexRegionClass(string name, params PropertyDeclarationSyntax[] fields)
{
var complexRegionClass = SyntaxFactory.ClassDeclaration(name)
.AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword))
.AddMembers(fields)
.WithAdditionalAnnotations(Formatter.Annotation, Simplifier.Annotation);
return complexRegionClass;
}
}
}

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

@ -0,0 +1,83 @@
/*
* 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.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Piranha.Analyzers
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class NonSingleFieldRegionAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "PA0001";
private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.NonSingleFieldRegionAnalyzerTitle), Resources.ResourceManager, typeof(Resources));
private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.NonSingleFieldRegionAnalyzerMessageFormat), Resources.ResourceManager, typeof(Resources));
private static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.NonSingleFieldRegionAnalyzerDescription), Resources.ResourceManager, typeof(Resources));
private const string Category = "Usage";
private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
public override void Initialize(AnalysisContext context)
{
// List of built-in field types that, per documentation, is primarily intended for complex regions.
context.RegisterSyntaxNodeAction(c => AnalyzeSyntaxNode(c, Constants.Types.PiranhaExtendFieldsAudioField), SyntaxKind.PropertyDeclaration);
context.RegisterSyntaxNodeAction(c => AnalyzeSyntaxNode(c, Constants.Types.PiranhaExtendFieldsCheckBoxField), SyntaxKind.PropertyDeclaration);
context.RegisterSyntaxNodeAction(c => AnalyzeSyntaxNode(c, Constants.Types.PiranhaExtendFieldsDateField), SyntaxKind.PropertyDeclaration);
context.RegisterSyntaxNodeAction(c => AnalyzeSyntaxNode(c, Constants.Types.PiranhaExtendFieldsDocumentField), SyntaxKind.PropertyDeclaration);
context.RegisterSyntaxNodeAction(c => AnalyzeSyntaxNode(c, Constants.Types.PiranhaExtendFieldsImageField), SyntaxKind.PropertyDeclaration);
context.RegisterSyntaxNodeAction(c => AnalyzeSyntaxNode(c, Constants.Types.PiranhaExtendFieldsMediaField), SyntaxKind.PropertyDeclaration);
context.RegisterSyntaxNodeAction(c => AnalyzeSyntaxNode(c, Constants.Types.PiranhaExtendFieldsNumberField), SyntaxKind.PropertyDeclaration);
context.RegisterSyntaxNodeAction(c => AnalyzeSyntaxNode(c, Constants.Types.PiranhaExtendFieldsPageField), SyntaxKind.PropertyDeclaration);
context.RegisterSyntaxNodeAction(c => AnalyzeSyntaxNode(c, Constants.Types.PiranhaExtendFieldsPostField), SyntaxKind.PropertyDeclaration);
context.RegisterSyntaxNodeAction(c => AnalyzeSyntaxNode(c, Constants.Types.PiranhaExtendFieldsStringField), SyntaxKind.PropertyDeclaration);
context.RegisterSyntaxNodeAction(c => AnalyzeSyntaxNode(c, Constants.Types.PiranhaExtendFieldsVideoField), SyntaxKind.PropertyDeclaration);
}
private static void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context, string fieldName)
{
if (!(context.Node is PropertyDeclarationSyntax pds))
{
return;
}
var fieldTypeSymbol = context.Compilation.GetTypeByMetadataName(fieldName);
var s = context.SemanticModel.GetTypeInfo(pds.Type, context.CancellationToken);
if (!s.Type.Equals(fieldTypeSymbol))
{
return;
}
// Verify that the property is marked with RegionAttribute.
var regionAttributeSymbol = context.Compilation.GetTypeByMetadataName("Piranha.Extend.RegionAttribute");
foreach (var attributeList in pds.AttributeLists)
{
foreach (var attribute in attributeList.Attributes)
{
var attributeTypeInfo = context.SemanticModel.GetTypeInfo(attribute, context.CancellationToken);
if (regionAttributeSymbol.Equals(attributeTypeInfo.ConvertedType))
{
context.ReportDiagnostic(Diagnostic.Create(Rule, pds.GetLocation(), fieldName.Split('.').Last()));
}
}
}
}
}
}

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

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IncludeBuildOutput>false</IncludeBuildOutput>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<NoPackageAnalysis>true</NoPackageAnalysis>
<Version>8.0.2.1</Version>
<Company>Piranha CMS</Company>
<AssemblyTitle>Piranha.Analyzers</AssemblyTitle>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="2.6.2" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="2.10" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<Compile Update="Resources.Designer.cs" DesignTime="True" AutoGen="True" DependentUpon="Resources.resx" />
<EmbeddedResource Update="Resources.resx" Generator="ResXFileCodeGenerator" LastGenOutput="Resources.Designer.cs" />
</ItemGroup>
<ItemGroup>
<None Update="tools\*.ps1" CopyToOutputDirectory="Always" Pack="true" PackagePath="" />
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>
</Project>

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

@ -0,0 +1,90 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Piranha.Analyzers {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Piranha.Analyzers.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Usage of field type as a single field region might not be supported.
/// </summary>
internal static string NonSingleFieldRegionAnalyzerDescription {
get {
return ResourceManager.GetString("NonSingleFieldRegionAnalyzerDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to {0} should not be used as a single field region.
/// </summary>
internal static string NonSingleFieldRegionAnalyzerMessageFormat {
get {
return ResourceManager.GetString("NonSingleFieldRegionAnalyzerMessageFormat", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Field type is primarily intended to be used in complex regions.
/// </summary>
internal static string NonSingleFieldRegionAnalyzerTitle {
get {
return ResourceManager.GetString("NonSingleFieldRegionAnalyzerTitle", resourceCulture);
}
}
}
}

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

@ -0,0 +1,132 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="NonSingleFieldRegionAnalyzerDescription" xml:space="preserve">
<value>Usage of field type as a single field region might not be supported</value>
<comment>An optional longer localizable description of the diagnostic.</comment>
</data>
<data name="NonSingleFieldRegionAnalyzerMessageFormat" xml:space="preserve">
<value>{0} should not be used as a single field region</value>
<comment>The format-able message the diagnostic displays. The parameter is the type of field.</comment>
</data>
<data name="NonSingleFieldRegionAnalyzerTitle" xml:space="preserve">
<value>Field type is primarily intended to be used in complex regions</value>
<comment>The title of the diagnostic.</comment>
</data>
</root>

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

@ -0,0 +1,58 @@
param($installPath, $toolsPath, $package, $project)
if($project.Object.SupportsPackageDependencyResolution)
{
if($project.Object.SupportsPackageDependencyResolution())
{
# Do not install analyzers via install.ps1, instead let the project system handle it.
return
}
}
$analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers") * -Resolve
foreach($analyzersPath in $analyzersPaths)
{
if (Test-Path $analyzersPath)
{
# Install the language agnostic analyzers.
foreach ($analyzerFilePath in Get-ChildItem -Path "$analyzersPath\*.dll" -Exclude *.resources.dll)
{
if($project.Object.AnalyzerReferences)
{
$project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName)
}
}
}
}
# $project.Type gives the language name like (C# or VB.NET)
$languageFolder = ""
if($project.Type -eq "C#")
{
$languageFolder = "cs"
}
if($project.Type -eq "VB.NET")
{
$languageFolder = "vb"
}
if($languageFolder -eq "")
{
return
}
foreach($analyzersPath in $analyzersPaths)
{
# Install language specific analyzers.
$languageAnalyzersPath = join-path $analyzersPath $languageFolder
if (Test-Path $languageAnalyzersPath)
{
foreach ($analyzerFilePath in Get-ChildItem -Path "$languageAnalyzersPath\*.dll" -Exclude *.resources.dll)
{
if($project.Object.AnalyzerReferences)
{
$project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName)
}
}
}
}

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

@ -0,0 +1,64 @@
param($installPath, $toolsPath, $package, $project)
if($project.Object.SupportsPackageDependencyResolution)
{
if($project.Object.SupportsPackageDependencyResolution())
{
# Do not uninstall analyzers via uninstall.ps1, instead let the project system handle it.
return
}
}
$analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers") * -Resolve
foreach($analyzersPath in $analyzersPaths)
{
# Uninstall the language agnostic analyzers.
if (Test-Path $analyzersPath)
{
foreach ($analyzerFilePath in Get-ChildItem -Path "$analyzersPath\*.dll" -Exclude *.resources.dll)
{
if($project.Object.AnalyzerReferences)
{
$project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName)
}
}
}
}
# $project.Type gives the language name like (C# or VB.NET)
$languageFolder = ""
if($project.Type -eq "C#")
{
$languageFolder = "cs"
}
if($project.Type -eq "VB.NET")
{
$languageFolder = "vb"
}
if($languageFolder -eq "")
{
return
}
foreach($analyzersPath in $analyzersPaths)
{
# Uninstall language specific analyzers.
$languageAnalyzersPath = join-path $analyzersPath $languageFolder
if (Test-Path $languageAnalyzersPath)
{
foreach ($analyzerFilePath in Get-ChildItem -Path "$languageAnalyzersPath\*.dll" -Exclude *.resources.dll)
{
if($project.Object.AnalyzerReferences)
{
try
{
$project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName)
}
catch
{
}
}
}
}
}

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

@ -0,0 +1,184 @@
/*
* 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.CodeFixes;
using Microsoft.CodeAnalysis.Diagnostics;
using TestHelper;
using Xunit;
namespace Piranha.Analyzers.Test
{
public class AudioFieldRegionAnalyzersUnitTests : CodeFixVerifier
{
[Fact]
public void NoDiagnosticIfNoCode()
{
var test = @"";
VerifyCSharpDiagnostic(test);
}
[Fact]
public void DiagnosticRegionAppliedToAudioFieldProperty()
{
var test = @"
using Piranha.Extend;
using Piranha.Extend.Fields;
using Piranha.Models;
namespace ConsoleApplication1
{
class TypeName : Post<TypeName>
{
[Region]
public AudioField Audio { get; set; }
}
}";
var expected = new DiagnosticResult
{
Id = "PA0001",
Message = "AudioField should not be used as a single field region",
Severity = DiagnosticSeverity.Warning,
Locations =
new[] {
new DiagnosticResultLocation("Test0.cs", 10, 9)
}
};
VerifyCSharpDiagnostic(test, expected);
var expectedFix = @"
using Piranha.Extend;
using Piranha.Extend.Fields;
using Piranha.Models;
namespace ConsoleApplication1
{
class TypeName : Post<TypeName>
{
[Region]
public ComplexRegion MyRegion { get; set; }
public class ComplexRegion
{
[Field]
public AudioField Audio { get; set; }
}
}
}";
VerifyCSharpFix(test, expectedFix);
}
[Fact]
public void DiagnosticRegionAppliedToAudioFieldPropertyWithExistingComplexRegion()
{
var test = @"
using Piranha.Extend;
using Piranha.Extend.Fields;
using Piranha.Models;
namespace ConsoleApplication1
{
class TypeName : Post<TypeName>
{
[Region]
public AudioField Audio { get; set; }
[Region]
public ComplexRegion Region { get; set; }
public class ComplexRegion
{
}
}
}";
var expected = new DiagnosticResult
{
Id = "PA0001",
Message = "AudioField should not be used as a single field region",
Severity = DiagnosticSeverity.Warning,
Locations =
new[] {
new DiagnosticResultLocation("Test0.cs", 10, 9)
}
};
VerifyCSharpDiagnostic(test, expected);
var expectedFix = @"
using Piranha.Extend;
using Piranha.Extend.Fields;
using Piranha.Models;
namespace ConsoleApplication1
{
class TypeName : Post<TypeName>
{
[Region]
public ComplexRegion1 MyRegion { get; set; }
[Region]
public ComplexRegion Region { get; set; }
public class ComplexRegion
{
}
public class ComplexRegion1
{
[Field]
public AudioField Audio { get; set; }
}
}
}";
VerifyCSharpFix(test, expectedFix);
}
[Fact]
public void NoDiagnosticAudioFieldInComplexRegion()
{
var test = @"
using Piranha.Extend;
using Piranha.Extend.Fields;
using Piranha.Models;
namespace ConsoleApplication1
{
class TypeName : Post<TypeName>
{
[Region]
public ContentRegion Content { get; set; }
public class ContentRegion
{
[Field]
public AudioField Audio { get; set; }
}
}
}";
VerifyCSharpDiagnostic(test);
}
protected override CodeFixProvider GetCSharpCodeFixProvider()
{
return new IntroduceComplexRegionCodeFixProvider();
}
protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
{
return new NonSingleFieldRegionAnalyzer();
}
}
}

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

@ -0,0 +1,95 @@
/*
* 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.CodeFixes;
using Microsoft.CodeAnalysis.Diagnostics;
using TestHelper;
using Xunit;
namespace Piranha.Analyzers.Test
{
public class CheckBoxFieldRegionAnalyzersUnitTests : CodeFixVerifier
{
[Fact]
public void NoDiagnosticIfNoCode()
{
var test = @"";
VerifyCSharpDiagnostic(test);
}
[Fact]
public void DiagnosticRegionAppliedToCheckBoxFieldProperty()
{
var test = @"
using Piranha.Extend;
using Piranha.Extend.Fields;
using Piranha.Models;
namespace ConsoleApplication1
{
class TypeName : Post<TypeName>
{
[Region]
public CheckBoxField CheckBox { get; set; }
}
}";
var expected = new DiagnosticResult
{
Id = "PA0001",
Message = "CheckBoxField should not be used as a single field region",
Severity = DiagnosticSeverity.Warning,
Locations =
new[] {
new DiagnosticResultLocation("Test0.cs", 10, 13)
}
};
VerifyCSharpDiagnostic(test, expected);
}
[Fact]
public void NoDiagnosticCheckBoxFieldInComplexRegion()
{
var test = @"
using Piranha.Extend;
using Piranha.Extend.Fields;
using Piranha.Models;
namespace ConsoleApplication1
{
class TypeName : Post<TypeName>
{
[Region]
public SettingsRegion Settings { get; set; }
public class SettingsRegion
{
[Field]
public CheckBoxField CheckBox { get; set; }
}
}
}";
VerifyCSharpDiagnostic(test);
}
protected override CodeFixProvider GetCSharpCodeFixProvider()
{
return null;
}
protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
{
return new NonSingleFieldRegionAnalyzer();
}
}
}

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

@ -0,0 +1,95 @@
/*
* 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.CodeFixes;
using Microsoft.CodeAnalysis.Diagnostics;
using TestHelper;
using Xunit;
namespace Piranha.Analyzers.Test
{
public class DateFieldRegionAnalyzersUnitTests : CodeFixVerifier
{
[Fact]
public void NoDiagnosticIfNoCode()
{
var test = @"";
VerifyCSharpDiagnostic(test);
}
[Fact]
public void DiagnosticRegionAppliedToDateFieldProperty()
{
var test = @"
using Piranha.Extend;
using Piranha.Extend.Fields;
using Piranha.Models;
namespace ConsoleApplication1
{
class TypeName : Post<TypeName>
{
[Region]
public DateField Date { get; set; }
}
}";
var expected = new DiagnosticResult
{
Id = "PA0001",
Message = "DateField should not be used as a single field region",
Severity = DiagnosticSeverity.Warning,
Locations =
new[] {
new DiagnosticResultLocation("Test0.cs", 10, 13)
}
};
VerifyCSharpDiagnostic(test, expected);
}
[Fact]
public void NoDiagnosticDateFieldInComplexRegion()
{
var test = @"
using Piranha.Extend;
using Piranha.Extend.Fields;
using Piranha.Models;
namespace ConsoleApplication1
{
class TypeName : Post<TypeName>
{
[Region]
public ContentRegion Content { get; set; }
public class ContentRegion
{
[Field]
public DateField Date { get; set; }
}
}
}";
VerifyCSharpDiagnostic(test);
}
protected override CodeFixProvider GetCSharpCodeFixProvider()
{
return null;
}
protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
{
return new NonSingleFieldRegionAnalyzer();
}
}
}

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

@ -0,0 +1,94 @@
/*
* 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.CodeActions;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Simplification;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
namespace TestHelper
{
/// <summary>
/// Diagnostic Producer class with extra methods dealing with applying codefixes
/// All methods are static
/// </summary>
public abstract partial class CodeFixVerifier : DiagnosticVerifier
{
/// <summary>
/// Apply the inputted CodeAction to the inputted document.
/// Meant to be used to apply codefixes.
/// </summary>
/// <param name="document">The Document to apply the fix on</param>
/// <param name="codeAction">A CodeAction that will be applied to the Document.</param>
/// <returns>A Document with the changes from the CodeAction</returns>
private static Document ApplyFix(Document document, CodeAction codeAction)
{
var operations = codeAction.GetOperationsAsync(CancellationToken.None).Result;
var solution = operations.OfType<ApplyChangesOperation>().Single().ChangedSolution;
return solution.GetDocument(document.Id);
}
/// <summary>
/// Compare two collections of Diagnostics,and return a list of any new diagnostics that appear only in the second collection.
/// Note: Considers Diagnostics to be the same if they have the same Ids. In the case of multiple diagnostics with the same Id in a row,
/// this method may not necessarily return the new one.
/// </summary>
/// <param name="diagnostics">The Diagnostics that existed in the code before the CodeFix was applied</param>
/// <param name="newDiagnostics">The Diagnostics that exist in the code after the CodeFix was applied</param>
/// <returns>A list of Diagnostics that only surfaced in the code after the CodeFix was applied</returns>
private static IEnumerable<Diagnostic> GetNewDiagnostics(IEnumerable<Diagnostic> diagnostics, IEnumerable<Diagnostic> newDiagnostics)
{
var oldArray = diagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray();
var newArray = newDiagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray();
int oldIndex = 0;
int newIndex = 0;
while (newIndex < newArray.Length)
{
if (oldIndex < oldArray.Length && oldArray[oldIndex].Id == newArray[newIndex].Id)
{
++oldIndex;
++newIndex;
}
else
{
yield return newArray[newIndex++];
}
}
}
/// <summary>
/// Get the existing compiler diagnostics on the inputted document.
/// </summary>
/// <param name="document">The Document to run the compiler diagnostic analyzers on</param>
/// <returns>The compiler diagnostics that were found in the code</returns>
private static IEnumerable<Diagnostic> GetCompilerDiagnostics(Document document)
{
return document.GetSemanticModelAsync().Result.GetDiagnostics();
}
/// <summary>
/// Given a document, turn it into a string based on the syntax root
/// </summary>
/// <param name="document">The Document to be converted to a string</param>
/// <returns>A string containing the syntax of the Document after formatting</returns>
private static string GetStringFromDocument(Document document)
{
var simplifiedDoc = Simplifier.ReduceAsync(document, Simplifier.Annotation).Result;
var root = simplifiedDoc.GetSyntaxRootAsync().Result;
root = Formatter.Format(root, Formatter.Annotation, simplifiedDoc.Project.Solution.Workspace);
return root.GetText().ToString();
}
}
}

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

@ -0,0 +1,97 @@
/*
* 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 System;
namespace TestHelper
{
/// <summary>
/// Location where the diagnostic appears, as determined by path, line number, and column number.
/// </summary>
public struct DiagnosticResultLocation
{
public DiagnosticResultLocation(string path, int line, int column)
{
if (line < -1)
{
throw new ArgumentOutOfRangeException(nameof(line), "line must be >= -1");
}
if (column < -1)
{
throw new ArgumentOutOfRangeException(nameof(column), "column must be >= -1");
}
this.Path = path;
this.Line = line;
this.Column = column;
}
public string Path { get; }
public int Line { get; }
public int Column { get; }
}
/// <summary>
/// Struct that stores information about a Diagnostic appearing in a source
/// </summary>
public struct DiagnosticResult
{
private DiagnosticResultLocation[] locations;
public DiagnosticResultLocation[] Locations
{
get
{
if (this.locations == null)
{
this.locations = new DiagnosticResultLocation[] { };
}
return this.locations;
}
set
{
this.locations = value;
}
}
public DiagnosticSeverity Severity { get; set; }
public string Id { get; set; }
public string Message { get; set; }
public string Path
{
get
{
return this.Locations.Length > 0 ? this.Locations[0].Path : "";
}
}
public int Line
{
get
{
return this.Locations.Length > 0 ? this.Locations[0].Line : -1;
}
}
public int Column
{
get
{
return this.Locations.Length > 0 ? this.Locations[0].Column : -1;
}
}
}
}

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

@ -0,0 +1,186 @@
/*
* 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.Diagnostics;
using Microsoft.CodeAnalysis.Text;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
namespace TestHelper
{
/// <summary>
/// Class for turning strings into documents and getting the diagnostics on them
/// All methods are static
/// </summary>
public abstract partial class DiagnosticVerifier
{
private static readonly MetadataReference NetStandard20 = MetadataReference.CreateFromFile(Assembly.Load("netstandard, Version=2.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51").Location);
private static readonly MetadataReference SystemRuntime = MetadataReference.CreateFromFile(Assembly.Load("System.Runtime, Version=4.2.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a").Location);
private static readonly MetadataReference CorlibReference = MetadataReference.CreateFromFile(typeof(object).Assembly.Location);
private static readonly MetadataReference SystemCoreReference = MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location);
private static readonly MetadataReference CSharpSymbolsReference = MetadataReference.CreateFromFile(typeof(CSharpCompilation).Assembly.Location);
private static readonly MetadataReference CodeAnalysisReference = MetadataReference.CreateFromFile(typeof(Compilation).Assembly.Location);
private static readonly MetadataReference PiranhaReference = MetadataReference.CreateFromFile(typeof(Piranha.Models.Content).Assembly.Location);
internal static string DefaultFilePathPrefix = "Test";
internal static string CSharpDefaultFileExt = "cs";
internal static string VisualBasicDefaultExt = "vb";
internal static string TestProjectName = "TestProject";
#region Get Diagnostics
/// <summary>
/// Given classes in the form of strings, their language, and an IDiagnosticAnalyzer to apply to it, return the diagnostics found in the string after converting it to a document.
/// </summary>
/// <param name="sources">Classes in the form of strings</param>
/// <param name="language">The language the source classes are in</param>
/// <param name="analyzer">The analyzer to be run on the sources</param>
/// <returns>An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location</returns>
private static Diagnostic[] GetSortedDiagnostics(string[] sources, string language, DiagnosticAnalyzer analyzer)
{
return GetSortedDiagnosticsFromDocuments(analyzer, GetDocuments(sources, language));
}
/// <summary>
/// Given an analyzer and a document to apply it to, run the analyzer and gather an array of diagnostics found in it.
/// The returned diagnostics are then ordered by location in the source document.
/// </summary>
/// <param name="analyzer">The analyzer to run on the documents</param>
/// <param name="documents">The Documents that the analyzer will be run on</param>
/// <returns>An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location</returns>
protected static Diagnostic[] GetSortedDiagnosticsFromDocuments(DiagnosticAnalyzer analyzer, Document[] documents)
{
var projects = new HashSet<Project>();
foreach (var document in documents)
{
projects.Add(document.Project);
}
var diagnostics = new List<Diagnostic>();
foreach (var project in projects)
{
var compilationWithAnalyzers = project.GetCompilationAsync().Result.WithAnalyzers(ImmutableArray.Create(analyzer));
var diags = compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().Result;
foreach (var diag in diags)
{
if (diag.Location == Location.None || diag.Location.IsInMetadata)
{
diagnostics.Add(diag);
}
else
{
for (int i = 0; i < documents.Length; i++)
{
var document = documents[i];
var tree = document.GetSyntaxTreeAsync().Result;
if (tree == diag.Location.SourceTree)
{
diagnostics.Add(diag);
}
}
}
}
}
var results = SortDiagnostics(diagnostics);
diagnostics.Clear();
return results;
}
/// <summary>
/// Sort diagnostics by location in source document
/// </summary>
/// <param name="diagnostics">The list of Diagnostics to be sorted</param>
/// <returns>An IEnumerable containing the Diagnostics in order of Location</returns>
private static Diagnostic[] SortDiagnostics(IEnumerable<Diagnostic> diagnostics)
{
return diagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray();
}
#endregion
#region Set up compilation and documents
/// <summary>
/// Given an array of strings as sources and a language, turn them into a project and return the documents and spans of it.
/// </summary>
/// <param name="sources">Classes in the form of strings</param>
/// <param name="language">The language the source code is in</param>
/// <returns>A Tuple containing the Documents produced from the sources and their TextSpans if relevant</returns>
private static Document[] GetDocuments(string[] sources, string language)
{
if (language != LanguageNames.CSharp && language != LanguageNames.VisualBasic)
{
throw new ArgumentException("Unsupported Language");
}
var project = CreateProject(sources, language);
var documents = project.Documents.ToArray();
if (sources.Length != documents.Length)
{
throw new InvalidOperationException("Amount of sources did not match amount of Documents created");
}
return documents;
}
/// <summary>
/// Create a Document from a string through creating a project that contains it.
/// </summary>
/// <param name="source">Classes in the form of a string</param>
/// <param name="language">The language the source code is in</param>
/// <returns>A Document created from the source string</returns>
protected static Document CreateDocument(string source, string language = LanguageNames.CSharp)
{
return CreateProject(new[] { source }, language).Documents.First();
}
/// <summary>
/// Create a project using the inputted strings as sources.
/// </summary>
/// <param name="sources">Classes in the form of strings</param>
/// <param name="language">The language the source code is in</param>
/// <returns>A Project created out of the Documents created from the source strings</returns>
private static Project CreateProject(string[] sources, string language = LanguageNames.CSharp)
{
string fileNamePrefix = DefaultFilePathPrefix;
string fileExt = language == LanguageNames.CSharp ? CSharpDefaultFileExt : VisualBasicDefaultExt;
var projectId = ProjectId.CreateNewId(debugName: TestProjectName);
var solution = new AdhocWorkspace()
.CurrentSolution
.AddProject(projectId, TestProjectName, TestProjectName, language)
.AddMetadataReference(projectId, NetStandard20)
.AddMetadataReference(projectId, SystemRuntime)
.AddMetadataReference(projectId, CorlibReference)
.AddMetadataReference(projectId, SystemCoreReference)
.AddMetadataReference(projectId, CSharpSymbolsReference)
.AddMetadataReference(projectId, CodeAnalysisReference)
.AddMetadataReference(projectId, PiranhaReference);
int count = 0;
foreach (var source in sources)
{
var newFileName = fileNamePrefix + count + "." + fileExt;
var documentId = DocumentId.CreateNewId(projectId, debugName: newFileName);
solution = solution.AddDocument(documentId, newFileName, SourceText.From(source));
count++;
}
return solution.GetProject(projectId);
}
#endregion
}
}

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

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="2.6.2" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="2.10" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Piranha" Version="8.0.2" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Piranha.Analyzers\Piranha.Analyzers.csproj" />
</ItemGroup>
</Project>

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

@ -0,0 +1,138 @@
/*
* 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.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Formatting;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Xunit;
namespace TestHelper
{
/// <summary>
/// Superclass of all Unit tests made for diagnostics with codefixes.
/// Contains methods used to verify correctness of codefixes
/// </summary>
public abstract partial class CodeFixVerifier : DiagnosticVerifier
{
/// <summary>
/// Returns the codefix being tested (C#) - to be implemented in non-abstract class
/// </summary>
/// <returns>The CodeFixProvider to be used for CSharp code</returns>
protected virtual CodeFixProvider GetCSharpCodeFixProvider()
{
return null;
}
/// <summary>
/// Returns the codefix being tested (VB) - to be implemented in non-abstract class
/// </summary>
/// <returns>The CodeFixProvider to be used for VisualBasic code</returns>
protected virtual CodeFixProvider GetBasicCodeFixProvider()
{
return null;
}
/// <summary>
/// Called to test a C# codefix when applied on the inputted string as a source
/// </summary>
/// <param name="oldSource">A class in the form of a string before the CodeFix was applied to it</param>
/// <param name="newSource">A class in the form of a string after the CodeFix was applied to it</param>
/// <param name="codeFixIndex">Index determining which codefix to apply if there are multiple</param>
/// <param name="allowNewCompilerDiagnostics">A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied</param>
protected void VerifyCSharpFix(string oldSource, string newSource, int? codeFixIndex = null, bool allowNewCompilerDiagnostics = false)
{
VerifyFix(LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), GetCSharpCodeFixProvider(), oldSource, newSource, codeFixIndex, allowNewCompilerDiagnostics);
}
/// <summary>
/// Called to test a VB codefix when applied on the inputted string as a source
/// </summary>
/// <param name="oldSource">A class in the form of a string before the CodeFix was applied to it</param>
/// <param name="newSource">A class in the form of a string after the CodeFix was applied to it</param>
/// <param name="codeFixIndex">Index determining which codefix to apply if there are multiple</param>
/// <param name="allowNewCompilerDiagnostics">A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied</param>
protected void VerifyBasicFix(string oldSource, string newSource, int? codeFixIndex = null, bool allowNewCompilerDiagnostics = false)
{
VerifyFix(LanguageNames.VisualBasic, GetBasicDiagnosticAnalyzer(), GetBasicCodeFixProvider(), oldSource, newSource, codeFixIndex, allowNewCompilerDiagnostics);
}
/// <summary>
/// General verifier for codefixes.
/// Creates a Document from the source string, then gets diagnostics on it and applies the relevant codefixes.
/// Then gets the string after the codefix is applied and compares it with the expected result.
/// Note: If any codefix causes new diagnostics to show up, the test fails unless allowNewCompilerDiagnostics is set to true.
/// </summary>
/// <param name="language">The language the source code is in</param>
/// <param name="analyzer">The analyzer to be applied to the source code</param>
/// <param name="codeFixProvider">The codefix to be applied to the code wherever the relevant Diagnostic is found</param>
/// <param name="oldSource">A class in the form of a string before the CodeFix was applied to it</param>
/// <param name="newSource">A class in the form of a string after the CodeFix was applied to it</param>
/// <param name="codeFixIndex">Index determining which codefix to apply if there are multiple</param>
/// <param name="allowNewCompilerDiagnostics">A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied</param>
private void VerifyFix(string language, DiagnosticAnalyzer analyzer, CodeFixProvider codeFixProvider, string oldSource, string newSource, int? codeFixIndex, bool allowNewCompilerDiagnostics)
{
var document = CreateDocument(oldSource, language);
var analyzerDiagnostics = GetSortedDiagnosticsFromDocuments(analyzer, new[] { document });
var compilerDiagnostics = GetCompilerDiagnostics(document);
var attempts = analyzerDiagnostics.Length;
for (int i = 0; i < attempts; ++i)
{
var actions = new List<CodeAction>();
var context = new CodeFixContext(document, analyzerDiagnostics[0], (a, d) => actions.Add(a), CancellationToken.None);
codeFixProvider.RegisterCodeFixesAsync(context).Wait();
if (!actions.Any())
{
break;
}
if (codeFixIndex != null)
{
document = ApplyFix(document, actions.ElementAt((int)codeFixIndex));
break;
}
document = ApplyFix(document, actions.ElementAt(0));
analyzerDiagnostics = GetSortedDiagnosticsFromDocuments(analyzer, new[] { document });
var newCompilerDiagnostics = GetNewDiagnostics(compilerDiagnostics, GetCompilerDiagnostics(document));
//check if applying the code fix introduced any new compiler diagnostics
if (!allowNewCompilerDiagnostics && newCompilerDiagnostics.Any())
{
// Format and get the compiler diagnostics again so that the locations make sense in the output
document = document.WithSyntaxRoot(Formatter.Format(document.GetSyntaxRootAsync().Result, Formatter.Annotation, document.Project.Solution.Workspace));
newCompilerDiagnostics = GetNewDiagnostics(compilerDiagnostics, GetCompilerDiagnostics(document));
Assert.True(false,
string.Format("Fix introduced new compiler diagnostics:\r\n{0}\r\n\r\nNew document:\r\n{1}\r\n",
string.Join("\r\n", newCompilerDiagnostics.Select(d => d.ToString())),
document.GetSyntaxRootAsync().Result.ToFullString()));
}
//check if there are analyzer diagnostics left after the code fix
if (!analyzerDiagnostics.Any())
{
break;
}
}
//after applying all of the code fixes, compare the resulting string to the inputted one
var actual = GetStringFromDocument(document);
Assert.Equal(newSource, actual);
}
}
}

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

@ -0,0 +1,280 @@
/*
* 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.Diagnostics;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Xunit;
namespace TestHelper
{
/// <summary>
/// Superclass of all Unit Tests for DiagnosticAnalyzers
/// </summary>
public abstract partial class DiagnosticVerifier
{
#region To be implemented by Test classes
/// <summary>
/// Get the CSharp analyzer being tested - to be implemented in non-abstract class
/// </summary>
protected virtual DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
{
return null;
}
/// <summary>
/// Get the Visual Basic analyzer being tested (C#) - to be implemented in non-abstract class
/// </summary>
protected virtual DiagnosticAnalyzer GetBasicDiagnosticAnalyzer()
{
return null;
}
#endregion
#region Verifier wrappers
/// <summary>
/// Called to test a C# DiagnosticAnalyzer when applied on the single inputted string as a source
/// Note: input a DiagnosticResult for each Diagnostic expected
/// </summary>
/// <param name="source">A class in the form of a string to run the analyzer on</param>
/// <param name="expected"> DiagnosticResults that should appear after the analyzer is run on the source</param>
protected void VerifyCSharpDiagnostic(string source, params DiagnosticResult[] expected)
{
VerifyDiagnostics(new[] { source }, LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), expected);
}
/// <summary>
/// Called to test a VB DiagnosticAnalyzer when applied on the single inputted string as a source
/// Note: input a DiagnosticResult for each Diagnostic expected
/// </summary>
/// <param name="source">A class in the form of a string to run the analyzer on</param>
/// <param name="expected">DiagnosticResults that should appear after the analyzer is run on the source</param>
protected void VerifyBasicDiagnostic(string source, params DiagnosticResult[] expected)
{
VerifyDiagnostics(new[] { source }, LanguageNames.VisualBasic, GetBasicDiagnosticAnalyzer(), expected);
}
/// <summary>
/// Called to test a C# DiagnosticAnalyzer when applied on the inputted strings as a source
/// Note: input a DiagnosticResult for each Diagnostic expected
/// </summary>
/// <param name="sources">An array of strings to create source documents from to run the analyzers on</param>
/// <param name="expected">DiagnosticResults that should appear after the analyzer is run on the sources</param>
protected void VerifyCSharpDiagnostic(string[] sources, params DiagnosticResult[] expected)
{
VerifyDiagnostics(sources, LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), expected);
}
/// <summary>
/// Called to test a VB DiagnosticAnalyzer when applied on the inputted strings as a source
/// Note: input a DiagnosticResult for each Diagnostic expected
/// </summary>
/// <param name="sources">An array of strings to create source documents from to run the analyzers on</param>
/// <param name="expected">DiagnosticResults that should appear after the analyzer is run on the sources</param>
protected void VerifyBasicDiagnostic(string[] sources, params DiagnosticResult[] expected)
{
VerifyDiagnostics(sources, LanguageNames.VisualBasic, GetBasicDiagnosticAnalyzer(), expected);
}
/// <summary>
/// General method that gets a collection of actual diagnostics found in the source after the analyzer is run,
/// then verifies each of them.
/// </summary>
/// <param name="sources">An array of strings to create source documents from to run the analyzers on</param>
/// <param name="language">The language of the classes represented by the source strings</param>
/// <param name="analyzer">The analyzer to be run on the source code</param>
/// <param name="expected">DiagnosticResults that should appear after the analyzer is run on the sources</param>
private void VerifyDiagnostics(string[] sources, string language, DiagnosticAnalyzer analyzer, params DiagnosticResult[] expected)
{
var diagnostics = GetSortedDiagnostics(sources, language, analyzer);
VerifyDiagnosticResults(diagnostics, analyzer, expected);
}
#endregion
#region Actual comparisons and verifications
/// <summary>
/// Checks each of the actual Diagnostics found and compares them with the corresponding DiagnosticResult in the array of expected results.
/// Diagnostics are considered equal only if the DiagnosticResultLocation, Id, Severity, and Message of the DiagnosticResult match the actual diagnostic.
/// </summary>
/// <param name="actualResults">The Diagnostics found by the compiler after running the analyzer on the source code</param>
/// <param name="analyzer">The analyzer that was being run on the sources</param>
/// <param name="expectedResults">Diagnostic Results that should have appeared in the code</param>
private static void VerifyDiagnosticResults(IEnumerable<Diagnostic> actualResults, DiagnosticAnalyzer analyzer, params DiagnosticResult[] expectedResults)
{
int expectedCount = expectedResults.Count();
int actualCount = actualResults.Count();
if (expectedCount != actualCount)
{
string diagnosticsOutput = actualResults.Any() ? FormatDiagnostics(analyzer, actualResults.ToArray()) : " NONE.";
Assert.True(false,
string.Format("Mismatch between number of diagnostics returned, expected \"{0}\" actual \"{1}\"\r\n\r\nDiagnostics:\r\n{2}\r\n", expectedCount, actualCount, diagnosticsOutput));
}
for (int i = 0; i < expectedResults.Length; i++)
{
var actual = actualResults.ElementAt(i);
var expected = expectedResults[i];
if (expected.Line == -1 && expected.Column == -1)
{
if (actual.Location != Location.None)
{
Assert.True(false,
string.Format("Expected:\nA project diagnostic with No location\nActual:\n{0}",
FormatDiagnostics(analyzer, actual)));
}
}
else
{
VerifyDiagnosticLocation(analyzer, actual, actual.Location, expected.Locations.First());
var additionalLocations = actual.AdditionalLocations.ToArray();
if (additionalLocations.Length != expected.Locations.Length - 1)
{
Assert.True(false,
string.Format("Expected {0} additional locations but got {1} for Diagnostic:\r\n {2}\r\n",
expected.Locations.Length - 1, additionalLocations.Length,
FormatDiagnostics(analyzer, actual)));
}
for (int j = 0; j < additionalLocations.Length; ++j)
{
VerifyDiagnosticLocation(analyzer, actual, additionalLocations[j], expected.Locations[j + 1]);
}
}
if (actual.Id != expected.Id)
{
Assert.True(false,
string.Format("Expected diagnostic id to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
expected.Id, actual.Id, FormatDiagnostics(analyzer, actual)));
}
if (actual.Severity != expected.Severity)
{
Assert.True(false,
string.Format("Expected diagnostic severity to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
expected.Severity, actual.Severity, FormatDiagnostics(analyzer, actual)));
}
if (actual.GetMessage() != expected.Message)
{
Assert.True(false,
string.Format("Expected diagnostic message to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
expected.Message, actual.GetMessage(), FormatDiagnostics(analyzer, actual)));
}
}
}
/// <summary>
/// Helper method to VerifyDiagnosticResult that checks the location of a diagnostic and compares it with the location in the expected DiagnosticResult.
/// </summary>
/// <param name="analyzer">The analyzer that was being run on the sources</param>
/// <param name="diagnostic">The diagnostic that was found in the code</param>
/// <param name="actual">The Location of the Diagnostic found in the code</param>
/// <param name="expected">The DiagnosticResultLocation that should have been found</param>
private static void VerifyDiagnosticLocation(DiagnosticAnalyzer analyzer, Diagnostic diagnostic, Location actual, DiagnosticResultLocation expected)
{
var actualSpan = actual.GetLineSpan();
Assert.True(actualSpan.Path == expected.Path || (actualSpan.Path != null && actualSpan.Path.Contains("Test0.") && expected.Path.Contains("Test.")),
string.Format("Expected diagnostic to be in file \"{0}\" was actually in file \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
expected.Path, actualSpan.Path, FormatDiagnostics(analyzer, diagnostic)));
var actualLinePosition = actualSpan.StartLinePosition;
// Only check line position if there is an actual line in the real diagnostic
if (actualLinePosition.Line > 0)
{
if (actualLinePosition.Line + 1 != expected.Line)
{
Assert.True(false,
string.Format("Expected diagnostic to be on line \"{0}\" was actually on line \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
expected.Line, actualLinePosition.Line + 1, FormatDiagnostics(analyzer, diagnostic)));
}
}
// Only check column position if there is an actual column position in the real diagnostic
if (actualLinePosition.Character > 0)
{
if (actualLinePosition.Character + 1 != expected.Column)
{
Assert.True(false,
string.Format("Expected diagnostic to start at column \"{0}\" was actually at column \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
expected.Column, actualLinePosition.Character + 1, FormatDiagnostics(analyzer, diagnostic)));
}
}
}
#endregion
#region Formatting Diagnostics
/// <summary>
/// Helper method to format a Diagnostic into an easily readable string
/// </summary>
/// <param name="analyzer">The analyzer that this verifier tests</param>
/// <param name="diagnostics">The Diagnostics to be formatted</param>
/// <returns>The Diagnostics formatted as a string</returns>
private static string FormatDiagnostics(DiagnosticAnalyzer analyzer, params Diagnostic[] diagnostics)
{
var builder = new StringBuilder();
for (int i = 0; i < diagnostics.Length; ++i)
{
builder.AppendLine("// " + diagnostics[i].ToString());
var analyzerType = analyzer.GetType();
var rules = analyzer.SupportedDiagnostics;
foreach (var rule in rules)
{
if (rule != null && rule.Id == diagnostics[i].Id)
{
var location = diagnostics[i].Location;
if (location == Location.None)
{
builder.AppendFormat("GetGlobalResult({0}.{1})", analyzerType.Name, rule.Id);
}
else
{
Assert.True(location.IsInSource,
$"Test base does not currently handle diagnostics in metadata locations. Diagnostic in metadata: {diagnostics[i]}\r\n");
string resultMethodName = diagnostics[i].Location.SourceTree.FilePath.EndsWith(".cs") ? "GetCSharpResultAt" : "GetBasicResultAt";
var linePosition = diagnostics[i].Location.GetLineSpan().StartLinePosition;
builder.AppendFormat("{0}({1}, {2}, {3}.{4})",
resultMethodName,
linePosition.Line + 1,
linePosition.Character + 1,
analyzerType.Name,
rule.Id);
}
if (i != diagnostics.Length - 1)
{
builder.Append(',');
}
builder.AppendLine();
break;
}
}
}
return builder.ToString();
}
#endregion
}
}