Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,6 @@ CSLA0019 | Usage | Error | FindRefAndOutParametersInOperationsAnalyzer
CSLA0020 | Usage | Warning | ObjectAuthorizationRulesAttributeMissing
CSLA0021 | Usage | Info | ObjectAuthorizationRulesPublic
CSLA0022 | Usage | Warning | ObjectAuthorizationRulesStatic
CSLA0023 | Refactoring | Info | EvaluateManagedBackingFieldsNameofAnalyzer
CSLA0023 | Refactoring | Info | EvaluateManagedBackingFieldsNameofAnalyzer
CSLA0024 | Usage | Warning | TypeWithOperationsShouldBePartialAnalyzer
CSLA0025 | Usage | Warning | UseGeneratedDataPortalExtensionAnalyzer
8 changes: 8 additions & 0 deletions Source/Csla.Analyzers/Csla.Analyzers/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,14 @@ public static class AnalyzerIdentifiers
///
/// </summary>
public const string EvaluateManagedBackingFieldsNameof = "CSLA0023";
/// <summary>
///
/// </summary>
public const string TypeWithOperationsShouldBePartial = "CSLA0024";
/// <summary>
///
/// </summary>
public const string UseGeneratedDataPortalExtension = "CSLA0025";

}

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions Source/Csla.Analyzers/Csla.Analyzers/Properties/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -279,4 +279,22 @@
<data name="ObjectAuthorizationRulesAttribute_RulesStaticMessage" xml:space="preserve">
<value>Authorization rules should be declared as static methods</value>
</data>
<data name="TypeWithOperationsShouldBePartial_Title" xml:space="preserve">
<value>Type with data portal operation methods should be partial</value>
</data>
<data name="TypeWithOperationsShouldBePartial_Message" xml:space="preserve">
<value>Type '{0}' has data portal operation methods and should be declared partial so the CSLA source generator can implement its operations interface</value>
</data>
<data name="TypeWithOperationsShouldBePartial_AddPartialDescription" xml:space="preserve">
<value>Make type partial</value>
</data>
<data name="UseGeneratedDataPortalExtension_Title" xml:space="preserve">
<value>Use generated data portal extension method</value>
</data>
<data name="UseGeneratedDataPortalExtension_Message" xml:space="preserve">
<value>Use the generated data portal extension method for '{0}' instead of calling '{1}' with untyped criteria</value>
</data>
<data name="UseGeneratedDataPortalExtension_UseExtensionDescription" xml:space="preserve">
<value>Use '{0}'</value>
</data>
</root>
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using System.Collections.Immutable;
using System.Composition;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Csla.Analyzers
{
/// <summary>
/// Adds the <c>partial</c> modifier to a type with data portal
/// operation methods and to all of its containing types.
/// </summary>
[ExportCodeFixProvider(LanguageNames.CSharp)]
[Shared]
public sealed class TypeWithOperationsShouldBePartialAddPartialCodeFix
: CodeFixProvider
{
/// <summary>
///
/// </summary>
public override ImmutableArray<string> FixableDiagnosticIds => [Constants.AnalyzerIdentifiers.TypeWithOperationsShouldBePartial];

/// <summary>
///
/// </summary>
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;

/// <summary>
///
/// </summary>
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
if (root is null)
{
return;
}

context.CancellationToken.ThrowIfCancellationRequested();

var diagnostic = context.Diagnostics.First();
var classNode = root.FindNode(diagnostic.Location.SourceSpan).FirstAncestorOrSelf<ClassDeclarationSyntax>();
if (classNode is null)
{
return;
}

var nonPartialNodes = TypeWithOperationsShouldBePartialAnalyzer.GetNonPartialTypeDeclarations(classNode);
if (nonPartialNodes.Count == 0)
{
return;
}

context.CancellationToken.ThrowIfCancellationRequested();

var newRoot = root.ReplaceNodes(nonPartialNodes, (_, rewritten) => AddPartialModifier(rewritten));
var description = TypeWithOperationsShouldBePartialAddPartialCodeFixConstants.AddPartialDescription;

context.RegisterCodeFix(
CodeAction.Create(description,
_ => Task.FromResult(context.Document.WithSyntaxRoot(newRoot)), description), diagnostic);
}

private static TypeDeclarationSyntax AddPartialModifier(TypeDeclarationSyntax node)
{
if (node.Modifiers.Count == 0)
{
// No modifiers: the partial keyword takes over the leading trivia
// (indentation, comments) of the type keyword.
var keyword = node.Keyword;
var partialToken = SyntaxFactory.Token(
keyword.LeadingTrivia, SyntaxKind.PartialKeyword, SyntaxFactory.TriviaList(SyntaxFactory.Space));
return node
.WithKeyword(keyword.WithLeadingTrivia(SyntaxFactory.TriviaList()))
.WithModifiers(SyntaxFactory.TokenList(partialToken));
}

// partial must be the last modifier, immediately before the type keyword.
var partialModifier = SyntaxFactory.Token(SyntaxKind.PartialKeyword)
.WithTrailingTrivia(SyntaxFactory.Space);
return node.WithModifiers(node.Modifiers.Add(partialModifier));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using System.Collections.Immutable;

namespace Csla.Analyzers
{
/// <summary>
/// Reports classes that declare data portal operation methods
/// (methods with <c>[Create]</c>, <c>[Fetch]</c>, etc.) when the
/// class or any of its containing types is not <c>partial</c>.
/// The CSLA source generator needs to add a partial declaration
/// to such types to implement their operations interface.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class TypeWithOperationsShouldBePartialAnalyzer
: DiagnosticAnalyzer
{
private static readonly string[] OperationAttributeMetadataNames =
[
"Csla.CreateAttribute",
"Csla.FetchAttribute",
"Csla.InsertAttribute",
"Csla.UpdateAttribute",
"Csla.ExecuteAttribute",
"Csla.DeleteAttribute",
"Csla.DeleteSelfAttribute",
"Csla.CreateChildAttribute",
"Csla.FetchChildAttribute",
"Csla.InsertChildAttribute",
"Csla.UpdateChildAttribute",
"Csla.DeleteSelfChildAttribute",
"Csla.ExecuteChildAttribute"
];

private static readonly DiagnosticDescriptor shouldBePartialRule =
new(
Constants.AnalyzerIdentifiers.TypeWithOperationsShouldBePartial, TypeWithOperationsShouldBePartialAnalyzerConstants.Title,
TypeWithOperationsShouldBePartialAnalyzerConstants.Message, Constants.Categories.Usage,
DiagnosticSeverity.Warning, true,
helpLinkUri: HelpUrlBuilder.Build(
Constants.AnalyzerIdentifiers.TypeWithOperationsShouldBePartial, nameof(TypeWithOperationsShouldBePartialAnalyzer)));

/// <summary>
///
/// </summary>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [shouldBePartialRule];

/// <summary>
///
/// </summary>
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterCompilationStartAction(compilationContext =>
{
var operationAttributes = new HashSet<INamedTypeSymbol>(SymbolEqualityComparer.Default);
foreach (var metadataName in OperationAttributeMetadataNames)
{
var attributeType = compilationContext.Compilation.GetTypeByMetadataName(metadataName);
if (attributeType is not null)
{
operationAttributes.Add(attributeType);
}
}

if (operationAttributes.Count == 0)
{
return;
}

compilationContext.RegisterSymbolAction(
symbolContext => AnalyzeNamedType(symbolContext, operationAttributes), SymbolKind.NamedType);
});
}

private static void AnalyzeNamedType(SymbolAnalysisContext context, HashSet<INamedTypeSymbol> operationAttributes)
{
var typeSymbol = (INamedTypeSymbol)context.Symbol;
if (typeSymbol.TypeKind != TypeKind.Class || typeSymbol.IsRecord)
{
return;
}

if (!HasOperationMethods(typeSymbol, operationAttributes))
{
return;
}

foreach (var reference in typeSymbol.DeclaringSyntaxReferences)
{
context.CancellationToken.ThrowIfCancellationRequested();

if (reference.GetSyntax(context.CancellationToken) is ClassDeclarationSyntax classNode &&
GetNonPartialTypeDeclarations(classNode).Count > 0)
{
context.ReportDiagnostic(Diagnostic.Create(
shouldBePartialRule, classNode.Identifier.GetLocation(), typeSymbol.Name));
return;
}
}
}

private static bool HasOperationMethods(INamedTypeSymbol typeSymbol, HashSet<INamedTypeSymbol> operationAttributes)
{
foreach (var member in typeSymbol.GetMembers())
{
if (member is IMethodSymbol method)
{
foreach (var attribute in method.GetAttributes())
{
if (attribute.AttributeClass is not null && operationAttributes.Contains(attribute.AttributeClass))
{
return true;
}
}
}
}

return false;
}

/// <summary>
/// Gets the given type declaration and all of its containing
/// type declarations that are not declared <c>partial</c>.
/// </summary>
internal static List<TypeDeclarationSyntax> GetNonPartialTypeDeclarations(TypeDeclarationSyntax typeNode)
{
var result = new List<TypeDeclarationSyntax>();
foreach (var node in typeNode.AncestorsAndSelf().OfType<TypeDeclarationSyntax>())
{
if (!node.Modifiers.Any(SyntaxKind.PartialKeyword))
{
result.Add(node);
}
}

return result;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Csla.Analyzers.Properties;
using Microsoft.CodeAnalysis;

namespace Csla.Analyzers
{
/// <summary>
///
/// </summary>
public static class TypeWithOperationsShouldBePartialAnalyzerConstants
{
/// <summary>
///
/// </summary>
public static readonly LocalizableResourceString Title = new(nameof(Resources.TypeWithOperationsShouldBePartial_Title), Resources.ResourceManager, typeof(Resources));
/// <summary>
///
/// </summary>
public static readonly LocalizableResourceString Message = new(nameof(Resources.TypeWithOperationsShouldBePartial_Message), Resources.ResourceManager, typeof(Resources));
}

/// <summary>
///
/// </summary>
public static class TypeWithOperationsShouldBePartialAddPartialCodeFixConstants
{
/// <summary>
///
/// </summary>
public static string AddPartialDescription => Resources.TypeWithOperationsShouldBePartial_AddPartialDescription;
}
}
Loading
Loading