diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f732b25c..6bf3a97ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,12 @@ # [vNext] ## Improvements: +* Make it so that the [Binding] attribute is no longer required on types declaring binding methods (#1044) ## Bug fixes: * Fix: Formatters incorrectly handle Unicode text file content of attachments. -*Contributors of this release (in alphabetical order):* @clrudolphi +*Contributors of this release (in alphabetical order):* @clrudolphi, @Code-Grump # v3.3.3 - 2026-01-27 diff --git a/Reqnroll/BindingSkeletons/DefaultSkeletonTemplates.sftemplate b/Reqnroll/BindingSkeletons/DefaultSkeletonTemplates.sftemplate index 38a07d895..fd5e32204 100644 --- a/Reqnroll/BindingSkeletons/DefaultSkeletonTemplates.sftemplate +++ b/Reqnroll/BindingSkeletons/DefaultSkeletonTemplates.sftemplate @@ -4,7 +4,6 @@ using Reqnroll; namespace {namespace} { - [Binding] public class {className} { private readonly IReqnrollOutputHelper _outputHelper; diff --git a/Reqnroll/Bindings/Discovery/BindingSourceProcessor.cs b/Reqnroll/Bindings/Discovery/BindingSourceProcessor.cs index 79dd1b770..d5b9a1928 100644 --- a/Reqnroll/Bindings/Discovery/BindingSourceProcessor.cs +++ b/Reqnroll/Bindings/Discovery/BindingSourceProcessor.cs @@ -29,14 +29,9 @@ public bool CanProcessMethodAttribute(string attributeTypeName) return true; } - private static bool IsPotentialBindingClass(IEnumerable attributeTypeNames) - { - return attributeTypeNames.Any(attr => attr.EndsWith($".{nameof(BindingAttribute)}", StringComparison.InvariantCulture)); - } - public bool PreFilterType(IEnumerable attributeTypeNames) { - return IsPotentialBindingClass(attributeTypeNames); + return true; } public bool ProcessType(BindingSourceType bindingSourceType) @@ -80,7 +75,7 @@ private IEnumerable GetScopes(IEnumerable private bool IsBindingType(BindingSourceType bindingSourceType) { - return bindingSourceType.Attributes.Any(attr => typeof(BindingAttribute).IsAssignableFrom(attr.AttributeType)); + return bindingSourceType.MethodAttributes != null && bindingSourceType.MethodAttributes.Any(); } private bool IsStepDefinitionAttribute(BindingSourceAttribute attribute) diff --git a/Reqnroll/Bindings/Discovery/BindingSourceType.cs b/Reqnroll/Bindings/Discovery/BindingSourceType.cs index e0cd3b987..e2137f296 100644 --- a/Reqnroll/Bindings/Discovery/BindingSourceType.cs +++ b/Reqnroll/Bindings/Discovery/BindingSourceType.cs @@ -14,6 +14,8 @@ public class BindingSourceType public BindingSourceAttribute[] Attributes { get; set; } + public BindingSourceAttribute[] MethodAttributes { get; set; } + public override string ToString() => BindingType?.ToString() ?? ""; } } \ No newline at end of file diff --git a/Reqnroll/Bindings/Discovery/RuntimeBindingRegistryBuilder.cs b/Reqnroll/Bindings/Discovery/RuntimeBindingRegistryBuilder.cs index 9eb43fb5a..f4edb17c7 100644 --- a/Reqnroll/Bindings/Discovery/RuntimeBindingRegistryBuilder.cs +++ b/Reqnroll/Bindings/Discovery/RuntimeBindingRegistryBuilder.cs @@ -90,12 +90,34 @@ internal bool BuildBindingsFromType(Type type) if (!_bindingSourceProcessor.PreFilterType(filteredAttributes.Select(attr => attr.GetType().FullName))) return false; - var bindingSourceType = CreateBindingSourceType(type, filteredAttributes); + var candiateMethods = type.GetMethods( + BindingFlags.Static | + BindingFlags.Instance | + BindingFlags.Public | + BindingFlags.NonPublic); + + List methodAttributes; + try + { + methodAttributes = [.. + candiateMethods.SelectMany(method => method.GetCustomAttributes(true).Cast())]; + } + catch (TypeLoadException ex) + { + _bindingSourceProcessor.RegisterTypeLoadError($"Could not load attributes for type '{type.FullName}': {ex}"); + // When the type attributes cannot be loaded, the type cannot be processed anyway so we can return with false here to avoid reporting further errors. + return false; + } + + var filteredMethodAttributes = methodAttributes + .Where(attr => _bindingSourceProcessor.CanProcessMethodAttribute(attr.GetType().FullName)); + + var bindingSourceType = CreateBindingSourceType(type, filteredAttributes, filteredMethodAttributes); if (!_bindingSourceProcessor.ProcessType(bindingSourceType)) return false; - foreach (var methodInfo in type.GetMethods(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + foreach (var methodInfo in candiateMethods) { _bindingSourceProcessor.ProcessMethod(CreateBindingSourceMethod(methodInfo)); } @@ -131,7 +153,10 @@ private IBindingType CreateBindingType(Type type) return new RuntimeBindingType(type); } - private BindingSourceType CreateBindingSourceType(Type type, IEnumerable filteredAttributes) + private BindingSourceType CreateBindingSourceType( + Type type, + IEnumerable filteredAttributes, + IEnumerable filteredMethodAttributes) { return new BindingSourceType { @@ -141,7 +166,8 @@ private BindingSourceType CreateBindingSourceType(Type type, IEnumerableThe type of the binding class. /// The binding class instance /// - /// The binding classes are the classes with the [Binding] attribute, that might - /// contain step definitions, hooks or step argument transformations. The method + /// The binding classes are the classes with which contain one or more methods decorated with binding attributes. + /// This includes step definitions, hooks or step argument transformations. The method /// is called when any binding method needs to be called. /// public object GetBindingInstance(Type bindingType) diff --git a/Tests/Reqnroll.Formatters.Tests/CucumberMessagesBasicTests.cs b/Tests/Reqnroll.Formatters.Tests/CucumberMessagesBasicTests.cs index 84532fec6..8f8ca8ea6 100644 --- a/Tests/Reqnroll.Formatters.Tests/CucumberMessagesBasicTests.cs +++ b/Tests/Reqnroll.Formatters.Tests/CucumberMessagesBasicTests.cs @@ -117,7 +117,6 @@ When I eat 5 cukes namespace CucumberMessages.CompatibilityTests.Smoke { - [Binding] internal class Hooks { public Hooks() diff --git a/Tests/Reqnroll.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs b/Tests/Reqnroll.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs index 4b77d1970..696893e33 100644 --- a/Tests/Reqnroll.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs +++ b/Tests/Reqnroll.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionIntegrationTests.cs @@ -168,33 +168,25 @@ private async Task PerformStepExecution(string methodName, strin transformations?.ToList().ForEach(binding => bindingRegistry.RegisterStepArgumentTransformationBinding(binding)); onBindingRegistryPreparation?.Invoke(bindingRegistry); + var givenAttribute = new BindingSourceAttribute + { + AttributeType = new RuntimeBindingType(typeof(GivenAttribute)), + AttributeValues = [ new BindingSourceAttributeValueProvider(expression) ] + }; + var bindingSourceMethod = new BindingSourceMethod { BindingMethod = new RuntimeBindingMethod(typeof(SampleBindings).GetMethod(methodName)), IsPublic = true, - Attributes = new[] - { - new BindingSourceAttribute - { - AttributeType = new RuntimeBindingType(typeof(GivenAttribute)), - AttributeValues = new IBindingSourceAttributeValueProvider[] - { - new BindingSourceAttributeValueProvider(expression) - } - } - } + Attributes = [ givenAttribute ] }; + bindingSourceProcessor.ProcessType( new BindingSourceType { BindingType = new RuntimeBindingType(typeof(SampleBindings)), - Attributes = new[] - { - new BindingSourceAttribute - { - AttributeType = new RuntimeBindingType(typeof(BindingAttribute)) - } - }, + Attributes = [], + MethodAttributes = [ givenAttribute ], IsPublic = true, IsClass = true }); diff --git a/Tests/Reqnroll.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistryTests.cs b/Tests/Reqnroll.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistryTests.cs index 8ebae36e0..48ba527f6 100644 --- a/Tests/Reqnroll.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistryTests.cs +++ b/Tests/Reqnroll.RuntimeTests/Bindings/CucumberExpressions/CucumberExpressionParameterTypeRegistryTests.cs @@ -123,33 +123,26 @@ private static void SetupBoundMethod(string expression, IBindingRegistry binding { bindingRegistry.RegisterStepArgumentTransformationBinding(transformation); } + + var givenAttribute = new BindingSourceAttribute + { + AttributeType = new RuntimeBindingType(typeof(GivenAttribute)), + AttributeValues = [new BindingSourceAttributeValueProvider(expression)] + }; + var bindingSourceMethod = new BindingSourceMethod { BindingMethod = new RuntimeBindingMethod(testType.GetMethod(methodName)), IsPublic = true, - Attributes = new[] - { - new BindingSourceAttribute - { - AttributeType = new RuntimeBindingType(typeof(GivenAttribute)), - AttributeValues = new IBindingSourceAttributeValueProvider[] - { - new BindingSourceAttributeValueProvider(expression) - } - } - } + Attributes = [givenAttribute] }; + bindingSourceProcessor.ProcessType( new BindingSourceType { BindingType = new RuntimeBindingType(typeof(CucumberExpressionIntegrationTests.SampleBindings)), - Attributes = new[] - { - new BindingSourceAttribute - { - AttributeType = new RuntimeBindingType(typeof(BindingAttribute)) - } - }, + Attributes = [], + MethodAttributes = [givenAttribute], IsPublic = true, IsClass = true }); diff --git a/Tests/Reqnroll.RuntimeTests/Bindings/Discovery/BindingSourceProcessorTests.cs b/Tests/Reqnroll.RuntimeTests/Bindings/Discovery/BindingSourceProcessorTests.cs index f795a843a..0aedf345d 100644 --- a/Tests/Reqnroll.RuntimeTests/Bindings/Discovery/BindingSourceProcessorTests.cs +++ b/Tests/Reqnroll.RuntimeTests/Bindings/Discovery/BindingSourceProcessorTests.cs @@ -22,8 +22,8 @@ public void ProcessTypeAndMethod_InVisualStudioExtension_ShouldFindBinding() //ARRANGE var sut = CreateBindingSourceProcessor(); - var bindingSourceType = CreateSyntheticBindingSourceType(); var bindingSourceMethod = CreateSyntheticStepDefBindingSourceMethod(); + var bindingSourceType = CreateSyntheticBindingSourceType(bindingSourceMethod); //ACT sut.ProcessType(bindingSourceType).Should().BeTrue(); @@ -62,15 +62,13 @@ private BindingSourceMethod CreateSyntheticHookBindingSourceMethod(HookType hook }; } - private BindingSourceType CreateSyntheticBindingSourceType() + private BindingSourceType CreateSyntheticBindingSourceType(BindingSourceMethod bindingSourceMethod) { var bindingSourceType = new BindingSourceType { IsClass = true, - Attributes = new[] - { - CreateBindingSourceAttribute("BindingAttribute", "Reqnroll.BindingAttribute") - }, + Attributes = [], + MethodAttributes = bindingSourceMethod?.Attributes ?? [] }; return bindingSourceType; } @@ -112,7 +110,7 @@ public async Task Async_void_binding_methods_are_not_supported() var bindingSourceMethod = CreateBindingSourceMethod(typeof(StepDefClassWithAsyncVoid), nameof(StepDefClassWithAsyncVoid.AsyncVoidStepDef), CreateBindingSourceAttribute("GivenAttribute", "Reqnroll.GivenAttribute").WithValue("an authenticated user")); - sut.ProcessType(CreateSyntheticBindingSourceType()).Should().BeTrue(); + sut.ProcessType(CreateSyntheticBindingSourceType(bindingSourceMethod)).Should().BeTrue(); sut.ProcessMethod(bindingSourceMethod); sut.BuildingCompleted(); @@ -126,7 +124,9 @@ public void Binding_type_errors_should_be_captured(bool isClass, bool isGenericT { var sut = CreateBindingSourceProcessor(); - var bindingSourceType = CreateSyntheticBindingSourceType(); + var bindingSourceMethod = CreateSyntheticStepDefBindingSourceMethod(); + var bindingSourceType = CreateSyntheticBindingSourceType(bindingSourceMethod); + // make it invalid bindingSourceType.IsClass = isClass; bindingSourceType.IsGenericTypeDefinition = isGenericTypeDefinition; @@ -143,11 +143,12 @@ public void Binding_method_errors_should_be_captured(bool isClassAbstract, bool { var sut = CreateBindingSourceProcessor(); - var bindingSourceType = CreateSyntheticBindingSourceType(); - bindingSourceType.IsAbstract = isClassAbstract; var bindingSourceMethod = CreateSyntheticStepDefBindingSourceMethod(); bindingSourceMethod.IsStatic = isMethodStatic; + var bindingSourceType = CreateSyntheticBindingSourceType(bindingSourceMethod); + bindingSourceType.IsAbstract = isClassAbstract; + sut.ProcessType(bindingSourceType).Should().BeTrue(); sut.ProcessMethod(bindingSourceMethod); sut.BuildingCompleted(); @@ -164,10 +165,11 @@ public void Non_static_feature_and_test_run_hook_errors_should_be_captured(HookT { var sut = CreateBindingSourceProcessor(); - var bindingSourceType = CreateSyntheticBindingSourceType(); var bindingSourceMethod = CreateSyntheticHookBindingSourceMethod(hookType); bindingSourceMethod.IsStatic = false; + var bindingSourceType = CreateSyntheticBindingSourceType(bindingSourceMethod); + sut.ProcessType(bindingSourceType).Should().BeTrue(); sut.ProcessMethod(bindingSourceMethod); sut.BuildingCompleted(); diff --git a/Tests/Reqnroll.Specs/Features/Contexts/AccessingContexts.feature b/Tests/Reqnroll.Specs/Features/Contexts/AccessingContexts.feature index be580c6a1..1706a2b23 100644 --- a/Tests/Reqnroll.Specs/Features/Contexts/AccessingContexts.feature +++ b/Tests/Reqnroll.Specs/Features/Contexts/AccessingContexts.feature @@ -25,7 +25,6 @@ Scenario: Should be able to inject ScenarioContext using System; using Reqnroll; - [Binding] public class StepsWithScenarioContext { public StepsWithScenarioContext(ScenarioContext scenarioContext) @@ -54,7 +53,6 @@ Scenario: The same ScenarioContext should be inject in the same scenario using System; using Reqnroll; - [Binding] public class StepsWithScenarioContext { private readonly ScenarioContext scenarioContext; @@ -77,7 +75,6 @@ Scenario: The same ScenarioContext should be inject in the same scenario using System; using Reqnroll; - [Binding] public class AnotherStepsWithScenarioContext { private readonly ScenarioContext scenarioContext; @@ -112,7 +109,6 @@ Scenario: Different scenarios should have their own ScenarioContext injected using System; using Reqnroll; - [Binding] public class StepsWithScenarioContext { private readonly ScenarioContext scenarioContext; @@ -151,7 +147,6 @@ Scenario: Should be able to inject FeatureContext using System; using Reqnroll; - [Binding] public class StepsWithScenarioContext { public StepsWithScenarioContext(FeatureContext featureContext) @@ -180,7 +175,6 @@ Scenario: The same FeatureContext should be inject in the scenarios of the same using System; using Reqnroll; - [Binding] public class StepsWithFeatureContext { private readonly FeatureContext featureContext; @@ -203,7 +197,6 @@ Scenario: The same FeatureContext should be inject in the scenarios of the same using System; using Reqnroll; - [Binding] public class AnotherStepsWithFeatureContext { private readonly FeatureContext featureContext; @@ -243,7 +236,6 @@ Scenario: ScenarioContext can be accessed from Steps base class using System; using Reqnroll; - [Binding] public class StepsWithScenarioContext : Steps { [Given(@"I put something into the context")] @@ -258,7 +250,6 @@ Scenario: ScenarioContext can be accessed from Steps base class using System; using Reqnroll; - [Binding] public class AnotherStepsWithScenarioContext : Steps { [Then(@"something should be found in the context")] @@ -285,7 +276,6 @@ Scenario: FeatureContext can be accessed from Steps base class using System; using Reqnroll; - [Binding] public class StepsWithFeatureContext : Steps { [Given(@"I put something into the context")] @@ -300,7 +290,6 @@ Scenario: FeatureContext can be accessed from Steps base class using System; using Reqnroll; - [Binding] public class AnotherStepsWithFeatureContext : Steps { [Then(@"something should be found in the context")] @@ -332,7 +321,6 @@ Scenario: StepContext can be accessed from Steps base class using System; using Reqnroll; - [Binding] public class MySteps : Steps { [When(@"I do something")] @@ -358,7 +346,6 @@ Scenario: StepContext can be accessed from the ScenarioContext using System; using Reqnroll; - [Binding] public class StepsWithScenarioContext { private readonly ScenarioContext scenarioContext; diff --git a/Tests/TestProjectGenerator/Reqnroll.TestProjectGenerator/Factories/BindingsGenerator/CSharpBindingsGenerator.cs b/Tests/TestProjectGenerator/Reqnroll.TestProjectGenerator/Factories/BindingsGenerator/CSharpBindingsGenerator.cs index da28d5d24..0c6a000d9 100644 --- a/Tests/TestProjectGenerator/Reqnroll.TestProjectGenerator/Factories/BindingsGenerator/CSharpBindingsGenerator.cs +++ b/Tests/TestProjectGenerator/Reqnroll.TestProjectGenerator/Factories/BindingsGenerator/CSharpBindingsGenerator.cs @@ -228,7 +228,6 @@ protected override string GetHookBindingClass( using System.Xml.Linq; using Reqnroll; - [Binding] {{scopeClassAttributes}} public class {{$"HooksClass_{Guid.NewGuid():N}"}} {