diff --git a/Source/Csla.Analyzers/Csla.Analyzers.IntegrationTests/Csla.Analyzers.IntegrationTests/BusyProperties.cs b/Source/Csla.Analyzers/Csla.Analyzers.IntegrationTests/Csla.Analyzers.IntegrationTests/BusyProperties.cs new file mode 100644 index 0000000000..4c1dbf9bf2 --- /dev/null +++ b/Source/Csla.Analyzers/Csla.Analyzers.IntegrationTests/Csla.Analyzers.IntegrationTests/BusyProperties.cs @@ -0,0 +1,20 @@ +using System; + +namespace Csla.Analyzers.IntegrationTests +{ + [Serializable] + public sealed class BusyProperties + : BusinessBase + { + public static readonly PropertyInfo IdProperty = RegisterProperty(c => c.Id); + public int Id + { + get + { + var x = 42; + return ReadProperty(IdProperty) + x; + } + private set { LoadProperty(IdProperty, value); } + } + } +} diff --git a/Source/Csla.Analyzers/Csla.Analyzers.IntegrationTests/Csla.Analyzers.IntegrationTests/OperationReturnValues.cs b/Source/Csla.Analyzers/Csla.Analyzers.IntegrationTests/Csla.Analyzers.IntegrationTests/OperationReturnValues.cs new file mode 100644 index 0000000000..33b5897966 --- /dev/null +++ b/Source/Csla.Analyzers/Csla.Analyzers.IntegrationTests/Csla.Analyzers.IntegrationTests/OperationReturnValues.cs @@ -0,0 +1,20 @@ +using System; +using System.Threading.Tasks; + +namespace Csla.Analyzers.IntegrationTests +{ + [Serializable] + public sealed class OperationReturnValues + : BusinessBase + { + private void Foo() { } + private void DataPortal_Fetch(Guid id) { } + private Task DataPortal_Fetch(int id) => Task.CompletedTask; + private string DataPortal_Fetch() => string.Empty; + } + + public sealed class OperationReturnValuesNotCsla + { + private string DataPortal_Fetch() => string.Empty; + } +} \ No newline at end of file diff --git a/Source/Csla.Analyzers/Csla.Analyzers.IntegrationTests/Csla.Analyzers.IntegrationTests/PublicForInterface.cs b/Source/Csla.Analyzers/Csla.Analyzers.IntegrationTests/Csla.Analyzers.IntegrationTests/PublicForInterface.cs new file mode 100644 index 0000000000..c83f2cc70a --- /dev/null +++ b/Source/Csla.Analyzers/Csla.Analyzers.IntegrationTests/Csla.Analyzers.IntegrationTests/PublicForInterface.cs @@ -0,0 +1,10 @@ +using Csla.Core; + +namespace Csla.Analyzers.IntegrationTests +{ + public interface PublicForInterface + : IBusinessObject + { + void DataPortal_Fetch(); + } +} diff --git a/Source/Csla.Analyzers/Csla.Analyzers.IntegrationTests/Csla.Analyzers.IntegrationTests/PublicOperation.cs b/Source/Csla.Analyzers/Csla.Analyzers.IntegrationTests/Csla.Analyzers.IntegrationTests/PublicOperation.cs new file mode 100644 index 0000000000..29c2a2ea16 --- /dev/null +++ b/Source/Csla.Analyzers/Csla.Analyzers.IntegrationTests/Csla.Analyzers.IntegrationTests/PublicOperation.cs @@ -0,0 +1,11 @@ +using System; + +namespace Csla.Analyzers.IntegrationTests +{ + [Serializable] + public sealed class PublicOperation + : BusinessBase + { + public void DataPortal_Fetch() { } + } +} diff --git a/Source/Csla.Analyzers/Csla.Analyzers.Tests/FindOperationsWithIncorrectReturnTypeResolveCorrectTypeCodeFixTests.cs b/Source/Csla.Analyzers/Csla.Analyzers.Tests/FindOperationsWithIncorrectReturnTypeResolveCorrectTypeCodeFixTests.cs new file mode 100644 index 0000000000..b145fa9fbf --- /dev/null +++ b/Source/Csla.Analyzers/Csla.Analyzers.Tests/FindOperationsWithIncorrectReturnTypeResolveCorrectTypeCodeFixTests.cs @@ -0,0 +1,125 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Csla.Analyzers.Tests +{ + [TestClass] + public sealed class FindOperationsWithIncorrectReturnTypeResolveCorrectTypeCodeFixTests + { + [TestMethod] + public void VerifyGetFixableDiagnosticIds() + { + var fix = new FindOperationsWithIncorrectReturnTypeResolveCorrectTypeCodeFix(); + var ids = fix.FixableDiagnosticIds.ToList(); + + Assert.AreEqual(1, ids.Count, nameof(ids.Count)); + Assert.AreEqual(ids[0], Constants.AnalyzerIdentifiers.FindOperationsWithIncorrectReturnTypes, + nameof(Constants.AnalyzerIdentifiers.FindOperationsWithIncorrectReturnTypes)); + } + + [TestMethod] + public async Task VerifyGetFixesWhenChangingToVoid() + { + var code = +@"using Csla; + +public class A : BusinessBase +{ + public string DataPortal_Fetch() { } +}"; + + var document = TestHelpers.Create(code); + var tree = await document.GetSyntaxTreeAsync(); + var diagnostics = await TestHelpers.GetDiagnosticsAsync(code, new FindOperationsWithIncorrectReturnTypesAnalyzer()); + var sourceSpan = diagnostics[0].Location.SourceSpan; + + var actions = new List(); + var codeActionRegistration = new Action>( + (a, _) => { actions.Add(a); }); + + var fix = new FindOperationsWithIncorrectReturnTypeResolveCorrectTypeCodeFix(); + var codeFixContext = new CodeFixContext(document, diagnostics[0], + codeActionRegistration, new CancellationToken(false)); + await fix.RegisterCodeFixesAsync(codeFixContext); + + Assert.AreEqual(1, actions.Count, nameof(actions.Count)); + + await TestHelpers.VerifyActionAsync(actions, + FindOperationsWithIncorrectReturnTypeResolveCorrectTypeCodeFixConstants.ChangeReturnTypeToVoidDescription, document, + tree, new[] { "void" }); + } + + [TestMethod] + public async Task VerifyGetFixesWhenChangingToTask() + { + var code = +@"using Csla; +using System.Threading.Tasks; + +public class A : BusinessBase +{ + public async string DataPortal_Fetch() { } +}"; + + var document = TestHelpers.Create(code); + var tree = await document.GetSyntaxTreeAsync(); + var diagnostics = await TestHelpers.GetDiagnosticsAsync(code, new FindOperationsWithIncorrectReturnTypesAnalyzer()); + var sourceSpan = diagnostics[0].Location.SourceSpan; + + var actions = new List(); + var codeActionRegistration = new Action>( + (a, _) => { actions.Add(a); }); + + var fix = new FindOperationsWithIncorrectReturnTypeResolveCorrectTypeCodeFix(); + var codeFixContext = new CodeFixContext(document, diagnostics[0], + codeActionRegistration, new CancellationToken(false)); + await fix.RegisterCodeFixesAsync(codeFixContext); + + Assert.AreEqual(1, actions.Count, nameof(actions.Count)); + + await TestHelpers.VerifyActionAsync(actions, + FindOperationsWithIncorrectReturnTypeResolveCorrectTypeCodeFixConstants.ChangeReturnTypeToTaskDescription, document, + tree, new[] { "Task" }); + } + + [TestMethod] + public async Task VerifyGetFixesWhenChangingToTaskAndUsingDoesNotExist() + { + var code = +@"using Csla; + +public class A : BusinessBase +{ + public async string DataPortal_Fetch() { } +}"; + + var document = TestHelpers.Create(code); + var tree = await document.GetSyntaxTreeAsync(); + var diagnostics = await TestHelpers.GetDiagnosticsAsync(code, new FindOperationsWithIncorrectReturnTypesAnalyzer()); + var sourceSpan = diagnostics[0].Location.SourceSpan; + + var actions = new List(); + var codeActionRegistration = new Action>( + (a, _) => { actions.Add(a); }); + + var fix = new FindOperationsWithIncorrectReturnTypeResolveCorrectTypeCodeFix(); + var codeFixContext = new CodeFixContext(document, diagnostics[0], + codeActionRegistration, new CancellationToken(false)); + await fix.RegisterCodeFixesAsync(codeFixContext); + + Assert.AreEqual(1, actions.Count, nameof(actions.Count)); + + await TestHelpers.VerifyActionAsync(actions, + FindOperationsWithIncorrectReturnTypeResolveCorrectTypeCodeFixConstants.ChangeReturnTypeToTaskDescription, document, + tree, new[] { "using System.Threading.Tasks;", "Task" }); + } + } +} diff --git a/Source/Csla.Analyzers/Csla.Analyzers.Tests/FindOperationsWithIncorrectReturnTypesAnalyzerTests.cs b/Source/Csla.Analyzers/Csla.Analyzers.Tests/FindOperationsWithIncorrectReturnTypesAnalyzerTests.cs new file mode 100644 index 0000000000..70a43525de --- /dev/null +++ b/Source/Csla.Analyzers/Csla.Analyzers.Tests/FindOperationsWithIncorrectReturnTypesAnalyzerTests.cs @@ -0,0 +1,99 @@ +using System; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Csla.Analyzers.Tests +{ + [TestClass] + public sealed class FindOperationsWithIncorrectReturnTypesAnalyzerTests + { + [TestMethod] + public void VerifySupportedDiagnostics() + { + var analyzer = new FindOperationsWithIncorrectReturnTypesAnalyzer(); + var diagnostics = analyzer.SupportedDiagnostics; + Assert.AreEqual(1, diagnostics.Length); + + var diagnostic = diagnostics[0]; + Assert.AreEqual(Constants.AnalyzerIdentifiers.FindOperationsWithIncorrectReturnTypes, diagnostic.Id, + nameof(DiagnosticDescriptor.Id)); + Assert.AreEqual(FindOperationsWithIncorrectReturnTypesAnalyzerConstants.Title, diagnostic.Title.ToString(), + nameof(DiagnosticDescriptor.Title)); + Assert.AreEqual(FindOperationsWithIncorrectReturnTypesAnalyzerConstants.Message, diagnostic.MessageFormat.ToString(), + nameof(DiagnosticDescriptor.MessageFormat)); + Assert.AreEqual(Constants.Categories.Design, diagnostic.Category, + nameof(DiagnosticDescriptor.Category)); + Assert.AreEqual(DiagnosticSeverity.Error, diagnostic.DefaultSeverity, + nameof(DiagnosticDescriptor.DefaultSeverity)); + Assert.AreEqual(HelpUrlBuilder.Build(Constants.AnalyzerIdentifiers.FindOperationsWithIncorrectReturnTypes, nameof(FindOperationsWithIncorrectReturnTypesAnalyzer)), + diagnostic.HelpLinkUri, + nameof(DiagnosticDescriptor.HelpLinkUri)); + } + + [TestMethod] + public async Task AnalyzeWithNotMobileObject() + { + var code = "public class A { }"; + await TestHelpers.RunAnalysisAsync( + code, Array.Empty()); + } + + [TestMethod] + public async Task AnalyzeWithMobileObjectAndMethodIsNotOperation() + { + var code = +@"using Csla; + +public class A : BusinessBase +{ + public void Foo() { } +}"; + await TestHelpers.RunAnalysisAsync( + code, Array.Empty()); + } + + [TestMethod] + public async Task AnalyzeWithMobileObjectAndMethodIsOperationReturningVoid() + { + var code = +@"using Csla; + +public class A : BusinessBase +{ + private void DataPortal_Fetch() { } +}"; + await TestHelpers.RunAnalysisAsync( + code, Array.Empty()); + } + + [TestMethod] + public async Task AnalyzeWithMobileObjectAndMethodIsOperationReturningTask() + { + var code = +@"using Csla; +using System.Threading.Tasks; + +public class A : BusinessBase +{ + private async Task DataPortal_Fetch() { } +}"; + await TestHelpers.RunAnalysisAsync( + code, Array.Empty()); + } + + [TestMethod] + public async Task AnalyzeWithMobileObjectAndMethodIsOperationReturningIncorrectType() + { + var code = +@"using Csla; + +public class A : BusinessBase +{ + private string DataPortal_Fetch() { } +}"; + await TestHelpers.RunAnalysisAsync( + code, new[] { Constants.AnalyzerIdentifiers.FindOperationsWithIncorrectReturnTypes }); + } + } +} diff --git a/Source/Csla.Analyzers/Csla.Analyzers/CheckConstructorsAnalyzer.cs b/Source/Csla.Analyzers/Csla.Analyzers/CheckConstructorsAnalyzer.cs index 3cf7fd42ed..d21b1c7a03 100644 --- a/Source/Csla.Analyzers/Csla.Analyzers/CheckConstructorsAnalyzer.cs +++ b/Source/Csla.Analyzers/Csla.Analyzers/CheckConstructorsAnalyzer.cs @@ -32,8 +32,12 @@ public sealed class CheckConstructorsAnalyzer publicNoArgumentConstructorIsMissingRule, constructorHasParametersRule); - public override void Initialize(AnalysisContext context) => + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); context.RegisterSyntaxNodeAction(AnalyzeClassDeclaration, SyntaxKind.ClassDeclaration); + } private static void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context) { diff --git a/Source/Csla.Analyzers/Csla.Analyzers/Constants.cs b/Source/Csla.Analyzers/Csla.Analyzers/Constants.cs index 752179d2c9..4bacbddfcf 100644 --- a/Source/Csla.Analyzers/Csla.Analyzers/Constants.cs +++ b/Source/Csla.Analyzers/Csla.Analyzers/Constants.cs @@ -8,13 +8,14 @@ public static class AnalyzerIdentifiers public const string IsOperationMethodPublic = "CSLA0002"; public const string PublicNoArgumentConstructorIsMissing = "CSLA0003"; public const string ConstructorHasParameters = "CSLA0004"; - public const string FindBusinessObjectCreation = "CSLA0011"; public const string FindSaveAssignmentIssue = "CSLA0005"; public const string FindSaveAsyncAssignmentIssue = "CSLA0006"; public const string OnlyUseCslaPropertyMethodsInGetSetRule = "CSLA0007"; public const string EvaluateManagedBackingFields = "CSLA0008"; public const string IsOperationMethodPublicForInterface = "CSLA0009"; public const string FindOperationsWithNonSerializableArguments = "CSLA0010"; + public const string FindBusinessObjectCreation = "CSLA0011"; + public const string FindOperationsWithIncorrectReturnTypes = "CSLA0012"; } public static class Categories diff --git a/Source/Csla.Analyzers/Csla.Analyzers/EvaluateManagedBackingFieldsAnalayzer.cs b/Source/Csla.Analyzers/Csla.Analyzers/EvaluateManagedBackingFieldsAnalayzer.cs index 9c48590e9d..9219957bfd 100644 --- a/Source/Csla.Analyzers/Csla.Analyzers/EvaluateManagedBackingFieldsAnalayzer.cs +++ b/Source/Csla.Analyzers/Csla.Analyzers/EvaluateManagedBackingFieldsAnalayzer.cs @@ -25,8 +25,12 @@ public sealed class EvaluateManagedBackingFieldsAnalayzer public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(mustBePublicStaticAndReadonlyRule); - public override void Initialize(AnalysisContext context) => + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); context.RegisterSyntaxNodeAction(AnalyzeFieldDeclaration, SyntaxKind.FieldDeclaration); + } private static void AnalyzeFieldDeclaration(SyntaxNodeAnalysisContext context) { diff --git a/Source/Csla.Analyzers/Csla.Analyzers/EvaluateManagedBackingFieldsWalker.cs b/Source/Csla.Analyzers/Csla.Analyzers/EvaluateManagedBackingFieldsWalker.cs index a89378369f..4386a9c15c 100644 --- a/Source/Csla.Analyzers/Csla.Analyzers/EvaluateManagedBackingFieldsWalker.cs +++ b/Source/Csla.Analyzers/Csla.Analyzers/EvaluateManagedBackingFieldsWalker.cs @@ -23,7 +23,7 @@ public override void VisitInvocationExpression(InvocationExpressionSyntax node) foreach (var argument in node.ArgumentList.Arguments) { var argumentSymbol = Model.GetSymbolInfo(argument.Expression).Symbol; - UsesField = argumentSymbol != null && argumentSymbol == FieldSymbol; + UsesField = argumentSymbol != null && Equals(argumentSymbol, FieldSymbol); } } } diff --git a/Source/Csla.Analyzers/Csla.Analyzers/EvaluatePropertiesForSimplicityAnalyzer.cs b/Source/Csla.Analyzers/Csla.Analyzers/EvaluatePropertiesForSimplicityAnalyzer.cs index c95158c45b..37e33a2bc6 100644 --- a/Source/Csla.Analyzers/Csla.Analyzers/EvaluatePropertiesForSimplicityAnalyzer.cs +++ b/Source/Csla.Analyzers/Csla.Analyzers/EvaluatePropertiesForSimplicityAnalyzer.cs @@ -22,8 +22,12 @@ public sealed class EvaluatePropertiesForSimplicityAnalyzer public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(onlyUseCslaPropertyMethodsInGetSetRule); - public override void Initialize(AnalysisContext context) => + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); context.RegisterSyntaxNodeAction(AnalyzePropertyDeclaration, SyntaxKind.PropertyDeclaration); + } private static void AnalyzePropertyDeclaration(SyntaxNodeAnalysisContext context) { diff --git a/Source/Csla.Analyzers/Csla.Analyzers/Extensions/ITypeSymbolExtensions.cs b/Source/Csla.Analyzers/Csla.Analyzers/Extensions/ITypeSymbolExtensions.cs index 267428dbce..b86f2e3447 100644 --- a/Source/Csla.Analyzers/Csla.Analyzers/Extensions/ITypeSymbolExtensions.cs +++ b/Source/Csla.Analyzers/Csla.Analyzers/Extensions/ITypeSymbolExtensions.cs @@ -1,9 +1,5 @@ using Microsoft.CodeAnalysis; -using System.Collections.Generic; -using System.Collections.Immutable; using System.Linq; -using System.Reflection; -using static System.Reflection.IntrospectionExtensions; namespace Csla.Analyzers.Extensions { @@ -62,21 +58,6 @@ internal static bool IsEditableStereotype(this ITypeSymbol @this) @this.BaseType.IsEditableStereotype()); } - private static ImmutableArray GetAllProperties(this ITypeSymbol @this) - { - var properties = new List(); - - var type = @this.GetType().GetTypeInfo(); - - while(type != null) - { - properties.AddRange(type.DeclaredProperties); - type = type.BaseType?.GetTypeInfo(); - } - - return properties.ToImmutableArray(); - } - internal static bool IsStereotype(this ITypeSymbol @this) { return @this != null && diff --git a/Source/Csla.Analyzers/Csla.Analyzers/FindBusinessObjectCreationAnalyzer.cs b/Source/Csla.Analyzers/Csla.Analyzers/FindBusinessObjectCreationAnalyzer.cs index df454034a1..dc44c61973 100644 --- a/Source/Csla.Analyzers/Csla.Analyzers/FindBusinessObjectCreationAnalyzer.cs +++ b/Source/Csla.Analyzers/Csla.Analyzers/FindBusinessObjectCreationAnalyzer.cs @@ -23,8 +23,12 @@ public sealed class FindBusinessObjectCreationAnalyzer public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(objectCreatedRule); - public override void Initialize(AnalysisContext context) => + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); context.RegisterSyntaxNodeAction(AnalyzeObjectCreationExpression, SyntaxKind.ObjectCreationExpression); + } private static void AnalyzeObjectCreationExpression(SyntaxNodeAnalysisContext context) { diff --git a/Source/Csla.Analyzers/Csla.Analyzers/FindOperationsWithIncorrectReturnTypeResolveCorrectTypeCodeFix.cs b/Source/Csla.Analyzers/Csla.Analyzers/FindOperationsWithIncorrectReturnTypeResolveCorrectTypeCodeFix.cs new file mode 100644 index 0000000000..05e40c6810 --- /dev/null +++ b/Source/Csla.Analyzers/Csla.Analyzers/FindOperationsWithIncorrectReturnTypeResolveCorrectTypeCodeFix.cs @@ -0,0 +1,73 @@ +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CodeActions; +using static Csla.Analyzers.Extensions.SyntaxNodeExtensions; + +namespace Csla.Analyzers +{ + [ExportCodeFixProvider(LanguageNames.CSharp)] + [Shared] + public sealed class FindOperationsWithIncorrectReturnTypeResolveCorrectTypeCodeFix + : CodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(Constants.AnalyzerIdentifiers.FindOperationsWithIncorrectReturnTypes); + + public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + + context.CancellationToken.ThrowIfCancellationRequested(); + + var diagnostic = context.Diagnostics.First(); + var methodNode = root.FindNode(diagnostic.Location.SourceSpan) as MethodDeclarationSyntax; + + context.CancellationToken.ThrowIfCancellationRequested(); + + await AddCodeFixAsync(context, root, diagnostic, methodNode); + } + + private static async Task AddCodeFixAsync(CodeFixContext context, SyntaxNode root, + Diagnostic diagnostic, MethodDeclarationSyntax methodNode) + { + var model = await context.Document.GetSemanticModelAsync(context.CancellationToken); + var methodSymbol = model.GetDeclaredSymbol(methodNode); + + if(methodSymbol.IsAsync) + { + var newRoot = root.ReplaceNode(methodNode.ReturnType, + SyntaxFactory.IdentifierName(typeof(Task).Name)); + + if (!root.HasUsing(FindOperationsWithIncorrectReturnTypeResolveCorrectTypeCodeFixConstants.SystemThreadingTasksNamespace)) + { + newRoot = (newRoot as CompilationUnitSyntax).AddUsings( + SyntaxFactory.UsingDirective(SyntaxFactory.ParseName( + FindOperationsWithIncorrectReturnTypeResolveCorrectTypeCodeFixConstants.SystemThreadingTasksNamespace))); + } + + context.RegisterCodeFix( + CodeAction.Create( + FindOperationsWithIncorrectReturnTypeResolveCorrectTypeCodeFixConstants.ChangeReturnTypeToTaskDescription, + _ => Task.FromResult(context.Document.WithSyntaxRoot(newRoot)), + FindOperationsWithIncorrectReturnTypeResolveCorrectTypeCodeFixConstants.ChangeReturnTypeToTaskDescription), diagnostic); + } + else + { + var newRoot = root.ReplaceNode(methodNode.ReturnType, + SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.VoidKeyword))); + context.RegisterCodeFix( + CodeAction.Create( + FindOperationsWithIncorrectReturnTypeResolveCorrectTypeCodeFixConstants.ChangeReturnTypeToVoidDescription, + _ => Task.FromResult(context.Document.WithSyntaxRoot(newRoot)), + FindOperationsWithIncorrectReturnTypeResolveCorrectTypeCodeFixConstants.ChangeReturnTypeToVoidDescription), diagnostic); + } + } + } +} \ No newline at end of file diff --git a/Source/Csla.Analyzers/Csla.Analyzers/FindOperationsWithIncorrectReturnTypesAnalyzer.cs b/Source/Csla.Analyzers/Csla.Analyzers/FindOperationsWithIncorrectReturnTypesAnalyzer.cs new file mode 100644 index 0000000000..f5ca668df8 --- /dev/null +++ b/Source/Csla.Analyzers/Csla.Analyzers/FindOperationsWithIncorrectReturnTypesAnalyzer.cs @@ -0,0 +1,50 @@ +using Csla.Analyzers.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Immutable; +using System.Threading.Tasks; + +namespace Csla.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class FindOperationsWithIncorrectReturnTypesAnalyzer + : DiagnosticAnalyzer + { + private static readonly DiagnosticDescriptor shouldOnlyReturnVoidOrTaskRule = + new DiagnosticDescriptor( + Constants.AnalyzerIdentifiers.FindOperationsWithIncorrectReturnTypes, FindOperationsWithIncorrectReturnTypesAnalyzerConstants.Title, + FindOperationsWithIncorrectReturnTypesAnalyzerConstants.Message, Constants.Categories.Design, + DiagnosticSeverity.Error, true, + helpLinkUri: HelpUrlBuilder.Build( + Constants.AnalyzerIdentifiers.FindOperationsWithIncorrectReturnTypes, nameof(FindOperationsWithIncorrectReturnTypesAnalyzer))); + + public override ImmutableArray SupportedDiagnostics => + ImmutableArray.Create(shouldOnlyReturnVoidOrTaskRule); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeMethodDeclaration, SyntaxKind.MethodDeclaration); + } + + private static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context) + { + var methodNode = (MethodDeclarationSyntax)context.Node; + var methodSymbol = context.SemanticModel.GetDeclaredSymbol(methodNode); + var typeSymbol = methodSymbol.ContainingType; + + if (typeSymbol.IsStereotype() && methodSymbol.IsDataPortalOperation()) + { + var taskType = context.Compilation.GetTypeByMetadataName(typeof(Task).FullName); + if(!(methodSymbol.ReturnsVoid || Equals(methodSymbol.ReturnType, taskType))) + { + context.ReportDiagnostic(Diagnostic.Create( + shouldOnlyReturnVoidOrTaskRule, methodSymbol.Locations[0])); + } + } + } + } +} \ No newline at end of file diff --git a/Source/Csla.Analyzers/Csla.Analyzers/FindOperationsWithIncorrectReturnTypesAnalyzerConstants.cs b/Source/Csla.Analyzers/Csla.Analyzers/FindOperationsWithIncorrectReturnTypesAnalyzerConstants.cs new file mode 100644 index 0000000000..f3ad8b32ca --- /dev/null +++ b/Source/Csla.Analyzers/Csla.Analyzers/FindOperationsWithIncorrectReturnTypesAnalyzerConstants.cs @@ -0,0 +1,18 @@ +using System.Threading.Tasks; + +namespace Csla.Analyzers +{ + public static class FindOperationsWithIncorrectReturnTypesAnalyzerConstants + { + public const string Title = "Find Operations With Incorrect Return Types"; + public const string IdentifierText = "FindOperationsWithIncorrectReturnTypes"; + public const string Message = "The return type from an operation should be either void or Task."; + } + + public static class FindOperationsWithIncorrectReturnTypeResolveCorrectTypeCodeFixConstants + { + public const string ChangeReturnTypeToTaskDescription = "Change return type to Task"; + public const string ChangeReturnTypeToVoidDescription = "Change return type to void"; + public static readonly string SystemThreadingTasksNamespace = typeof(Task).Namespace; + } +} diff --git a/Source/Csla.Analyzers/Csla.Analyzers/FindOperationsWithNonSerializableArgumentsAnalyzer.cs b/Source/Csla.Analyzers/Csla.Analyzers/FindOperationsWithNonSerializableArgumentsAnalyzer.cs index fc9506e871..09f3980682 100644 --- a/Source/Csla.Analyzers/Csla.Analyzers/FindOperationsWithNonSerializableArgumentsAnalyzer.cs +++ b/Source/Csla.Analyzers/Csla.Analyzers/FindOperationsWithNonSerializableArgumentsAnalyzer.cs @@ -22,8 +22,12 @@ public sealed class FindOperationsWithNonSerializableArgumentsAnalyzer public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(shouldUseSerializableTypesRule); - public override void Initialize(AnalysisContext context) => + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); context.RegisterSyntaxNodeAction(AnalyzeMethodDeclaration, SyntaxKind.MethodDeclaration); + } private static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context) { diff --git a/Source/Csla.Analyzers/Csla.Analyzers/FindSaveAssignmentIssueAnalyzer.cs b/Source/Csla.Analyzers/Csla.Analyzers/FindSaveAssignmentIssueAnalyzer.cs index 743fa7dca3..d755c5e009 100644 --- a/Source/Csla.Analyzers/Csla.Analyzers/FindSaveAssignmentIssueAnalyzer.cs +++ b/Source/Csla.Analyzers/Csla.Analyzers/FindSaveAssignmentIssueAnalyzer.cs @@ -30,8 +30,12 @@ public sealed class FindSaveAssignmentIssueAnalyzer public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(saveResultIsNotAssignedRule, saveAsyncResultIsNotAssignedRule); - public override void Initialize(AnalysisContext context) => + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression); + } private static void AnalyzeInvocation(SyntaxNodeAnalysisContext context) { diff --git a/Source/Csla.Analyzers/Csla.Analyzers/IsBusinessObjectSerializableAnalyzer.cs b/Source/Csla.Analyzers/Csla.Analyzers/IsBusinessObjectSerializableAnalyzer.cs index bd02a8cdcd..6489411899 100644 --- a/Source/Csla.Analyzers/Csla.Analyzers/IsBusinessObjectSerializableAnalyzer.cs +++ b/Source/Csla.Analyzers/Csla.Analyzers/IsBusinessObjectSerializableAnalyzer.cs @@ -21,8 +21,12 @@ public sealed class IsBusinessObjectSerializableAnalyzer public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(makeSerializableRule); - public override void Initialize(AnalysisContext context) => + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); context.RegisterSyntaxNodeAction(AnalyzeClassDeclaration, SyntaxKind.ClassDeclaration); + } private static void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context) { diff --git a/Source/Csla.Analyzers/Csla.Analyzers/IsOperationMethodPublicAnalyzer.cs b/Source/Csla.Analyzers/Csla.Analyzers/IsOperationMethodPublicAnalyzer.cs index f9cbc9359c..005b8ff601 100644 --- a/Source/Csla.Analyzers/Csla.Analyzers/IsOperationMethodPublicAnalyzer.cs +++ b/Source/Csla.Analyzers/Csla.Analyzers/IsOperationMethodPublicAnalyzer.cs @@ -32,8 +32,12 @@ public sealed class IsOperationMethodPublicAnalyzer public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(makeNonPublicRule, makeNonPublicForInterfaceRule); - public override void Initialize(AnalysisContext context) => + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); context.RegisterSyntaxNodeAction(AnalyzeMethodDeclaration, SyntaxKind.MethodDeclaration); + } private static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context) { diff --git a/docs/analyzers/CSLA0012-FindOperationsWithIncorrectReturnTypesAnalyzer.md b/docs/analyzers/CSLA0012-FindOperationsWithIncorrectReturnTypesAnalyzer.md new file mode 100644 index 0000000000..8054918ee0 --- /dev/null +++ b/docs/analyzers/CSLA0012-FindOperationsWithIncorrectReturnTypesAnalyzer.md @@ -0,0 +1,19 @@ +# The return type from an operation should be either void or Task. + +## Issue +This analyzer is tripped if an operation with a business object returns anything else other than `void` or `Task`: + +``` +using Csla; +using System; + +[Serializable] +public class Customer + : BusinessBase +{ + private string DataPortal_Fetch(int id) => string.Empty; +} +``` + +## Code Fix +A code fix will show up to change the return type to `void` if the method isn't asynchronous, and `Task` if it is. It will also add the `System.Threading.Tasks` `using` statement in the code file if needed. It won't try to change the method to remove any `return` statements - that's up to the developer to fix. \ No newline at end of file