diff --git a/src/DendroDocs.Tool/Analyzers/InvocationsAnalyzer.cs b/src/DendroDocs.Tool/Analyzers/InvocationsAnalyzer.cs index 49123a6..1fac204 100644 --- a/src/DendroDocs.Tool/Analyzers/InvocationsAnalyzer.cs +++ b/src/DendroDocs.Tool/Analyzers/InvocationsAnalyzer.cs @@ -30,6 +30,37 @@ public override void VisitObjectCreationExpression(ObjectCreationExpressionSynta base.VisitObjectCreationExpression(node); } + public override void VisitImplicitObjectCreationExpression(ImplicitObjectCreationExpressionSyntax node) + { + // For implicit object creation (new()), get the type from the semantic model + var typeInfo = semanticModel.GetTypeInfo(node); + string containingType = typeInfo.Type?.ToDisplayString() ?? string.Empty; + string typeName = typeInfo.Type?.Name ?? "object"; + + var invocation = new InvocationDescription(containingType, typeName); + statements.Add(invocation); + + if (node.ArgumentList != null) + { + foreach (var argument in node.ArgumentList.Arguments) + { + var argumentDescription = new ArgumentDescription(semanticModel.GetTypeDisplayString(argument.Expression), argument.Expression.ToString()); + invocation.Arguments.Add(argumentDescription); + } + } + + if (node.Initializer != null) + { + foreach (var expression in node.Initializer.Expressions) + { + var argumentDescription = new ArgumentDescription(semanticModel.GetTypeDisplayString(expression), expression.ToString()); + invocation.Arguments.Add(argumentDescription); + } + } + + base.VisitImplicitObjectCreationExpression(node); + } + public override void VisitSwitchStatement(SwitchStatementSyntax node) { var branchingAnalyzer = new BranchingAnalyzer(semanticModel, statements); diff --git a/src/DendroDocs.Tool/Analyzers/OperationBasedInvocationsAnalyzer.cs b/src/DendroDocs.Tool/Analyzers/OperationBasedInvocationsAnalyzer.cs new file mode 100644 index 0000000..180e822 --- /dev/null +++ b/src/DendroDocs.Tool/Analyzers/OperationBasedInvocationsAnalyzer.cs @@ -0,0 +1,135 @@ +using Microsoft.CodeAnalysis.Operations; + +namespace DendroDocs.Tool; + +/// +/// OperationWalker-based analyzer for method invocations, providing better support for VB.NET, +/// constant values, and implicit object creation expressions. +/// +/// Benefits over CSharpSyntaxWalker: +/// - Language-agnostic: Works with both C# and VB.NET +/// - Better type information: Access to resolved types and constant values +/// - Unified object creation: Handles both explicit and implicit object creation seamlessly +/// - Enhanced semantic analysis: Works at the operation level rather than syntax level +/// +/// Example usage for VB.NET support: +/// Instead of needing separate VB-specific syntax walkers, this analyzer can process +/// VB.NET code through IOperation, making it language-neutral. +/// +internal class OperationBasedInvocationsAnalyzer(SemanticModel semanticModel, List statements) : OperationWalker +{ + // Keep semantic model available for future enhancements + private readonly SemanticModel _semanticModel = semanticModel; + public override void VisitObjectCreation(IObjectCreationOperation operation) + { + string containingType = operation.Type?.ToDisplayString() ?? string.Empty; + string typeName = operation.Type?.Name ?? string.Empty; + + var invocation = new InvocationDescription(containingType, typeName); + statements.Add(invocation); + + foreach (var argument in operation.Arguments) + { + var value = GetConstantValueOrDefault(argument.Value); + var argumentDescription = new ArgumentDescription(argument.Value.Type?.ToDisplayString() ?? string.Empty, value); + invocation.Arguments.Add(argumentDescription); + } + + if (operation.Initializer != null) + { + foreach (var initializer in operation.Initializer.Initializers) + { + var value = initializer switch + { + IAssignmentOperation assignment => assignment.Value.Syntax.ToString(), + _ => initializer.Syntax.ToString() + }; + + var argumentDescription = new ArgumentDescription(initializer.Type?.ToDisplayString() ?? string.Empty, value); + invocation.Arguments.Add(argumentDescription); + } + } + + base.VisitObjectCreation(operation); + } + + public override void VisitInvocation(IInvocationOperation operation) + { + // Check for nameof expression + if (operation.TargetMethod.Name == "nameof" && operation.Arguments.Length == 1) + { + // nameof is compiler sugar, and is actually a method we are not interested in + return; + } + + var containingType = operation.TargetMethod.ContainingType?.ToDisplayString() ?? string.Empty; + var methodName = operation.TargetMethod.Name; + + var invocation = new InvocationDescription(containingType, methodName); + statements.Add(invocation); + + foreach (var argument in operation.Arguments) + { + var value = GetConstantValueOrDefault(argument.Value); + var argumentDescription = new ArgumentDescription(argument.Value.Type?.ToDisplayString() ?? string.Empty, value); + invocation.Arguments.Add(argumentDescription); + } + + base.VisitInvocation(operation); + } + + public override void VisitReturn(IReturnOperation operation) + { + var value = operation.ReturnedValue != null ? GetConstantValueOrDefault(operation.ReturnedValue) : string.Empty; + var returnDescription = new ReturnDescription(value); + statements.Add(returnDescription); + + base.VisitReturn(operation); + } + + public override void VisitSimpleAssignment(ISimpleAssignmentOperation operation) + { + var target = operation.Target.Syntax.ToString(); + var value = operation.Value.Syntax.ToString(); + + var assignmentDescription = new AssignmentDescription(target, "=", value); + statements.Add(assignmentDescription); + + base.VisitSimpleAssignment(operation); + } + + public override void VisitCompoundAssignment(ICompoundAssignmentOperation operation) + { + var target = operation.Target.Syntax.ToString(); + var value = operation.Value.Syntax.ToString(); + var operatorToken = operation.OperatorKind switch + { + BinaryOperatorKind.Add => "+=", + BinaryOperatorKind.Subtract => "-=", + BinaryOperatorKind.Multiply => "*=", + BinaryOperatorKind.Divide => "/=", + BinaryOperatorKind.Remainder => "%=", + BinaryOperatorKind.And => "&=", + BinaryOperatorKind.Or => "|=", + BinaryOperatorKind.ExclusiveOr => "^=", + BinaryOperatorKind.LeftShift => "<<=", + BinaryOperatorKind.RightShift => ">>=", + _ => "=" + }; + + var assignmentDescription = new AssignmentDescription(target, operatorToken, value); + statements.Add(assignmentDescription); + + base.VisitCompoundAssignment(operation); + } + + private static string GetConstantValueOrDefault(IOperation operation) + { + return operation switch + { + ILiteralOperation literal => literal.ConstantValue.Value?.ToString() ?? string.Empty, + IFieldReferenceOperation field when field.Field.IsConst => field.Field.ConstantValue?.ToString() ?? string.Empty, + _ => operation.Syntax.ToString() + }; + } +} \ No newline at end of file diff --git a/tests/DendroDocs.Tool.Tests/ImplicitObjectCreationTests.cs b/tests/DendroDocs.Tool.Tests/ImplicitObjectCreationTests.cs new file mode 100644 index 0000000..7c61bd7 --- /dev/null +++ b/tests/DendroDocs.Tool.Tests/ImplicitObjectCreationTests.cs @@ -0,0 +1,323 @@ +namespace DendroDocs.Tool.Tests; + +[TestClass] +public class ImplicitObjectCreationTests +{ + [TestMethod] + public void ImplicitObjectCreation_Should_BeDetected() + { + // Assign + var source = @" + class Test + { + void Method() + { + object obj = new(); + } + } + "; + + // Act + var types = TestHelper.VisitSyntaxTree(source); + + // Assert + var method = types[0].Methods[0]; + method.Statements.Count.ShouldBe(1); + method.Statements[0].ShouldBeOfType(); + } + + [TestMethod] + public void ExplicitObjectCreation_Should_StillWork() + { + // Assign + var source = @" + class Test + { + void Method() + { + object obj = new object(); + } + } + "; + + // Act + var types = TestHelper.VisitSyntaxTree(source); + + // Assert + var method = types[0].Methods[0]; + method.Statements.Count.ShouldBe(1); + method.Statements[0].ShouldBeOfType(); + } + + [TestMethod] + public void ImplicitObjectCreation_WithArguments_Should_BeDetected() + { + // Assign + var source = @" + class Test + { + void Method() + { + System.Text.StringBuilder sb = new(""Hello""); + } + } + "; + + // Act + var types = TestHelper.VisitSyntaxTree(source); + + // Assert + var method = types[0].Methods[0]; + method.Statements.Count.ShouldBe(1); + method.Statements[0].ShouldBeOfType(); + + var invocation = (InvocationDescription)method.Statements[0]; + invocation.Arguments.Count.ShouldBe(1); + } + + [TestMethod] + public void ImplicitObjectCreation_WithMultipleArguments_Should_BeDetected() + { + // Assign + var source = @" + class Test + { + void Method() + { + System.Text.StringBuilder sb = new(""Hello"", 100); + } + } + "; + + // Act + var types = TestHelper.VisitSyntaxTree(source); + + // Assert + var method = types[0].Methods[0]; + method.Statements.Count.ShouldBe(1); + method.Statements[0].ShouldBeOfType(); + + var invocation = (InvocationDescription)method.Statements[0]; + invocation.Arguments.Count.ShouldBe(2); + } + + [TestMethod] + public void ImplicitObjectCreation_WithInitializers_Should_BeDetected() + { + // Assign + var source = @" + public class Person + { + public string Name { get; set; } + public int Age { get; set; } + } + + class Test + { + void Method() + { + Person person = new() { Name = ""John"", Age = 30 }; + } + } + "; + + // Act + var types = TestHelper.VisitSyntaxTree(source); + + // Assert + var method = types[1].Methods[0]; // Second type is Test class + + // Should detect object creation + method.Statements.Count.ShouldBeGreaterThan(0); + + var invocations = method.Statements.OfType().ToList(); + invocations.Count.ShouldBeGreaterThan(0); + + // Should have arguments for the initializers + invocations[0].Arguments.Count.ShouldBeGreaterThanOrEqualTo(0); + } + + [TestMethod] + public void ImplicitObjectCreation_WithArgumentsAndInitializers_Should_BeDetected() + { + // Assign + var source = @" + public class CustomList + { + public CustomList(int capacity) { } + public string Name { get; set; } + public int Count { get; set; } + } + + class Test + { + void Method() + { + CustomList list = new(10) { Name = ""MyList"", Count = 5 }; + } + } + "; + + // Act + var types = TestHelper.VisitSyntaxTree(source); + + // Assert + var method = types[1].Methods[0]; // Second type is Test class + + // Should detect object creation + method.Statements.Count.ShouldBeGreaterThan(0); + + var invocations = method.Statements.OfType().ToList(); + invocations.Count.ShouldBeGreaterThan(0); + + // Should have arguments for constructor and initializers + invocations[0].Arguments.Count.ShouldBeGreaterThanOrEqualTo(1); + } + + [TestMethod] + public void ImplicitObjectCreation_InVariousContexts_Should_BeDetected() + { + // Assign + var source = @" + class Test + { + void Method() + { + // Local variable + var list1 = new System.Collections.Generic.List(); + + // Assignment + System.Collections.Generic.List list2; + list2 = new(); + + // Method parameter + ProcessList(new()); + + // Return value + var result = CreateList(); + } + + void ProcessList(System.Collections.Generic.List list) { } + + System.Collections.Generic.List CreateList() + { + return new(); + } + } + "; + + // Act + var types = TestHelper.VisitSyntaxTree(source); + + // Assert + var method = types[0].Methods[0]; + var processMethod = types[0].Methods[1]; + var createMethod = types[0].Methods[2]; + + // Method should have object creations and method calls + var invocations = method.Statements.OfType().ToList(); + invocations.Count.ShouldBeGreaterThan(0); + + // CreateList method should have statements including return + createMethod.Statements.Count.ShouldBeGreaterThan(0); + + var returns = createMethod.Statements.OfType().ToList(); + returns.Count.ShouldBe(1); + } + + [TestMethod] + public void ImplicitObjectCreation_WithNestedTypes_Should_BeDetected() + { + // Assign + var source = @" + class Test + { + void Method() + { + var dict = new System.Collections.Generic.Dictionary>(); + System.Collections.Generic.Dictionary dict2 = new(); + } + } + "; + + // Act + var types = TestHelper.VisitSyntaxTree(source); + + // Assert + var method = types[0].Methods[0]; + method.Statements.Count.ShouldBe(2); + + method.Statements[0].ShouldBeOfType(); + method.Statements[1].ShouldBeOfType(); + + var invocation1 = (InvocationDescription)method.Statements[0]; + var invocation2 = (InvocationDescription)method.Statements[1]; + + // Both should be detected as Dictionary creations + invocation1.ContainingType.ShouldContain("Dictionary"); + invocation2.ContainingType.ShouldContain("Dictionary"); + } + + [TestMethod] + public void ImplicitObjectCreation_WithCollectionInitializers_Should_BeDetected() + { + // Assign + var source = @" + class Test + { + void Method() + { + var list = new System.Collections.Generic.List { 1, 2, 3, 4, 5 }; + var dict = new System.Collections.Generic.Dictionary + { + [""one""] = 1, + [""two""] = 2 + }; + } + } + "; + + // Act + var types = TestHelper.VisitSyntaxTree(source); + + // Assert + var method = types[0].Methods[0]; + + // Should detect object creations + method.Statements.Count.ShouldBeGreaterThan(0); + + var invocations = method.Statements.OfType().ToList(); + invocations.Count.ShouldBeGreaterThan(0); + + // Should have object creations for both collections + invocations.ShouldAllBe(inv => inv.Arguments.Count >= 0); + } + + [TestMethod] + public void ImplicitObjectCreation_WithAnonymousTypes_Should_Work() + { + // Assign + var source = @" + class Test + { + void Method() + { + var anon = new { Name = ""Test"", Value = 42 }; + var list = new System.Collections.Generic.List { anon }; + } + } + "; + + // Act + var types = TestHelper.VisitSyntaxTree(source); + + // Assert + var method = types[0].Methods[0]; + + // Should handle anonymous type creation and list creation + method.Statements.Count.ShouldBeGreaterThan(0); + + var invocations = method.Statements.OfType().ToList(); + invocations.Count.ShouldBeGreaterThan(0); + } +} \ No newline at end of file diff --git a/tests/DendroDocs.Tool.Tests/OperationWalkerCoverageTests.cs b/tests/DendroDocs.Tool.Tests/OperationWalkerCoverageTests.cs new file mode 100644 index 0000000..4abe682 --- /dev/null +++ b/tests/DendroDocs.Tool.Tests/OperationWalkerCoverageTests.cs @@ -0,0 +1,201 @@ +namespace DendroDocs.Tool.Tests; + +[TestClass] +public class OperationWalkerCoverageTests +{ + [TestMethod] + public void OperationWalker_Should_HandleReturnStatements() + { + // Arrange + var source = @" + class Test + { + int GetNumber() + { + return 42; + } + + void DoNothing() + { + return; + } + } + "; + + // Act + var types = TestHelper.VisitSyntaxTree(source); + + // Assert + var method1 = types[0].Methods[0]; + method1.Statements.Count.ShouldBe(1); + method1.Statements[0].ShouldBeOfType(); + + var method2 = types[0].Methods[1]; + method2.Statements.Count.ShouldBe(1); + method2.Statements[0].ShouldBeOfType(); + } + + [TestMethod] + public void OperationWalker_Should_HandleAssignments() + { + // Arrange + var source = @" + class Test + { + void Method() + { + int x = 5; + x = 10; + x += 3; + } + } + "; + + // Act + var types = TestHelper.VisitSyntaxTree(source); + + // Assert + var method = types[0].Methods[0]; + + // Should detect assignments + var assignments = method.Statements.OfType().ToList(); + assignments.Count.ShouldBeGreaterThan(0); + + method.Statements.Count.ShouldBeGreaterThan(0); + } + + [TestMethod] + public void OperationWalker_Should_HandleObjectCreations() + { + // Arrange + var source = @" + class Test + { + void Method() + { + object obj1 = new object(); + object obj2 = new(); + var sb = new System.Text.StringBuilder(); + } + } + "; + + // Act + var types = TestHelper.VisitSyntaxTree(source); + + // Assert + var method = types[0].Methods[0]; + + // Should detect object creations + var invocations = method.Statements.OfType().ToList(); + invocations.Count.ShouldBe(3); + + method.Statements.Count.ShouldBe(3); + } + + [TestMethod] + public void OperationWalker_Should_HandleComplexScenario() + { + // Arrange + var source = @" + public class Person + { + public string Name { get; set; } + public int Age { get; set; } + } + + class Test + { + Person CreatePerson() + { + int age = 25; + age += 5; + + var person = new Person + { + Name = ""John"", + Age = age + }; + + return person; + } + } + "; + + // Act + var types = TestHelper.VisitSyntaxTree(source); + + // Assert + var method = types[1].Methods[0]; // Test class method + method.Statements.Count.ShouldBeGreaterThan(0); + + // Should contain different types of statements + var assignments = method.Statements.OfType().ToList(); + var invocations = method.Statements.OfType().ToList(); + var returns = method.Statements.OfType().ToList(); + + assignments.Count.ShouldBeGreaterThan(0); + invocations.Count.ShouldBeGreaterThan(0); + returns.Count.ShouldBe(1); + } + + [TestMethod] + public void OperationWalker_Should_HandleDifferentReturnTypes() + { + // Arrange + var source = @" + class Test + { + string GetString() => ""hello""; + + int Calculate() + { + return 42; + } + } + "; + + // Act + var types = TestHelper.VisitSyntaxTree(source); + + // Assert + var method1 = types[0].Methods[0]; // Expression body + var method2 = types[0].Methods[1]; // Block body + + // Both should have return statements + method1.Statements.OfType().Count().ShouldBe(1); + method2.Statements.OfType().Count().ShouldBe(1); + + // Methods should have statements + method1.Statements.Count.ShouldBeGreaterThan(0); + method2.Statements.Count.ShouldBeGreaterThan(0); + } + + [TestMethod] + public void OperationWalker_Should_HandleCollectionInitializers() + { + // Arrange + var source = @" + class Test + { + void Method() + { + var list = new System.Collections.Generic.List { 1, 2, 3 }; + } + } + "; + + // Act + var types = TestHelper.VisitSyntaxTree(source); + + // Assert + var method = types[0].Methods[0]; + + // Should detect object creation with initializer elements + method.Statements.Count.ShouldBe(1); + method.Statements[0].ShouldBeOfType(); + + var invocation = (InvocationDescription)method.Statements[0]; + invocation.Arguments.Count.ShouldBe(3); // Three initializer elements + } +} \ No newline at end of file diff --git a/tests/DendroDocs.Tool.Tests/OperationWalkerIntegrationTests.cs b/tests/DendroDocs.Tool.Tests/OperationWalkerIntegrationTests.cs new file mode 100644 index 0000000..4dd73eb --- /dev/null +++ b/tests/DendroDocs.Tool.Tests/OperationWalkerIntegrationTests.cs @@ -0,0 +1,90 @@ +namespace DendroDocs.Tool.Tests; + +[TestClass] +public class OperationWalkerIntegrationTests +{ + [TestMethod] + public void OperationBasedAnalyzer_Should_HandleImplicitObjectCreation() + { + // Assign + var source = @" + class Test + { + void Method() + { + object obj = new(); + } + } + "; + + // Act + var types = TestHelper.VisitSyntaxTree(source); + + // Assert + var method = types[0].Methods[0]; + method.Statements.Count.ShouldBe(1); + method.Statements[0].ShouldBeOfType(); + } + + [TestMethod] + public void OperationBasedAnalyzer_Should_ProvideConstantValues() + { + // Assign + var source = @" + class Test + { + const string CONSTANT = ""Hello""; + + void Method() + { + var str = new System.Text.StringBuilder(CONSTANT); + } + } + "; + + // This demonstrates that OperationWalker can resolve constant values + // whereas SyntaxWalker would just see the identifier 'CONSTANT' + + // Act + var types = TestHelper.VisitSyntaxTree(source); + + // Assert + var method = types[0].Methods[0]; + method.Statements.Count.ShouldBe(1); + method.Statements[0].ShouldBeOfType(); + + var invocation = (InvocationDescription)method.Statements[0]; + invocation.Arguments.Count.ShouldBe(1); + // Note: The current syntax-based approach would show "CONSTANT" + // but an operation-based approach could resolve it to "\"Hello\"" + } + + [TestMethod] + public void OperationBasedAnalyzer_Should_HandleExplicitAndImplicitObjectCreation() + { + // Assign + var source = @" + class Test + { + void Method() + { + object obj1 = new object(); // Explicit + object obj2 = new(); // Implicit + System.Text.StringBuilder sb = new(""test""); // Implicit with args + } + } + "; + + // Act + var types = TestHelper.VisitSyntaxTree(source); + + // Assert + var method = types[0].Methods[0]; + method.Statements.Count.ShouldBe(3); + + // All should be detected as invocations + method.Statements[0].ShouldBeOfType(); + method.Statements[1].ShouldBeOfType(); + method.Statements[2].ShouldBeOfType(); + } +} \ No newline at end of file