diff --git a/Source/Csla.Analyzers/Csla.Analyzers.IntegrationTests/Csla.Analyzers.IntegrationTests/CallingNewTests.cs b/Source/Csla.Analyzers/Csla.Analyzers.IntegrationTests/Csla.Analyzers.IntegrationTests/CallingNewTests.cs new file mode 100644 index 0000000000..0dfd8925e1 --- /dev/null +++ b/Source/Csla.Analyzers/Csla.Analyzers.IntegrationTests/Csla.Analyzers.IntegrationTests/CallingNewTests.cs @@ -0,0 +1,27 @@ +using System; +using Csla.Server; + +namespace Csla.Analyzers.IntegrationTests +{ + [Serializable] + public class A : BusinessBase { } + + public class B : ObjectFactory + { + public void Foo() + { + var a = new A(); + } + } + + public class C + { + public void Foo() + { + // This should be an error + // because you can't create a new business object + // outside of an ObjectFactory. + var a = new A(); + } + } +} diff --git a/Source/Csla.Analyzers/Csla.Analyzers.Tests/Extensions/ITypeSymbolExtensionsTests.cs b/Source/Csla.Analyzers/Csla.Analyzers.Tests/Extensions/ITypeSymbolExtensionsTests.cs index 7c15d37116..55ab452ac0 100644 --- a/Source/Csla.Analyzers/Csla.Analyzers.Tests/Extensions/ITypeSymbolExtensionsTests.cs +++ b/Source/Csla.Analyzers/Csla.Analyzers.Tests/Extensions/ITypeSymbolExtensionsTests.cs @@ -113,6 +113,23 @@ public async Task IsPrimitiveForDouble() [TestMethod] public void IsBusinessBaseWhenSymbolIsNull() => Assert.IsFalse((null as ITypeSymbol).IsBusinessBase()); + [TestMethod] + public async Task IsObjectFactoryForNotObjectFactoryType() + { + var code = "public class A { }"; + Assert.IsFalse((await GetTypeSymbolAsync(code, "A")).IsObjectFactory()); + } + + [TestMethod] + public async Task IsObjectFactoryForObjectFactoryType() + { + var code = +@"using Csla.Server; + +public class A : ObjectFactory { }"; + Assert.IsTrue((await GetTypeSymbolAsync(code, "A")).IsObjectFactory()); + } + [TestMethod] public async Task IsIPropertyInfoWhenSymbolDoesNotDeriveFromIPropertyInfo() { diff --git a/Source/Csla.Analyzers/Csla.Analyzers.Tests/FindBusinessObjectCreationAnalyzerTests.cs b/Source/Csla.Analyzers/Csla.Analyzers.Tests/FindBusinessObjectCreationAnalyzerTests.cs new file mode 100644 index 0000000000..66c3611a3e --- /dev/null +++ b/Source/Csla.Analyzers/Csla.Analyzers.Tests/FindBusinessObjectCreationAnalyzerTests.cs @@ -0,0 +1,89 @@ +using Microsoft.CodeAnalysis; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Threading.Tasks; + +namespace Csla.Analyzers.Tests +{ + [TestClass] + + public sealed class FindBusinessObjectCreationAnalyzerTests + { + [TestMethod] + public void VerifySupportedDiagnostics() + { + var analyzer = new FindBusinessObjectCreationAnalyzer(); + var diagnostics = analyzer.SupportedDiagnostics; + Assert.AreEqual(1, diagnostics.Length); + + var diagnostic = diagnostics[0]; + Assert.AreEqual(Constants.AnalyzerIdentifiers.FindBusinessObjectCreation, diagnostic.Id, + nameof(DiagnosticDescriptor.Id)); + Assert.AreEqual(FindBusinessObjectCreationConstants.Title, diagnostic.Title.ToString(), + nameof(DiagnosticDescriptor.Title)); + Assert.AreEqual(FindBusinessObjectCreationConstants.Message, diagnostic.MessageFormat.ToString(), + nameof(DiagnosticDescriptor.MessageFormat)); + Assert.AreEqual(Constants.Categories.Usage, diagnostic.Category, + nameof(DiagnosticDescriptor.Category)); + Assert.AreEqual(DiagnosticSeverity.Error, diagnostic.DefaultSeverity, + nameof(DiagnosticDescriptor.DefaultSeverity)); + } + + [TestMethod] + public async Task AnalyzeWhenConstructorIsNotOnBusinessObject() + { + var code = +@"public class A { } + + public class B + { + void Foo() + { + var a = new A(); + } + }"; + await TestHelpers.RunAnalysisAsync( + code, Array.Empty()); + } + + [TestMethod] + public async Task AnalyzeWhenConstructorIsOnBusinessObjectWithinObjectFactory() + { + var code = +@"using Csla; +using Csla.Server; + +public class A : BusinessBase { } + +public class B : ObjectFactory +{ + void Foo() + { + var a = new A(); + } +}"; + await TestHelpers.RunAnalysisAsync( + code, Array.Empty()); + } + + [TestMethod] + public async Task AnalyzeWhenConstructorIsOnBusinessObjectOutsideOfObjectFactory() + { + var code = +@"using Csla; +using Csla.Server; + +public class A : BusinessBase { } + +public class B +{ + void Foo() + { + var a = new A(); + } +}"; + await TestHelpers.RunAnalysisAsync( + code, new[] { Constants.AnalyzerIdentifiers.FindBusinessObjectCreation }); + } + } +} diff --git a/Source/Csla.Analyzers/Csla.Analyzers/CheckConstructorsAnalyzerConstants.cs b/Source/Csla.Analyzers/Csla.Analyzers/CheckConstructorsAnalyzerConstants.cs index f3bd51a234..d6d9225c51 100644 --- a/Source/Csla.Analyzers/Csla.Analyzers/CheckConstructorsAnalyzerConstants.cs +++ b/Source/Csla.Analyzers/Csla.Analyzers/CheckConstructorsAnalyzerConstants.cs @@ -15,6 +15,13 @@ public static class ConstructorHasParametersConstants public const string Message = "CSLA business objects should not have public constructors with parameters."; } + public static class FindBusinessObjectCreationConstants + { + public const string Title = "Find CSLA Business Objects That Are Created Outside of a ObjectFactory"; + public const string IdentifierText = "BusinessObjectCreated"; + public const string Message = "CSLA business objects should not be created outside of a ObjectFactory instance."; + } + public static class CheckConstructorsAnalyzerPublicConstructorCodeFixConstants { public const string AddPublicConstructorDescription = "Add public constructor with no arguments"; diff --git a/Source/Csla.Analyzers/Csla.Analyzers/Constants.cs b/Source/Csla.Analyzers/Csla.Analyzers/Constants.cs index 71b72b26b1..752179d2c9 100644 --- a/Source/Csla.Analyzers/Csla.Analyzers/Constants.cs +++ b/Source/Csla.Analyzers/Csla.Analyzers/Constants.cs @@ -8,6 +8,7 @@ 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"; diff --git a/Source/Csla.Analyzers/Csla.Analyzers/CslaMemberConstants.cs b/Source/Csla.Analyzers/Csla.Analyzers/CslaMemberConstants.cs index b3f52a8444..0e23dc8fe7 100644 --- a/Source/Csla.Analyzers/Csla.Analyzers/CslaMemberConstants.cs +++ b/Source/Csla.Analyzers/Csla.Analyzers/CslaMemberConstants.cs @@ -46,6 +46,7 @@ public static class Types public const string IMobileObject = "IMobileObject"; public const string IPropertyInfo = "IPropertyInfo"; public const string ManagedObjectBase = "ManagedObjectBase"; + public const string ObjectFactoryBase = "ObjectFactory"; public const string ReadOnlyBase = "ReadOnlyBase"; } diff --git a/Source/Csla.Analyzers/Csla.Analyzers/Extensions/ITypeSymbolExtensions.cs b/Source/Csla.Analyzers/Csla.Analyzers/Extensions/ITypeSymbolExtensions.cs index 8fa70851f1..267428dbce 100644 --- a/Source/Csla.Analyzers/Csla.Analyzers/Extensions/ITypeSymbolExtensions.cs +++ b/Source/Csla.Analyzers/Csla.Analyzers/Extensions/ITypeSymbolExtensions.cs @@ -9,6 +9,14 @@ namespace Csla.Analyzers.Extensions { internal static class ITypeSymbolExtensions { + internal static bool IsObjectFactory(this ITypeSymbol @this) + { + return @this != null && + ((@this.Name == CslaMemberConstants.Types.ObjectFactoryBase && + @this.ContainingAssembly.Name == CslaMemberConstants.AssemblyName) || + @this.BaseType.IsObjectFactory()); + } + internal static bool IsBusinessBase(this ITypeSymbol @this) { return @this != null && diff --git a/Source/Csla.Analyzers/Csla.Analyzers/FindBusinessObjectCreationAnalyzer.cs b/Source/Csla.Analyzers/Csla.Analyzers/FindBusinessObjectCreationAnalyzer.cs new file mode 100644 index 0000000000..296860c4c1 --- /dev/null +++ b/Source/Csla.Analyzers/Csla.Analyzers/FindBusinessObjectCreationAnalyzer.cs @@ -0,0 +1,50 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Immutable; +using static Csla.Analyzers.Extensions.ITypeSymbolExtensions; +using static Csla.Analyzers.Extensions.SyntaxNodeExtensions; + +namespace Csla.Analyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public sealed class FindBusinessObjectCreationAnalyzer + : DiagnosticAnalyzer + { + private static readonly DiagnosticDescriptor objectCreatedRule = + new DiagnosticDescriptor( + Constants.AnalyzerIdentifiers.FindBusinessObjectCreation, FindBusinessObjectCreationConstants.Title, + FindBusinessObjectCreationConstants.Message, Constants.Categories.Usage, + DiagnosticSeverity.Error, true); + + public override ImmutableArray SupportedDiagnostics => + ImmutableArray.Create(objectCreatedRule); + + public override void Initialize(AnalysisContext context) => + context.RegisterSyntaxNodeAction(AnalyzeObjectCreationExpression, SyntaxKind.ObjectCreationExpression); + + private static void AnalyzeObjectCreationExpression(SyntaxNodeAnalysisContext context) + { + var constructorNode = (ObjectCreationExpressionSyntax)context.Node; + var constructorSymbol = context.SemanticModel.GetSymbolInfo(constructorNode).Symbol as IMethodSymbol; + var containingSymbol = constructorSymbol?.ContainingType; + + if(containingSymbol.IsStereotype()) + { + context.CancellationToken.ThrowIfCancellationRequested(); + var callerClassNode = constructorNode.FindParent(); + + if(callerClassNode != null) + { + var callerClassSymbol = context.SemanticModel.GetDeclaredSymbol(callerClassNode) as ITypeSymbol; + + if(!callerClassSymbol.IsObjectFactory()) + { + context.ReportDiagnostic(Diagnostic.Create(objectCreatedRule, constructorNode.GetLocation())); + } + } + } + } + } +} \ No newline at end of file