From 92e614000dcbe472c9b031049b28c38c8f814340 Mon Sep 17 00:00:00 2001 From: Evangelink Date: Fri, 12 Jun 2026 14:03:26 +0200 Subject: [PATCH 1/9] Add [Condition] attribute for static-member-based test conditions Adds a new `Microsoft.VisualStudio.TestTools.UnitTesting.ConditionAttribute` deriving from `ConditionBaseAttribute` that evaluates one or more static `bool` members (property, field, or parameterless method) referenced by `Type` + member name to decide whether a test class or test method runs. This mirrors the `[ConditionalFact]` / `[ConditionalTheory]` pattern from `Microsoft.DotNet.XUnitExtensions` used heavily by dotnet/runtime, dotnet/sdk, and dotnet/aspnetcore, removing the need to define a one-off `ConditionBaseAttribute` subclass per condition (e.g. `Is64BitProcessCondition`). Behavior: - Multiple member names within a single attribute are AND-combined. - Each attribute instance gets a unique `GroupName` derived from the type and members, so stacking multiple `[Condition]` attributes ANDs them across instances (matching xUnit's usage pattern). - `ConditionMode.Exclude` flips the semantics (skip when the condition holds). - Member resolution looks up public/non-public `static bool` property, field, or parameterless method (in that order). On any resolution failure (missing, non-bool, non-static, instance-only, or method with parameters), `IsConditionMet` throws `InvalidOperationException` so the test fails rather than silently skipping -- avoiding the classic "typo in member name silently disables the test" pitfall. Provides four CLS-friendly ctor overloads (`(Type, string)`, `(ConditionMode, Type, string)` plus `params`-array variants marked `[CLSCompliant(false)]`). Uses `DynamicallyAccessedMembers` annotations for trimming/AOT correctness. Targets all existing TFMs (netstandard2.0, net462, net8.0, net9.0). Adds 23 unit tests in `TestFramework.UnitTests` covering argument validation, member resolution variants (public/non-public, property/field/method), AND-semantics, `ConditionMode.Exclude`, error messages, and `GroupName` uniqueness. Fixes #9070. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TestMethod/ConditionAttribute.cs | 270 ++++++++++++++++++ .../PublicAPI/PublicAPI.Unshipped.txt | 9 + .../Attributes/ConditionAttributeTests.cs | 195 +++++++++++++ 3 files changed, 474 insertions(+) create mode 100644 src/TestFramework/TestFramework/Attributes/TestMethod/ConditionAttribute.cs create mode 100644 test/UnitTests/TestFramework.UnitTests/Attributes/ConditionAttributeTests.cs diff --git a/src/TestFramework/TestFramework/Attributes/TestMethod/ConditionAttribute.cs b/src/TestFramework/TestFramework/Attributes/TestMethod/ConditionAttribute.cs new file mode 100644 index 0000000000..532342e6f7 --- /dev/null +++ b/src/TestFramework/TestFramework/Attributes/TestMethod/ConditionAttribute.cs @@ -0,0 +1,270 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Conditionally runs or ignores a test class or test method based on the value of one or more +/// members (property, field, or parameterless method) +/// referenced by and member name. +/// +/// +/// +/// When multiple member names are supplied to a single attribute, their values are combined with +/// a logical AND: the attribute's is only if +/// every referenced member evaluates to . +/// +/// +/// Each instance forms its own , +/// so stacking multiple declarations on the same target is combined +/// with a logical AND, matching the typical [ConditionalFact] usage pattern in other test frameworks. +/// +/// +/// If the referenced member cannot be found, is not , does not return +/// , or (for methods) requires parameters, evaluating +/// throws an . This surfaces as a test error rather than a +/// silent skip so typos and refactors don't accidentally disable tests. +/// +/// +/// This attribute isn't inherited. Applying it to a base class will not affect derived classes. +/// +/// +/// +/// [TestMethod] +/// [Condition(typeof(Environment), nameof(Environment.Is64BitProcess))] +/// public void Only_Runs_On_64Bit() { } +/// +/// [TestMethod] +/// [Condition(typeof(PlatformDetection), +/// nameof(PlatformDetection.IsNotBrowser), +/// nameof(PlatformDetection.IsThreadingSupported))] +/// public void Requires_Threading_And_Not_Browser() { } +/// +/// [TestMethod] +/// [Condition(ConditionMode.Exclude, typeof(PlatformDetection), nameof(PlatformDetection.IsMonoRuntime))] +/// public void Does_Not_Run_On_Mono() { } +/// +/// +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false, AllowMultiple = true)] +public sealed class ConditionAttribute : ConditionBaseAttribute +{ + private const DynamicallyAccessedMemberTypes RequiredMembers = + DynamicallyAccessedMemberTypes.PublicProperties + | DynamicallyAccessedMemberTypes.NonPublicProperties + | DynamicallyAccessedMemberTypes.PublicFields + | DynamicallyAccessedMemberTypes.NonPublicFields + | DynamicallyAccessedMemberTypes.PublicMethods + | DynamicallyAccessedMemberTypes.NonPublicMethods; + + private readonly string[] _conditionMemberNames; + private string? _groupName; + + /// + /// Initializes a new instance of the class with + /// semantics: the test runs only when the referenced + /// member evaluates to . + /// + /// The type declaring the static member to evaluate. + /// + /// The name of the member (property, field, + /// or parameterless method) to evaluate. + /// + public ConditionAttribute( + [DynamicallyAccessedMembers(RequiredMembers)] Type conditionType, + string conditionMemberName) + : this(ConditionMode.Include, conditionType, conditionMemberName, additionalConditionMemberNames: []) + { + } + + /// + /// Initializes a new instance of the class with + /// semantics: the test runs only when every referenced + /// member evaluates to . + /// + /// The type declaring the static member(s) to evaluate. + /// + /// The name of the first member (property, field, + /// or parameterless method) to evaluate. + /// + /// + /// Additional member name(s) to evaluate. All + /// referenced members are AND-combined. + /// + [CLSCompliant(false)] + public ConditionAttribute( + [DynamicallyAccessedMembers(RequiredMembers)] Type conditionType, + string conditionMemberName, + params string[] additionalConditionMemberNames) + : this(ConditionMode.Include, conditionType, conditionMemberName, additionalConditionMemberNames) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Whether the test should be included (run when the condition is met) or excluded + /// (skipped when the condition is met). + /// + /// The type declaring the static member to evaluate. + /// + /// The name of the member (property, field, + /// or parameterless method) to evaluate. + /// + public ConditionAttribute( + ConditionMode mode, + [DynamicallyAccessedMembers(RequiredMembers)] Type conditionType, + string conditionMemberName) + : this(mode, conditionType, conditionMemberName, additionalConditionMemberNames: []) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Whether the test should be included (run when the condition is met) or excluded + /// (skipped when the condition is met). + /// + /// The type declaring the static member(s) to evaluate. + /// + /// The name of the first member (property, field, + /// or parameterless method) to evaluate. + /// + /// + /// Additional member name(s) to evaluate. All + /// referenced members are AND-combined. + /// + [CLSCompliant(false)] + public ConditionAttribute( + ConditionMode mode, + [DynamicallyAccessedMembers(RequiredMembers)] Type conditionType, + string conditionMemberName, + params string[] additionalConditionMemberNames) + : base(mode) + { + ConditionType = conditionType ?? throw new ArgumentNullException(nameof(conditionType)); + if (conditionMemberName is null) + { + throw new ArgumentNullException(nameof(conditionMemberName)); + } + + if (StringEx.IsNullOrWhiteSpace(conditionMemberName)) + { + throw new ArgumentException( + "Condition member name must not be empty or whitespace.", + nameof(conditionMemberName)); + } + + if (additionalConditionMemberNames is null || additionalConditionMemberNames.Length == 0) + { + _conditionMemberNames = [conditionMemberName]; + } + else + { + _conditionMemberNames = new string[additionalConditionMemberNames.Length + 1]; + _conditionMemberNames[0] = conditionMemberName; + for (int i = 0; i < additionalConditionMemberNames.Length; i++) + { + string name = additionalConditionMemberNames[i]; + if (StringEx.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException( + "Condition member names must not be null, empty, or whitespace.", + nameof(additionalConditionMemberNames)); + } + + _conditionMemberNames[i + 1] = name; + } + } + + IgnoreMessage = mode == ConditionMode.Include + ? $"Test is only supported when ({FormatMemberList()}) on '{conditionType.FullName ?? conditionType.Name}' is true." + : $"Test is not supported when ({FormatMemberList()}) on '{conditionType.FullName ?? conditionType.Name}' is true."; + } + + /// + /// Gets the type declaring the member(s) used to evaluate the condition. + /// + [DynamicallyAccessedMembers(RequiredMembers)] + public Type ConditionType { get; } + + /// + /// Gets the name(s) of the member(s) (property, + /// field, or parameterless method) on evaluated for this condition. + /// Multiple values are combined with a logical AND. + /// + public IReadOnlyList ConditionMemberNames => _conditionMemberNames; + + /// + /// + /// Each instance produces a group name derived from + /// and , so stacking multiple + /// declarations on the same target combines them with a + /// logical AND. + /// + public override string GroupName + => _groupName ??= $"{nameof(ConditionAttribute)}:{ConditionType.FullName ?? ConditionType.Name}:{string.Join("|", _conditionMemberNames)}"; + + /// + /// + /// All referenced members are evaluated in order and combined with a logical AND. Throws + /// if a member can't be resolved as a + /// or + /// property, field, or parameterless method. + /// + public override bool IsConditionMet + { + get + { + foreach (string memberName in _conditionMemberNames) + { + if (!EvaluateMember(memberName)) + { + return false; + } + } + + return true; + } + } + + private bool EvaluateMember(string memberName) + { + const BindingFlags Flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static; + string typeName = ConditionType.FullName ?? ConditionType.Name; + + PropertyInfo? property = ConditionType.GetProperty(memberName, Flags); + if (property is not null) + { + return property.PropertyType != typeof(bool) || property.GetMethod is null + ? throw new InvalidOperationException( + $"Member '{typeName}.{memberName}' must be a static bool readable property to be used with [Condition].") + : (bool)property.GetValue(null)!; + } + + FieldInfo? field = ConditionType.GetField(memberName, Flags); + if (field is not null) + { + return field.FieldType != typeof(bool) + ? throw new InvalidOperationException( + $"Member '{typeName}.{memberName}' must be a static bool field to be used with [Condition].") + : (bool)field.GetValue(null)!; + } + + MethodInfo? method = ConditionType.GetMethod(memberName, Flags, binder: null, types: Type.EmptyTypes, modifiers: null); + return method is null + ? throw new InvalidOperationException( + $"Could not find a static bool property, field, or parameterless method named '{memberName}' on type '{typeName}'.") + : method.ReturnType != typeof(bool) + ? throw new InvalidOperationException( + $"Member '{typeName}.{memberName}' must be a static parameterless bool method to be used with [Condition].") + : (bool)method.Invoke(null, null)!; + } + + private string FormatMemberList() + => _conditionMemberNames.Length == 1 + ? _conditionMemberNames[0] + : string.Join(" AND ", _conditionMemberNames); +} diff --git a/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt b/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt index ea572077cb..d8f68a20a1 100644 --- a/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt @@ -5,6 +5,15 @@ Microsoft.VisualStudio.TestTools.UnitTesting.AssemblyFixtureProviderAttribute.As Microsoft.VisualStudio.TestTools.UnitTesting.AssemblyFixtureProviderAttribute.FixtureType.get -> System.Type! Microsoft.VisualStudio.TestTools.UnitTesting.AssertFailedException.ActualText.get -> string? Microsoft.VisualStudio.TestTools.UnitTesting.AssertFailedException.ExpectedText.get -> string? +Microsoft.VisualStudio.TestTools.UnitTesting.ConditionAttribute +Microsoft.VisualStudio.TestTools.UnitTesting.ConditionAttribute.ConditionAttribute(Microsoft.VisualStudio.TestTools.UnitTesting.ConditionMode mode, System.Type! conditionType, string! conditionMemberName) -> void +Microsoft.VisualStudio.TestTools.UnitTesting.ConditionAttribute.ConditionAttribute(Microsoft.VisualStudio.TestTools.UnitTesting.ConditionMode mode, System.Type! conditionType, string! conditionMemberName, params string![]! additionalConditionMemberNames) -> void +Microsoft.VisualStudio.TestTools.UnitTesting.ConditionAttribute.ConditionAttribute(System.Type! conditionType, string! conditionMemberName) -> void +Microsoft.VisualStudio.TestTools.UnitTesting.ConditionAttribute.ConditionAttribute(System.Type! conditionType, string! conditionMemberName, params string![]! additionalConditionMemberNames) -> void +Microsoft.VisualStudio.TestTools.UnitTesting.ConditionAttribute.ConditionMemberNames.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.VisualStudio.TestTools.UnitTesting.ConditionAttribute.ConditionType.get -> System.Type! +override Microsoft.VisualStudio.TestTools.UnitTesting.ConditionAttribute.GroupName.get -> string! +override Microsoft.VisualStudio.TestTools.UnitTesting.ConditionAttribute.IsConditionMet.get -> bool Microsoft.VisualStudio.TestTools.UnitTesting.SequenceOrder Microsoft.VisualStudio.TestTools.UnitTesting.SequenceOrder.InAnyOrder = 1 -> Microsoft.VisualStudio.TestTools.UnitTesting.SequenceOrder Microsoft.VisualStudio.TestTools.UnitTesting.SequenceOrder.InOrder = 0 -> Microsoft.VisualStudio.TestTools.UnitTesting.SequenceOrder diff --git a/test/UnitTests/TestFramework.UnitTests/Attributes/ConditionAttributeTests.cs b/test/UnitTests/TestFramework.UnitTests/Attributes/ConditionAttributeTests.cs new file mode 100644 index 0000000000..bab6293c90 --- /dev/null +++ b/test/UnitTests/TestFramework.UnitTests/Attributes/ConditionAttributeTests.cs @@ -0,0 +1,195 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using AwesomeAssertions; + +using TestFramework.ForTestingMSTest; + +namespace UnitTestFramework.Tests; + +public class ConditionAttributeTests : TestContainer +{ + #region Test helpers (condition members) + + private sealed class Conditions + { + public static bool TruePropertyValue => true; + + public static bool FalsePropertyValue => false; + + public static readonly bool TrueField = true; + +#pragma warning disable CA1805 // explicit init illustrates intent in the test fixture + public static readonly bool FalseField = false; +#pragma warning restore CA1805 + + public static bool TrueMethod() => true; + + public static bool FalseMethod() => false; + + public static int NotABool => 42; + + public static bool WithParam(int _) => true; + + public bool InstanceProp => true; + + internal static bool InternalTrueProperty => true; + } + + #endregion + + public void Constructor_DefaultMode_IsInclude() + { + var attribute = new ConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue)); + + attribute.Mode.Should().Be(ConditionMode.Include); + attribute.ConditionType.Should().Be(typeof(Conditions)); + attribute.ConditionMemberNames.Should().BeEquivalentTo([nameof(Conditions.TruePropertyValue)]); + } + + public void Constructor_ExplicitMode_IsHonored() + { + var attribute = new ConditionAttribute(ConditionMode.Exclude, typeof(Conditions), nameof(Conditions.TruePropertyValue)); + + attribute.Mode.Should().Be(ConditionMode.Exclude); + } + + public void Constructor_NullType_Throws() + => ((Action)(() => _ = new ConditionAttribute(null!, "Foo"))) + .Should().Throw() + .And.ParamName.Should().Be("conditionType"); + + public void Constructor_NullMemberName_Throws() + => ((Action)(() => _ = new ConditionAttribute(typeof(Conditions), null!))) + .Should().Throw() + .And.ParamName.Should().Be("conditionMemberName"); + + public void Constructor_NullAdditionalMemberNames_DoesNotThrow() + { + var attribute = new ConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue), null!); + attribute.ConditionMemberNames.Should().BeEquivalentTo([nameof(Conditions.TruePropertyValue)]); + } + + public void Constructor_EmptyAdditionalMemberNames_Ok() + { + var attribute = new ConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue), []); + attribute.ConditionMemberNames.Should().BeEquivalentTo([nameof(Conditions.TruePropertyValue)]); + } + + public void Constructor_WhitespaceMemberName_Throws() + => ((Action)(() => _ = new ConditionAttribute(typeof(Conditions), " "))) + .Should().Throw() + .And.ParamName.Should().Be("conditionMemberName"); + + public void Constructor_WhitespaceAdditionalMemberName_Throws() + => ((Action)(() => _ = new ConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue), " "))) + .Should().Throw() + .And.ParamName.Should().Be("additionalConditionMemberNames"); + + public void IgnoreMessage_Include_HasExpectedText() + { + var attribute = new ConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue)); + + attribute.IgnoreMessage.Should().Contain("only supported") + .And.Contain(nameof(Conditions.TruePropertyValue)) + .And.Contain(typeof(Conditions).FullName!); + } + + public void IgnoreMessage_Exclude_HasExpectedText() + { + var attribute = new ConditionAttribute(ConditionMode.Exclude, typeof(Conditions), nameof(Conditions.TruePropertyValue)); + + attribute.IgnoreMessage.Should().Contain("not supported") + .And.Contain(nameof(Conditions.TruePropertyValue)); + } + + public void IgnoreMessage_MultipleMembers_ListsAllWithAnd() + { + var attribute = new ConditionAttribute( + typeof(Conditions), + nameof(Conditions.TruePropertyValue), + nameof(Conditions.TrueField)); + + attribute.IgnoreMessage.Should().Contain(nameof(Conditions.TruePropertyValue)) + .And.Contain(nameof(Conditions.TrueField)) + .And.Contain(" AND "); + } + + public void IsConditionMet_StaticPublicProperty_ReturnsValue() + { + new ConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue)).IsConditionMet.Should().BeTrue(); + new ConditionAttribute(typeof(Conditions), nameof(Conditions.FalsePropertyValue)).IsConditionMet.Should().BeFalse(); + } + + public void IsConditionMet_StaticPublicField_ReturnsValue() + { + new ConditionAttribute(typeof(Conditions), nameof(Conditions.TrueField)).IsConditionMet.Should().BeTrue(); + new ConditionAttribute(typeof(Conditions), nameof(Conditions.FalseField)).IsConditionMet.Should().BeFalse(); + } + + public void IsConditionMet_StaticParameterlessMethod_ReturnsValue() + { + new ConditionAttribute(typeof(Conditions), nameof(Conditions.TrueMethod)).IsConditionMet.Should().BeTrue(); + new ConditionAttribute(typeof(Conditions), nameof(Conditions.FalseMethod)).IsConditionMet.Should().BeFalse(); + } + + public void IsConditionMet_NonPublicStaticProperty_IsResolved() + => new ConditionAttribute(typeof(Conditions), "InternalTrueProperty").IsConditionMet.Should().BeTrue(); + + public void IsConditionMet_MultipleMembers_AndsValues() + { + new ConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue), nameof(Conditions.TrueField), nameof(Conditions.TrueMethod)) + .IsConditionMet.Should().BeTrue(); + + new ConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue), nameof(Conditions.FalseField)) + .IsConditionMet.Should().BeFalse(); + + new ConditionAttribute(typeof(Conditions), nameof(Conditions.FalsePropertyValue), nameof(Conditions.TrueField)) + .IsConditionMet.Should().BeFalse(); + } + + public void IsConditionMet_MissingMember_ThrowsInvalidOperation() + => ((Func)(() => new ConditionAttribute(typeof(Conditions), "DoesNotExist").IsConditionMet)) + .Should().Throw() + .WithMessage("*DoesNotExist*"); + + public void IsConditionMet_NonBoolProperty_ThrowsInvalidOperation() + => ((Func)(() => new ConditionAttribute(typeof(Conditions), nameof(Conditions.NotABool)).IsConditionMet)) + .Should().Throw() + .WithMessage("*static bool*"); + + public void IsConditionMet_MethodWithParameters_FallsThroughAndThrows() + => ((Func)(() => new ConditionAttribute(typeof(Conditions), nameof(Conditions.WithParam)).IsConditionMet)) + .Should().Throw() + .WithMessage("*WithParam*"); + + public void IsConditionMet_InstanceProperty_NotFoundForStaticLookup() + => ((Func)(() => new ConditionAttribute(typeof(Conditions), nameof(Conditions.InstanceProp)).IsConditionMet)) + .Should().Throw() + .WithMessage("*InstanceProp*"); + + public void GroupName_EncodesTypeAndMembers() + { + var attribute = new ConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue)); + + attribute.GroupName.Should().Contain(nameof(ConditionAttribute)) + .And.Contain(typeof(Conditions).FullName!) + .And.Contain(nameof(Conditions.TruePropertyValue)); + } + + public void GroupName_DifferentMembers_AreDifferentGroups() + { + var a = new ConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue)); + var b = new ConditionAttribute(typeof(Conditions), nameof(Conditions.FalsePropertyValue)); + + a.GroupName.Should().NotBe(b.GroupName); + } + + public void GroupName_SameTypeAndMembers_AreSameGroup() + { + var a = new ConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue)); + var b = new ConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue)); + + a.GroupName.Should().Be(b.GroupName); + } +} From 460fbf9768627101e4d7ec1b336f163cd90eb514 Mon Sep 17 00:00:00 2001 From: Evangelink Date: Fri, 12 Jun 2026 14:13:02 +0200 Subject: [PATCH 2/9] Restrict [Condition] member resolution to public static members Per design review, restrict the resolver to `BindingFlags.Public | Static` (was `Public | NonPublic | Static`). Rationale: - The `conditionType` is effectively acting as a public API surface for the condition. Non-public members of another type don't belong in that contract. - Slimmer `DynamicallyAccessedMembers` annotations for trim/AOT (3 flags instead of 6) -- only public properties, fields, and methods are preserved. - Matches xUnit's `MemberData` resolution which defaults to `Public | Static`. - Avoids surprises with `InternalsVisibleTo` when the condition type lives in a different assembly than the test. Error messages updated to say "public static bool"; XML docs aligned. Test `IsConditionMet_NonPublicStaticProperty_IsResolved` flipped to `IsConditionMet_NonPublicStaticProperty_ThrowsInvalidOperation`. All 23 tests still pass on net8.0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TestMethod/ConditionAttribute.cs | 52 +++++++++---------- .../Attributes/ConditionAttributeTests.cs | 6 ++- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/TestFramework/TestFramework/Attributes/TestMethod/ConditionAttribute.cs b/src/TestFramework/TestFramework/Attributes/TestMethod/ConditionAttribute.cs index 532342e6f7..c2705b9ad1 100644 --- a/src/TestFramework/TestFramework/Attributes/TestMethod/ConditionAttribute.cs +++ b/src/TestFramework/TestFramework/Attributes/TestMethod/ConditionAttribute.cs @@ -20,10 +20,11 @@ namespace Microsoft.VisualStudio.TestTools.UnitTesting; /// with a logical AND, matching the typical [ConditionalFact] usage pattern in other test frameworks. /// /// -/// If the referenced member cannot be found, is not , does not return -/// , or (for methods) requires parameters, evaluating -/// throws an . This surfaces as a test error rather than a -/// silent skip so typos and refactors don't accidentally disable tests. +/// If the referenced member cannot be found as a +/// property, field, or parameterless method, or (for methods) requires parameters, +/// evaluating throws an . This +/// surfaces as a test error rather than a silent skip so typos and refactors don't accidentally +/// disable tests. /// /// /// This attribute isn't inherited. Applying it to a base class will not affect derived classes. @@ -51,11 +52,8 @@ public sealed class ConditionAttribute : ConditionBaseAttribute { private const DynamicallyAccessedMemberTypes RequiredMembers = DynamicallyAccessedMemberTypes.PublicProperties - | DynamicallyAccessedMemberTypes.NonPublicProperties | DynamicallyAccessedMemberTypes.PublicFields - | DynamicallyAccessedMemberTypes.NonPublicFields - | DynamicallyAccessedMemberTypes.PublicMethods - | DynamicallyAccessedMemberTypes.NonPublicMethods; + | DynamicallyAccessedMemberTypes.PublicMethods; private readonly string[] _conditionMemberNames; private string? _groupName; @@ -67,8 +65,8 @@ public sealed class ConditionAttribute : ConditionBaseAttribute /// /// The type declaring the static member to evaluate. /// - /// The name of the member (property, field, - /// or parameterless method) to evaluate. + /// The name of the member + /// (property, field, or parameterless method) to evaluate. /// public ConditionAttribute( [DynamicallyAccessedMembers(RequiredMembers)] Type conditionType, @@ -84,12 +82,12 @@ public ConditionAttribute( /// /// The type declaring the static member(s) to evaluate. /// - /// The name of the first member (property, field, - /// or parameterless method) to evaluate. + /// The name of the first + /// member (property, field, or parameterless method) to evaluate. /// /// - /// Additional member name(s) to evaluate. All - /// referenced members are AND-combined. + /// Additional member + /// name(s) to evaluate. All referenced members are AND-combined. /// [CLSCompliant(false)] public ConditionAttribute( @@ -109,8 +107,8 @@ public ConditionAttribute( /// /// The type declaring the static member to evaluate. /// - /// The name of the member (property, field, - /// or parameterless method) to evaluate. + /// The name of the member + /// (property, field, or parameterless method) to evaluate. /// public ConditionAttribute( ConditionMode mode, @@ -129,12 +127,12 @@ public ConditionAttribute( /// /// The type declaring the static member(s) to evaluate. /// - /// The name of the first member (property, field, - /// or parameterless method) to evaluate. + /// The name of the first + /// member (property, field, or parameterless method) to evaluate. /// /// - /// Additional member name(s) to evaluate. All - /// referenced members are AND-combined. + /// Additional member + /// name(s) to evaluate. All referenced members are AND-combined. /// [CLSCompliant(false)] public ConditionAttribute( @@ -211,8 +209,8 @@ public override string GroupName /// /// All referenced members are evaluated in order and combined with a logical AND. Throws /// if a member can't be resolved as a - /// or - /// property, field, or parameterless method. + /// property, field, or + /// parameterless method. /// public override bool IsConditionMet { @@ -232,7 +230,7 @@ public override bool IsConditionMet private bool EvaluateMember(string memberName) { - const BindingFlags Flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static; + const BindingFlags Flags = BindingFlags.Public | BindingFlags.Static; string typeName = ConditionType.FullName ?? ConditionType.Name; PropertyInfo? property = ConditionType.GetProperty(memberName, Flags); @@ -240,7 +238,7 @@ private bool EvaluateMember(string memberName) { return property.PropertyType != typeof(bool) || property.GetMethod is null ? throw new InvalidOperationException( - $"Member '{typeName}.{memberName}' must be a static bool readable property to be used with [Condition].") + $"Member '{typeName}.{memberName}' must be a public static bool readable property to be used with [Condition].") : (bool)property.GetValue(null)!; } @@ -249,17 +247,17 @@ private bool EvaluateMember(string memberName) { return field.FieldType != typeof(bool) ? throw new InvalidOperationException( - $"Member '{typeName}.{memberName}' must be a static bool field to be used with [Condition].") + $"Member '{typeName}.{memberName}' must be a public static bool field to be used with [Condition].") : (bool)field.GetValue(null)!; } MethodInfo? method = ConditionType.GetMethod(memberName, Flags, binder: null, types: Type.EmptyTypes, modifiers: null); return method is null ? throw new InvalidOperationException( - $"Could not find a static bool property, field, or parameterless method named '{memberName}' on type '{typeName}'.") + $"Could not find a public static bool property, field, or parameterless method named '{memberName}' on type '{typeName}'.") : method.ReturnType != typeof(bool) ? throw new InvalidOperationException( - $"Member '{typeName}.{memberName}' must be a static parameterless bool method to be used with [Condition].") + $"Member '{typeName}.{memberName}' must be a public static parameterless bool method to be used with [Condition].") : (bool)method.Invoke(null, null)!; } diff --git a/test/UnitTests/TestFramework.UnitTests/Attributes/ConditionAttributeTests.cs b/test/UnitTests/TestFramework.UnitTests/Attributes/ConditionAttributeTests.cs index bab6293c90..b660b2f823 100644 --- a/test/UnitTests/TestFramework.UnitTests/Attributes/ConditionAttributeTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Attributes/ConditionAttributeTests.cs @@ -133,8 +133,10 @@ public void IsConditionMet_StaticParameterlessMethod_ReturnsValue() new ConditionAttribute(typeof(Conditions), nameof(Conditions.FalseMethod)).IsConditionMet.Should().BeFalse(); } - public void IsConditionMet_NonPublicStaticProperty_IsResolved() - => new ConditionAttribute(typeof(Conditions), "InternalTrueProperty").IsConditionMet.Should().BeTrue(); + public void IsConditionMet_NonPublicStaticProperty_ThrowsInvalidOperation() + => ((Func)(() => new ConditionAttribute(typeof(Conditions), "InternalTrueProperty").IsConditionMet)) + .Should().Throw() + .WithMessage("*InternalTrueProperty*"); public void IsConditionMet_MultipleMembers_AndsValues() { From 360447b984dbbc9f3a28cab31cc4289213fd7f5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 12 Jun 2026 14:41:52 +0200 Subject: [PATCH 3/9] Address review: harden [Condition] property resolution and ConditionMemberNames - Wrap `_conditionMemberNames` in a cached `ReadOnlyCollection` so `ConditionMemberNames` can no longer be downcast to `string[]` and mutated. - In `EvaluateMember`, reject indexer properties (those with index parameters) alongside non-bool/non-readable properties so they consistently surface as `InvalidOperationException` instead of `TargetParameterCountException`. Use `GetGetMethod(nonPublic: true)` to reliably detect missing getters. - Add `ConditionMemberNames_CannotBeDowncastToMutableArray` test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Attributes/TestMethod/ConditionAttribute.cs | 12 +++++++++--- .../Attributes/ConditionAttributeTests.cs | 7 +++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/TestFramework/TestFramework/Attributes/TestMethod/ConditionAttribute.cs b/src/TestFramework/TestFramework/Attributes/TestMethod/ConditionAttribute.cs index c2705b9ad1..d401f68481 100644 --- a/src/TestFramework/TestFramework/Attributes/TestMethod/ConditionAttribute.cs +++ b/src/TestFramework/TestFramework/Attributes/TestMethod/ConditionAttribute.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Collections.ObjectModel; + namespace Microsoft.VisualStudio.TestTools.UnitTesting; /// @@ -57,6 +59,7 @@ public sealed class ConditionAttribute : ConditionBaseAttribute private readonly string[] _conditionMemberNames; private string? _groupName; + private ReadOnlyCollection? _conditionMemberNamesView; /// /// Initializes a new instance of the class with @@ -193,7 +196,8 @@ public ConditionAttribute( /// field, or parameterless method) on evaluated for this condition. /// Multiple values are combined with a logical AND. /// - public IReadOnlyList ConditionMemberNames => _conditionMemberNames; + public IReadOnlyList ConditionMemberNames + => _conditionMemberNamesView ??= new ReadOnlyCollection(_conditionMemberNames); /// /// @@ -236,9 +240,11 @@ private bool EvaluateMember(string memberName) PropertyInfo? property = ConditionType.GetProperty(memberName, Flags); if (property is not null) { - return property.PropertyType != typeof(bool) || property.GetMethod is null + return property.PropertyType != typeof(bool) + || property.GetIndexParameters().Length != 0 + || property.GetGetMethod(nonPublic: true) is null ? throw new InvalidOperationException( - $"Member '{typeName}.{memberName}' must be a public static bool readable property to be used with [Condition].") + $"Member '{typeName}.{memberName}' must be a public static bool readable parameterless property to be used with [Condition].") : (bool)property.GetValue(null)!; } diff --git a/test/UnitTests/TestFramework.UnitTests/Attributes/ConditionAttributeTests.cs b/test/UnitTests/TestFramework.UnitTests/Attributes/ConditionAttributeTests.cs index b660b2f823..a4e2feb09d 100644 --- a/test/UnitTests/TestFramework.UnitTests/Attributes/ConditionAttributeTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Attributes/ConditionAttributeTests.cs @@ -194,4 +194,11 @@ public void GroupName_SameTypeAndMembers_AreSameGroup() a.GroupName.Should().Be(b.GroupName); } + + public void ConditionMemberNames_CannotBeDowncastToMutableArray() + { + var attribute = new ConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue)); + + attribute.ConditionMemberNames.Should().NotBeAssignableTo(); + } } From 3642f826212a9a0b5e069a8e35d457e81bd521ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 12 Jun 2026 14:57:50 +0200 Subject: [PATCH 4/9] Address self-review: cache evaluators, encode Mode in GroupName, expand tests - `IsConditionMet` now resolves each member once and caches a `Func` delegate. Subsequent evaluations skip the per-call `GetProperty` / `GetField` / `GetMethod` lookups (~3xN reflection lookups per access). - `GroupName` now includes `Mode` so two `[Condition]` attributes with the same type/members but opposite modes no longer share a group (previously they would silently OR-combine and the test would always run). - `IsConditionMet` uses `All` for the AND short-circuit, addressing the CodeQL note. - Added `IsConditionMet_ParameterlessMethodWithNonBoolReturn_ThrowsInvalidOperation`, `GroupName_DifferentMode_AreDifferentGroups`, and tightened `IgnoreMessage_Exclude_HasExpectedText` to also assert the type FullName (matching the Include variant). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TestMethod/ConditionAttribute.cs | 54 +++++++++++-------- .../Attributes/ConditionAttributeTests.cs | 18 ++++++- 2 files changed, 48 insertions(+), 24 deletions(-) diff --git a/src/TestFramework/TestFramework/Attributes/TestMethod/ConditionAttribute.cs b/src/TestFramework/TestFramework/Attributes/TestMethod/ConditionAttribute.cs index d401f68481..9f7a7086b8 100644 --- a/src/TestFramework/TestFramework/Attributes/TestMethod/ConditionAttribute.cs +++ b/src/TestFramework/TestFramework/Attributes/TestMethod/ConditionAttribute.cs @@ -60,6 +60,7 @@ public sealed class ConditionAttribute : ConditionBaseAttribute private readonly string[] _conditionMemberNames; private string? _groupName; private ReadOnlyCollection? _conditionMemberNamesView; + private Func[]? _evaluators; /// /// Initializes a new instance of the class with @@ -202,37 +203,44 @@ public IReadOnlyList ConditionMemberNames /// /// /// Each instance produces a group name derived from - /// and , so stacking multiple - /// declarations on the same target combines them with a - /// logical AND. + /// , , and + /// , so stacking multiple + /// declarations on the same target combines them with a logical AND -- including pairs with + /// the same type/members but opposite values, which would otherwise + /// silently cancel each other out. /// public override string GroupName - => _groupName ??= $"{nameof(ConditionAttribute)}:{ConditionType.FullName ?? ConditionType.Name}:{string.Join("|", _conditionMemberNames)}"; + => _groupName ??= $"{nameof(ConditionAttribute)}:{ConditionType.FullName ?? ConditionType.Name}:{string.Join("|", _conditionMemberNames)}:{Mode}"; /// /// /// All referenced members are evaluated in order and combined with a logical AND. Throws /// if a member can't be resolved as a /// property, field, or - /// parameterless method. + /// parameterless method. Resolved members are cached after the first access so subsequent + /// evaluations don't pay the reflection cost again. /// public override bool IsConditionMet { get { - foreach (string memberName in _conditionMemberNames) - { - if (!EvaluateMember(memberName)) - { - return false; - } - } + Func[] evaluators = _evaluators ??= BuildEvaluators(); + return evaluators.All(static evaluator => evaluator()); + } + } - return true; + private Func[] BuildEvaluators() + { + var evaluators = new Func[_conditionMemberNames.Length]; + for (int i = 0; i < _conditionMemberNames.Length; i++) + { + evaluators[i] = BuildEvaluator(_conditionMemberNames[i]); } + + return evaluators; } - private bool EvaluateMember(string memberName) + private Func BuildEvaluator(string memberName) { const BindingFlags Flags = BindingFlags.Public | BindingFlags.Static; string typeName = ConditionType.FullName ?? ConditionType.Name; @@ -245,7 +253,7 @@ private bool EvaluateMember(string memberName) || property.GetGetMethod(nonPublic: true) is null ? throw new InvalidOperationException( $"Member '{typeName}.{memberName}' must be a public static bool readable parameterless property to be used with [Condition].") - : (bool)property.GetValue(null)!; + : () => (bool)property.GetValue(null)!; } FieldInfo? field = ConditionType.GetField(memberName, Flags); @@ -254,17 +262,17 @@ private bool EvaluateMember(string memberName) return field.FieldType != typeof(bool) ? throw new InvalidOperationException( $"Member '{typeName}.{memberName}' must be a public static bool field to be used with [Condition].") - : (bool)field.GetValue(null)!; + : () => (bool)field.GetValue(null)!; } - MethodInfo? method = ConditionType.GetMethod(memberName, Flags, binder: null, types: Type.EmptyTypes, modifiers: null); - return method is null + MethodInfo? method = ConditionType.GetMethod(memberName, Flags, binder: null, types: Type.EmptyTypes, modifiers: null) + ?? throw new InvalidOperationException( + $"Could not find a public static bool property, field, or parameterless method named '{memberName}' on type '{typeName}'."); + + return method.ReturnType != typeof(bool) ? throw new InvalidOperationException( - $"Could not find a public static bool property, field, or parameterless method named '{memberName}' on type '{typeName}'.") - : method.ReturnType != typeof(bool) - ? throw new InvalidOperationException( - $"Member '{typeName}.{memberName}' must be a public static parameterless bool method to be used with [Condition].") - : (bool)method.Invoke(null, null)!; + $"Member '{typeName}.{memberName}' must be a public static parameterless bool method to be used with [Condition].") + : () => (bool)method.Invoke(null, null)!; } private string FormatMemberList() diff --git a/test/UnitTests/TestFramework.UnitTests/Attributes/ConditionAttributeTests.cs b/test/UnitTests/TestFramework.UnitTests/Attributes/ConditionAttributeTests.cs index a4e2feb09d..501d13ffdb 100644 --- a/test/UnitTests/TestFramework.UnitTests/Attributes/ConditionAttributeTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Attributes/ConditionAttributeTests.cs @@ -29,6 +29,8 @@ private sealed class Conditions public static int NotABool => 42; + public static int NotABoolMethod() => 42; + public static bool WithParam(int _) => true; public bool InstanceProp => true; @@ -100,7 +102,8 @@ public void IgnoreMessage_Exclude_HasExpectedText() var attribute = new ConditionAttribute(ConditionMode.Exclude, typeof(Conditions), nameof(Conditions.TruePropertyValue)); attribute.IgnoreMessage.Should().Contain("not supported") - .And.Contain(nameof(Conditions.TruePropertyValue)); + .And.Contain(nameof(Conditions.TruePropertyValue)) + .And.Contain(typeof(Conditions).FullName!); } public void IgnoreMessage_MultipleMembers_ListsAllWithAnd() @@ -165,6 +168,11 @@ public void IsConditionMet_MethodWithParameters_FallsThroughAndThrows() .Should().Throw() .WithMessage("*WithParam*"); + public void IsConditionMet_ParameterlessMethodWithNonBoolReturn_ThrowsInvalidOperation() + => ((Func)(() => new ConditionAttribute(typeof(Conditions), nameof(Conditions.NotABoolMethod)).IsConditionMet)) + .Should().Throw() + .WithMessage("*static parameterless bool method*"); + public void IsConditionMet_InstanceProperty_NotFoundForStaticLookup() => ((Func)(() => new ConditionAttribute(typeof(Conditions), nameof(Conditions.InstanceProp)).IsConditionMet)) .Should().Throw() @@ -195,6 +203,14 @@ public void GroupName_SameTypeAndMembers_AreSameGroup() a.GroupName.Should().Be(b.GroupName); } + public void GroupName_DifferentMode_AreDifferentGroups() + { + var include = new ConditionAttribute(ConditionMode.Include, typeof(Conditions), nameof(Conditions.TruePropertyValue)); + var exclude = new ConditionAttribute(ConditionMode.Exclude, typeof(Conditions), nameof(Conditions.TruePropertyValue)); + + include.GroupName.Should().NotBe(exclude.GroupName); + } + public void ConditionMemberNames_CannotBeDowncastToMutableArray() { var attribute = new ConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue)); From dd50f2b8062d79369adde74a95247d80d11f07ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 12 Jun 2026 16:28:35 +0200 Subject: [PATCH 5/9] Rename ConditionAttribute to MemberConditionAttribute and drop unnecessary [CLSCompliant(false)] - The previous name was too generic and clashed conceptually with the existing ConditionBaseAttribute / OSConditionAttribute / CIConditionAttribute family. MemberConditionAttribute better reflects that the attribute targets a static member by name. - [CLSCompliant(false)] was not needed on the params string[] overloads: the existing DataRowAttribute(params object?[]?) and DynamicDataAttribute(string, params object?[]) constructors compile cleanly without it in the same CLS-compliant assembly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...tribute.cs => MemberConditionAttribute.cs} | 42 ++++++----- .../PublicAPI/PublicAPI.Unshipped.txt | 18 ++--- ...ts.cs => MemberConditionAttributeTests.cs} | 72 +++++++++---------- 3 files changed, 65 insertions(+), 67 deletions(-) rename src/TestFramework/TestFramework/Attributes/TestMethod/{ConditionAttribute.cs => MemberConditionAttribute.cs} (88%) rename test/UnitTests/TestFramework.UnitTests/Attributes/{ConditionAttributeTests.cs => MemberConditionAttributeTests.cs} (58%) diff --git a/src/TestFramework/TestFramework/Attributes/TestMethod/ConditionAttribute.cs b/src/TestFramework/TestFramework/Attributes/TestMethod/MemberConditionAttribute.cs similarity index 88% rename from src/TestFramework/TestFramework/Attributes/TestMethod/ConditionAttribute.cs rename to src/TestFramework/TestFramework/Attributes/TestMethod/MemberConditionAttribute.cs index 9f7a7086b8..e70d635a0a 100644 --- a/src/TestFramework/TestFramework/Attributes/TestMethod/ConditionAttribute.cs +++ b/src/TestFramework/TestFramework/Attributes/TestMethod/MemberConditionAttribute.cs @@ -17,8 +17,8 @@ namespace Microsoft.VisualStudio.TestTools.UnitTesting; /// every referenced member evaluates to . /// /// -/// Each instance forms its own , -/// so stacking multiple declarations on the same target is combined +/// Each instance forms its own , +/// so stacking multiple declarations on the same target is combined /// with a logical AND, matching the typical [ConditionalFact] usage pattern in other test frameworks. /// /// @@ -34,23 +34,23 @@ namespace Microsoft.VisualStudio.TestTools.UnitTesting; /// /// /// [TestMethod] -/// [Condition(typeof(Environment), nameof(Environment.Is64BitProcess))] +/// [MemberCondition(typeof(Environment), nameof(Environment.Is64BitProcess))] /// public void Only_Runs_On_64Bit() { } /// /// [TestMethod] -/// [Condition(typeof(PlatformDetection), +/// [MemberCondition(typeof(PlatformDetection), /// nameof(PlatformDetection.IsNotBrowser), /// nameof(PlatformDetection.IsThreadingSupported))] /// public void Requires_Threading_And_Not_Browser() { } /// /// [TestMethod] -/// [Condition(ConditionMode.Exclude, typeof(PlatformDetection), nameof(PlatformDetection.IsMonoRuntime))] +/// [MemberCondition(ConditionMode.Exclude, typeof(PlatformDetection), nameof(PlatformDetection.IsMonoRuntime))] /// public void Does_Not_Run_On_Mono() { } /// /// /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false, AllowMultiple = true)] -public sealed class ConditionAttribute : ConditionBaseAttribute +public sealed class MemberConditionAttribute : ConditionBaseAttribute { private const DynamicallyAccessedMemberTypes RequiredMembers = DynamicallyAccessedMemberTypes.PublicProperties @@ -63,7 +63,7 @@ public sealed class ConditionAttribute : ConditionBaseAttribute private Func[]? _evaluators; /// - /// Initializes a new instance of the class with + /// Initializes a new instance of the class with /// semantics: the test runs only when the referenced /// member evaluates to . /// @@ -72,7 +72,7 @@ public sealed class ConditionAttribute : ConditionBaseAttribute /// The name of the member /// (property, field, or parameterless method) to evaluate. /// - public ConditionAttribute( + public MemberConditionAttribute( [DynamicallyAccessedMembers(RequiredMembers)] Type conditionType, string conditionMemberName) : this(ConditionMode.Include, conditionType, conditionMemberName, additionalConditionMemberNames: []) @@ -80,7 +80,7 @@ public ConditionAttribute( } /// - /// Initializes a new instance of the class with + /// Initializes a new instance of the class with /// semantics: the test runs only when every referenced /// member evaluates to . /// @@ -93,8 +93,7 @@ public ConditionAttribute( /// Additional member /// name(s) to evaluate. All referenced members are AND-combined. /// - [CLSCompliant(false)] - public ConditionAttribute( + public MemberConditionAttribute( [DynamicallyAccessedMembers(RequiredMembers)] Type conditionType, string conditionMemberName, params string[] additionalConditionMemberNames) @@ -103,7 +102,7 @@ public ConditionAttribute( } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// /// Whether the test should be included (run when the condition is met) or excluded @@ -114,7 +113,7 @@ public ConditionAttribute( /// The name of the member /// (property, field, or parameterless method) to evaluate. /// - public ConditionAttribute( + public MemberConditionAttribute( ConditionMode mode, [DynamicallyAccessedMembers(RequiredMembers)] Type conditionType, string conditionMemberName) @@ -123,7 +122,7 @@ public ConditionAttribute( } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// /// Whether the test should be included (run when the condition is met) or excluded @@ -138,8 +137,7 @@ public ConditionAttribute( /// Additional member /// name(s) to evaluate. All referenced members are AND-combined. /// - [CLSCompliant(false)] - public ConditionAttribute( + public MemberConditionAttribute( ConditionMode mode, [DynamicallyAccessedMembers(RequiredMembers)] Type conditionType, string conditionMemberName, @@ -202,15 +200,15 @@ public IReadOnlyList ConditionMemberNames /// /// - /// Each instance produces a group name derived from + /// Each instance produces a group name derived from /// , , and - /// , so stacking multiple + /// , so stacking multiple /// declarations on the same target combines them with a logical AND -- including pairs with /// the same type/members but opposite values, which would otherwise /// silently cancel each other out. /// public override string GroupName - => _groupName ??= $"{nameof(ConditionAttribute)}:{ConditionType.FullName ?? ConditionType.Name}:{string.Join("|", _conditionMemberNames)}:{Mode}"; + => _groupName ??= $"{nameof(MemberConditionAttribute)}:{ConditionType.FullName ?? ConditionType.Name}:{string.Join("|", _conditionMemberNames)}:{Mode}"; /// /// @@ -252,7 +250,7 @@ private Func BuildEvaluator(string memberName) || property.GetIndexParameters().Length != 0 || property.GetGetMethod(nonPublic: true) is null ? throw new InvalidOperationException( - $"Member '{typeName}.{memberName}' must be a public static bool readable parameterless property to be used with [Condition].") + $"Member '{typeName}.{memberName}' must be a public static bool readable parameterless property to be used with [MemberCondition].") : () => (bool)property.GetValue(null)!; } @@ -261,7 +259,7 @@ private Func BuildEvaluator(string memberName) { return field.FieldType != typeof(bool) ? throw new InvalidOperationException( - $"Member '{typeName}.{memberName}' must be a public static bool field to be used with [Condition].") + $"Member '{typeName}.{memberName}' must be a public static bool field to be used with [MemberCondition].") : () => (bool)field.GetValue(null)!; } @@ -271,7 +269,7 @@ private Func BuildEvaluator(string memberName) return method.ReturnType != typeof(bool) ? throw new InvalidOperationException( - $"Member '{typeName}.{memberName}' must be a public static parameterless bool method to be used with [Condition].") + $"Member '{typeName}.{memberName}' must be a public static parameterless bool method to be used with [MemberCondition].") : () => (bool)method.Invoke(null, null)!; } diff --git a/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt b/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt index d8f68a20a1..28fd2cf850 100644 --- a/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt @@ -5,15 +5,15 @@ Microsoft.VisualStudio.TestTools.UnitTesting.AssemblyFixtureProviderAttribute.As Microsoft.VisualStudio.TestTools.UnitTesting.AssemblyFixtureProviderAttribute.FixtureType.get -> System.Type! Microsoft.VisualStudio.TestTools.UnitTesting.AssertFailedException.ActualText.get -> string? Microsoft.VisualStudio.TestTools.UnitTesting.AssertFailedException.ExpectedText.get -> string? -Microsoft.VisualStudio.TestTools.UnitTesting.ConditionAttribute -Microsoft.VisualStudio.TestTools.UnitTesting.ConditionAttribute.ConditionAttribute(Microsoft.VisualStudio.TestTools.UnitTesting.ConditionMode mode, System.Type! conditionType, string! conditionMemberName) -> void -Microsoft.VisualStudio.TestTools.UnitTesting.ConditionAttribute.ConditionAttribute(Microsoft.VisualStudio.TestTools.UnitTesting.ConditionMode mode, System.Type! conditionType, string! conditionMemberName, params string![]! additionalConditionMemberNames) -> void -Microsoft.VisualStudio.TestTools.UnitTesting.ConditionAttribute.ConditionAttribute(System.Type! conditionType, string! conditionMemberName) -> void -Microsoft.VisualStudio.TestTools.UnitTesting.ConditionAttribute.ConditionAttribute(System.Type! conditionType, string! conditionMemberName, params string![]! additionalConditionMemberNames) -> void -Microsoft.VisualStudio.TestTools.UnitTesting.ConditionAttribute.ConditionMemberNames.get -> System.Collections.Generic.IReadOnlyList! -Microsoft.VisualStudio.TestTools.UnitTesting.ConditionAttribute.ConditionType.get -> System.Type! -override Microsoft.VisualStudio.TestTools.UnitTesting.ConditionAttribute.GroupName.get -> string! -override Microsoft.VisualStudio.TestTools.UnitTesting.ConditionAttribute.IsConditionMet.get -> bool +Microsoft.VisualStudio.TestTools.UnitTesting.MemberConditionAttribute +Microsoft.VisualStudio.TestTools.UnitTesting.MemberConditionAttribute.MemberConditionAttribute(Microsoft.VisualStudio.TestTools.UnitTesting.ConditionMode mode, System.Type! conditionType, string! conditionMemberName) -> void +Microsoft.VisualStudio.TestTools.UnitTesting.MemberConditionAttribute.MemberConditionAttribute(Microsoft.VisualStudio.TestTools.UnitTesting.ConditionMode mode, System.Type! conditionType, string! conditionMemberName, params string![]! additionalConditionMemberNames) -> void +Microsoft.VisualStudio.TestTools.UnitTesting.MemberConditionAttribute.MemberConditionAttribute(System.Type! conditionType, string! conditionMemberName) -> void +Microsoft.VisualStudio.TestTools.UnitTesting.MemberConditionAttribute.MemberConditionAttribute(System.Type! conditionType, string! conditionMemberName, params string![]! additionalConditionMemberNames) -> void +Microsoft.VisualStudio.TestTools.UnitTesting.MemberConditionAttribute.ConditionMemberNames.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.VisualStudio.TestTools.UnitTesting.MemberConditionAttribute.ConditionType.get -> System.Type! +override Microsoft.VisualStudio.TestTools.UnitTesting.MemberConditionAttribute.GroupName.get -> string! +override Microsoft.VisualStudio.TestTools.UnitTesting.MemberConditionAttribute.IsConditionMet.get -> bool Microsoft.VisualStudio.TestTools.UnitTesting.SequenceOrder Microsoft.VisualStudio.TestTools.UnitTesting.SequenceOrder.InAnyOrder = 1 -> Microsoft.VisualStudio.TestTools.UnitTesting.SequenceOrder Microsoft.VisualStudio.TestTools.UnitTesting.SequenceOrder.InOrder = 0 -> Microsoft.VisualStudio.TestTools.UnitTesting.SequenceOrder diff --git a/test/UnitTests/TestFramework.UnitTests/Attributes/ConditionAttributeTests.cs b/test/UnitTests/TestFramework.UnitTests/Attributes/MemberConditionAttributeTests.cs similarity index 58% rename from test/UnitTests/TestFramework.UnitTests/Attributes/ConditionAttributeTests.cs rename to test/UnitTests/TestFramework.UnitTests/Attributes/MemberConditionAttributeTests.cs index 501d13ffdb..4ed3003b51 100644 --- a/test/UnitTests/TestFramework.UnitTests/Attributes/ConditionAttributeTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Attributes/MemberConditionAttributeTests.cs @@ -7,7 +7,7 @@ namespace UnitTestFramework.Tests; -public class ConditionAttributeTests : TestContainer +public class MemberConditionAttributeTests : TestContainer { #region Test helpers (condition members) @@ -42,7 +42,7 @@ private sealed class Conditions public void Constructor_DefaultMode_IsInclude() { - var attribute = new ConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue)); + var attribute = new MemberConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue)); attribute.Mode.Should().Be(ConditionMode.Include); attribute.ConditionType.Should().Be(typeof(Conditions)); @@ -51,46 +51,46 @@ public void Constructor_DefaultMode_IsInclude() public void Constructor_ExplicitMode_IsHonored() { - var attribute = new ConditionAttribute(ConditionMode.Exclude, typeof(Conditions), nameof(Conditions.TruePropertyValue)); + var attribute = new MemberConditionAttribute(ConditionMode.Exclude, typeof(Conditions), nameof(Conditions.TruePropertyValue)); attribute.Mode.Should().Be(ConditionMode.Exclude); } public void Constructor_NullType_Throws() - => ((Action)(() => _ = new ConditionAttribute(null!, "Foo"))) + => ((Action)(() => _ = new MemberConditionAttribute(null!, "Foo"))) .Should().Throw() .And.ParamName.Should().Be("conditionType"); public void Constructor_NullMemberName_Throws() - => ((Action)(() => _ = new ConditionAttribute(typeof(Conditions), null!))) + => ((Action)(() => _ = new MemberConditionAttribute(typeof(Conditions), null!))) .Should().Throw() .And.ParamName.Should().Be("conditionMemberName"); public void Constructor_NullAdditionalMemberNames_DoesNotThrow() { - var attribute = new ConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue), null!); + var attribute = new MemberConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue), null!); attribute.ConditionMemberNames.Should().BeEquivalentTo([nameof(Conditions.TruePropertyValue)]); } public void Constructor_EmptyAdditionalMemberNames_Ok() { - var attribute = new ConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue), []); + var attribute = new MemberConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue), []); attribute.ConditionMemberNames.Should().BeEquivalentTo([nameof(Conditions.TruePropertyValue)]); } public void Constructor_WhitespaceMemberName_Throws() - => ((Action)(() => _ = new ConditionAttribute(typeof(Conditions), " "))) + => ((Action)(() => _ = new MemberConditionAttribute(typeof(Conditions), " "))) .Should().Throw() .And.ParamName.Should().Be("conditionMemberName"); public void Constructor_WhitespaceAdditionalMemberName_Throws() - => ((Action)(() => _ = new ConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue), " "))) + => ((Action)(() => _ = new MemberConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue), " "))) .Should().Throw() .And.ParamName.Should().Be("additionalConditionMemberNames"); public void IgnoreMessage_Include_HasExpectedText() { - var attribute = new ConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue)); + var attribute = new MemberConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue)); attribute.IgnoreMessage.Should().Contain("only supported") .And.Contain(nameof(Conditions.TruePropertyValue)) @@ -99,7 +99,7 @@ public void IgnoreMessage_Include_HasExpectedText() public void IgnoreMessage_Exclude_HasExpectedText() { - var attribute = new ConditionAttribute(ConditionMode.Exclude, typeof(Conditions), nameof(Conditions.TruePropertyValue)); + var attribute = new MemberConditionAttribute(ConditionMode.Exclude, typeof(Conditions), nameof(Conditions.TruePropertyValue)); attribute.IgnoreMessage.Should().Contain("not supported") .And.Contain(nameof(Conditions.TruePropertyValue)) @@ -108,7 +108,7 @@ public void IgnoreMessage_Exclude_HasExpectedText() public void IgnoreMessage_MultipleMembers_ListsAllWithAnd() { - var attribute = new ConditionAttribute( + var attribute = new MemberConditionAttribute( typeof(Conditions), nameof(Conditions.TruePropertyValue), nameof(Conditions.TrueField)); @@ -120,100 +120,100 @@ public void IgnoreMessage_MultipleMembers_ListsAllWithAnd() public void IsConditionMet_StaticPublicProperty_ReturnsValue() { - new ConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue)).IsConditionMet.Should().BeTrue(); - new ConditionAttribute(typeof(Conditions), nameof(Conditions.FalsePropertyValue)).IsConditionMet.Should().BeFalse(); + new MemberConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue)).IsConditionMet.Should().BeTrue(); + new MemberConditionAttribute(typeof(Conditions), nameof(Conditions.FalsePropertyValue)).IsConditionMet.Should().BeFalse(); } public void IsConditionMet_StaticPublicField_ReturnsValue() { - new ConditionAttribute(typeof(Conditions), nameof(Conditions.TrueField)).IsConditionMet.Should().BeTrue(); - new ConditionAttribute(typeof(Conditions), nameof(Conditions.FalseField)).IsConditionMet.Should().BeFalse(); + new MemberConditionAttribute(typeof(Conditions), nameof(Conditions.TrueField)).IsConditionMet.Should().BeTrue(); + new MemberConditionAttribute(typeof(Conditions), nameof(Conditions.FalseField)).IsConditionMet.Should().BeFalse(); } public void IsConditionMet_StaticParameterlessMethod_ReturnsValue() { - new ConditionAttribute(typeof(Conditions), nameof(Conditions.TrueMethod)).IsConditionMet.Should().BeTrue(); - new ConditionAttribute(typeof(Conditions), nameof(Conditions.FalseMethod)).IsConditionMet.Should().BeFalse(); + new MemberConditionAttribute(typeof(Conditions), nameof(Conditions.TrueMethod)).IsConditionMet.Should().BeTrue(); + new MemberConditionAttribute(typeof(Conditions), nameof(Conditions.FalseMethod)).IsConditionMet.Should().BeFalse(); } public void IsConditionMet_NonPublicStaticProperty_ThrowsInvalidOperation() - => ((Func)(() => new ConditionAttribute(typeof(Conditions), "InternalTrueProperty").IsConditionMet)) + => ((Func)(() => new MemberConditionAttribute(typeof(Conditions), "InternalTrueProperty").IsConditionMet)) .Should().Throw() .WithMessage("*InternalTrueProperty*"); public void IsConditionMet_MultipleMembers_AndsValues() { - new ConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue), nameof(Conditions.TrueField), nameof(Conditions.TrueMethod)) + new MemberConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue), nameof(Conditions.TrueField), nameof(Conditions.TrueMethod)) .IsConditionMet.Should().BeTrue(); - new ConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue), nameof(Conditions.FalseField)) + new MemberConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue), nameof(Conditions.FalseField)) .IsConditionMet.Should().BeFalse(); - new ConditionAttribute(typeof(Conditions), nameof(Conditions.FalsePropertyValue), nameof(Conditions.TrueField)) + new MemberConditionAttribute(typeof(Conditions), nameof(Conditions.FalsePropertyValue), nameof(Conditions.TrueField)) .IsConditionMet.Should().BeFalse(); } public void IsConditionMet_MissingMember_ThrowsInvalidOperation() - => ((Func)(() => new ConditionAttribute(typeof(Conditions), "DoesNotExist").IsConditionMet)) + => ((Func)(() => new MemberConditionAttribute(typeof(Conditions), "DoesNotExist").IsConditionMet)) .Should().Throw() .WithMessage("*DoesNotExist*"); public void IsConditionMet_NonBoolProperty_ThrowsInvalidOperation() - => ((Func)(() => new ConditionAttribute(typeof(Conditions), nameof(Conditions.NotABool)).IsConditionMet)) + => ((Func)(() => new MemberConditionAttribute(typeof(Conditions), nameof(Conditions.NotABool)).IsConditionMet)) .Should().Throw() .WithMessage("*static bool*"); public void IsConditionMet_MethodWithParameters_FallsThroughAndThrows() - => ((Func)(() => new ConditionAttribute(typeof(Conditions), nameof(Conditions.WithParam)).IsConditionMet)) + => ((Func)(() => new MemberConditionAttribute(typeof(Conditions), nameof(Conditions.WithParam)).IsConditionMet)) .Should().Throw() .WithMessage("*WithParam*"); public void IsConditionMet_ParameterlessMethodWithNonBoolReturn_ThrowsInvalidOperation() - => ((Func)(() => new ConditionAttribute(typeof(Conditions), nameof(Conditions.NotABoolMethod)).IsConditionMet)) + => ((Func)(() => new MemberConditionAttribute(typeof(Conditions), nameof(Conditions.NotABoolMethod)).IsConditionMet)) .Should().Throw() .WithMessage("*static parameterless bool method*"); public void IsConditionMet_InstanceProperty_NotFoundForStaticLookup() - => ((Func)(() => new ConditionAttribute(typeof(Conditions), nameof(Conditions.InstanceProp)).IsConditionMet)) + => ((Func)(() => new MemberConditionAttribute(typeof(Conditions), nameof(Conditions.InstanceProp)).IsConditionMet)) .Should().Throw() .WithMessage("*InstanceProp*"); public void GroupName_EncodesTypeAndMembers() { - var attribute = new ConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue)); + var attribute = new MemberConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue)); - attribute.GroupName.Should().Contain(nameof(ConditionAttribute)) + attribute.GroupName.Should().Contain(nameof(MemberConditionAttribute)) .And.Contain(typeof(Conditions).FullName!) .And.Contain(nameof(Conditions.TruePropertyValue)); } public void GroupName_DifferentMembers_AreDifferentGroups() { - var a = new ConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue)); - var b = new ConditionAttribute(typeof(Conditions), nameof(Conditions.FalsePropertyValue)); + var a = new MemberConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue)); + var b = new MemberConditionAttribute(typeof(Conditions), nameof(Conditions.FalsePropertyValue)); a.GroupName.Should().NotBe(b.GroupName); } public void GroupName_SameTypeAndMembers_AreSameGroup() { - var a = new ConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue)); - var b = new ConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue)); + var a = new MemberConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue)); + var b = new MemberConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue)); a.GroupName.Should().Be(b.GroupName); } public void GroupName_DifferentMode_AreDifferentGroups() { - var include = new ConditionAttribute(ConditionMode.Include, typeof(Conditions), nameof(Conditions.TruePropertyValue)); - var exclude = new ConditionAttribute(ConditionMode.Exclude, typeof(Conditions), nameof(Conditions.TruePropertyValue)); + var include = new MemberConditionAttribute(ConditionMode.Include, typeof(Conditions), nameof(Conditions.TruePropertyValue)); + var exclude = new MemberConditionAttribute(ConditionMode.Exclude, typeof(Conditions), nameof(Conditions.TruePropertyValue)); include.GroupName.Should().NotBe(exclude.GroupName); } public void ConditionMemberNames_CannotBeDowncastToMutableArray() { - var attribute = new ConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue)); + var attribute = new MemberConditionAttribute(typeof(Conditions), nameof(Conditions.TruePropertyValue)); attribute.ConditionMemberNames.Should().NotBeAssignableTo(); } From 6065c5fd9bb7a811aee596542a2b491e23382ce4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 12 Jun 2026 16:46:51 +0200 Subject: [PATCH 6/9] Address review: resolve inherited static members, require public getter - Add `BindingFlags.FlattenHierarchy` to the member lookup so `[MemberCondition(typeof(Derived), nameof(Base.SomeFlag))]` resolves the inherited static member via the derived type, matching how C# allows accessing public static base members through a derived type name. - Tighten the property validation to require a public getter (`GetGetMethod(nonPublic: false)`). A public static property with a private getter doesn't satisfy the documented `public static bool readable property` contract and isn't guaranteed to be preserved by the `PublicProperties` trimming annotation on `ConditionType`. - Add `IsConditionMet_InheritedStaticProperty_ResolvesViaDerivedType` and `IsConditionMet_PropertyWithPrivateGetter_ThrowsInvalidOperation` tests (plus matching fixture types). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TestMethod/MemberConditionAttribute.cs | 4 ++-- .../MemberConditionAttributeTests.cs | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/TestFramework/TestFramework/Attributes/TestMethod/MemberConditionAttribute.cs b/src/TestFramework/TestFramework/Attributes/TestMethod/MemberConditionAttribute.cs index e70d635a0a..5d1d1ffd2d 100644 --- a/src/TestFramework/TestFramework/Attributes/TestMethod/MemberConditionAttribute.cs +++ b/src/TestFramework/TestFramework/Attributes/TestMethod/MemberConditionAttribute.cs @@ -240,7 +240,7 @@ private Func[] BuildEvaluators() private Func BuildEvaluator(string memberName) { - const BindingFlags Flags = BindingFlags.Public | BindingFlags.Static; + const BindingFlags Flags = BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy; string typeName = ConditionType.FullName ?? ConditionType.Name; PropertyInfo? property = ConditionType.GetProperty(memberName, Flags); @@ -248,7 +248,7 @@ private Func BuildEvaluator(string memberName) { return property.PropertyType != typeof(bool) || property.GetIndexParameters().Length != 0 - || property.GetGetMethod(nonPublic: true) is null + || property.GetGetMethod(nonPublic: false) is null ? throw new InvalidOperationException( $"Member '{typeName}.{memberName}' must be a public static bool readable parameterless property to be used with [MemberCondition].") : () => (bool)property.GetValue(null)!; diff --git a/test/UnitTests/TestFramework.UnitTests/Attributes/MemberConditionAttributeTests.cs b/test/UnitTests/TestFramework.UnitTests/Attributes/MemberConditionAttributeTests.cs index 4ed3003b51..8c88723b93 100644 --- a/test/UnitTests/TestFramework.UnitTests/Attributes/MemberConditionAttributeTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Attributes/MemberConditionAttributeTests.cs @@ -17,6 +17,8 @@ private sealed class Conditions public static bool FalsePropertyValue => false; + public static bool PropertyWithPrivateGetter { private get; set; } + public static readonly bool TrueField = true; #pragma warning disable CA1805 // explicit init illustrates intent in the test fixture @@ -38,6 +40,15 @@ private sealed class Conditions internal static bool InternalTrueProperty => true; } + private class ConditionsBase + { + public static bool InheritedTrueProperty => true; + } + + private sealed class ConditionsDerived : ConditionsBase + { + } + #endregion public void Constructor_DefaultMode_IsInclude() @@ -217,4 +228,16 @@ public void ConditionMemberNames_CannotBeDowncastToMutableArray() attribute.ConditionMemberNames.Should().NotBeAssignableTo(); } + + public void IsConditionMet_InheritedStaticProperty_ResolvesViaDerivedType() + { + var attribute = new MemberConditionAttribute(typeof(ConditionsDerived), nameof(ConditionsBase.InheritedTrueProperty)); + + attribute.IsConditionMet.Should().BeTrue(); + } + + public void IsConditionMet_PropertyWithPrivateGetter_ThrowsInvalidOperation() + => ((Func)(() => new MemberConditionAttribute(typeof(Conditions), nameof(Conditions.PropertyWithPrivateGetter)).IsConditionMet)) + .Should().Throw() + .WithMessage("*public static bool readable*"); } From 4d9c906a8cc254a30360b458136dd964269a4a5a Mon Sep 17 00:00:00 2001 From: Evangelink Date: Fri, 12 Jun 2026 14:37:30 +0200 Subject: [PATCH 7/9] Add MSTEST0070 analyzer validating [MemberCondition] member references Adds `MemberConditionShouldBeValidAnalyzer` (MSTEST0070) which catches at build time the same errors that `MemberConditionAttribute.IsConditionMet` throws at runtime, so typos and refactors don't silently break test gating. Rules emitted (one diagnostic per member name in the attribute): - MemberNotFoundRule -- the named member doesn't exist on the type - MemberNotPublicRule -- member is not public - MemberNotStaticRule -- member is not static - MemberWrongKindRule -- member is an event/nested type/etc. - MemberWrongReturnTypeRule -- property/field/method doesn't return bool - MethodHasParametersRule -- referenced method has parameters - PropertyNotReadableRule -- property has no public getter The analyzer fires on any symbol carrying [MemberCondition] (test class or test method), walks each constructor argument, validates every member name, and matches the runtime resolution order (property -> field -> method) including inherited public static members. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AnalyzerReleases.Unshipped.md | 1 + .../MSTest.Analyzers/Helpers/DiagnosticIds.cs | 3 +- .../Helpers/WellKnownTypeNames.cs | 3 +- .../MemberConditionShouldBeValidAnalyzer.cs | 299 +++++++++++ src/Analyzers/MSTest.Analyzers/Resources.resx | 36 ++ .../MSTest.Analyzers/xlf/Resources.cs.xlf | 47 +- .../MSTest.Analyzers/xlf/Resources.de.xlf | 47 +- .../MSTest.Analyzers/xlf/Resources.es.xlf | 47 +- .../MSTest.Analyzers/xlf/Resources.fr.xlf | 47 +- .../MSTest.Analyzers/xlf/Resources.it.xlf | 47 +- .../MSTest.Analyzers/xlf/Resources.ja.xlf | 47 +- .../MSTest.Analyzers/xlf/Resources.ko.xlf | 47 +- .../MSTest.Analyzers/xlf/Resources.pl.xlf | 47 +- .../MSTest.Analyzers/xlf/Resources.pt-BR.xlf | 47 +- .../MSTest.Analyzers/xlf/Resources.ru.xlf | 47 +- .../MSTest.Analyzers/xlf/Resources.tr.xlf | 47 +- .../xlf/Resources.zh-Hans.xlf | 47 +- .../xlf/Resources.zh-Hant.xlf | 47 +- ...mberConditionShouldBeValidAnalyzerTests.cs | 497 ++++++++++++++++++ 19 files changed, 1435 insertions(+), 15 deletions(-) create mode 100644 src/Analyzers/MSTest.Analyzers/MemberConditionShouldBeValidAnalyzer.cs create mode 100644 test/UnitTests/MSTest.Analyzers.UnitTests/MemberConditionShouldBeValidAnalyzerTests.cs diff --git a/src/Analyzers/MSTest.Analyzers/AnalyzerReleases.Unshipped.md b/src/Analyzers/MSTest.Analyzers/AnalyzerReleases.Unshipped.md index 43b4b1fb3b..60a1fce5b5 100644 --- a/src/Analyzers/MSTest.Analyzers/AnalyzerReleases.Unshipped.md +++ b/src/Analyzers/MSTest.Analyzers/AnalyzerReleases.Unshipped.md @@ -10,3 +10,4 @@ MSTEST0065 | Usage | Warning | AvoidAssertAreEqualOnCollectionsAnalyzer, [Docume MSTEST0066 | Design | Info | IgnoreShouldHaveJustificationAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/core/testing/mstest-analyzers/mstest0066) MSTEST0067 | Usage | Disabled | AvoidThreadSleepAndTaskWaitInTestsAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/core/testing/mstest-analyzers/mstest0067) MSTEST0068 | Usage | Info | CollectionAssertToAssertAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/core/testing/mstest-analyzers/mstest0068) +MSTEST0070 | Usage | Warning | MemberConditionShouldBeValidAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/core/testing/mstest-analyzers/mstest0070) diff --git a/src/Analyzers/MSTest.Analyzers/Helpers/DiagnosticIds.cs b/src/Analyzers/MSTest.Analyzers/Helpers/DiagnosticIds.cs index 6774a4ef0b..585c7b2398 100644 --- a/src/Analyzers/MSTest.Analyzers/Helpers/DiagnosticIds.cs +++ b/src/Analyzers/MSTest.Analyzers/Helpers/DiagnosticIds.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. namespace MSTest.Analyzers.Helpers; @@ -74,4 +74,5 @@ internal static class DiagnosticIds public const string AvoidThreadSleepAndTaskWaitInTestsRuleId = "MSTEST0067"; public const string CollectionAssertToAssertRuleId = "MSTEST0068"; // public const string InheritedTestClassAttributeWithSourceGeneratorRuleId = "MSTEST0069"; - // Reserved. Owned by MSTest.SourceGeneration analyzer; don't reuse this ID. + public const string MemberConditionShouldBeValidRuleId = "MSTEST0070"; } diff --git a/src/Analyzers/MSTest.Analyzers/Helpers/WellKnownTypeNames.cs b/src/Analyzers/MSTest.Analyzers/Helpers/WellKnownTypeNames.cs index 69a0d144e2..4eca392aa2 100644 --- a/src/Analyzers/MSTest.Analyzers/Helpers/WellKnownTypeNames.cs +++ b/src/Analyzers/MSTest.Analyzers/Helpers/WellKnownTypeNames.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. namespace MSTest.Analyzers.Helpers; @@ -29,6 +29,7 @@ internal static class WellKnownTypeNames public const string MicrosoftVisualStudioTestToolsUnitTestingGlobalTestInitializeAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.GlobalTestInitializeAttribute"; public const string MicrosoftVisualStudioTestToolsUnitTestingIgnoreAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.IgnoreAttribute"; public const string MicrosoftVisualStudioTestToolsUnitTestingInheritanceBehavior = "Microsoft.VisualStudio.TestTools.UnitTesting.InheritanceBehavior"; + public const string MicrosoftVisualStudioTestToolsUnitTestingMemberConditionAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.MemberConditionAttribute"; public const string MicrosoftVisualStudioTestToolsUnitTestingOwnerAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.OwnerAttribute"; public const string MicrosoftVisualStudioTestToolsUnitTestingParallelizeAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.ParallelizeAttribute"; public const string MicrosoftVisualStudioTestToolsUnitTestingPriorityAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.PriorityAttribute"; diff --git a/src/Analyzers/MSTest.Analyzers/MemberConditionShouldBeValidAnalyzer.cs b/src/Analyzers/MSTest.Analyzers/MemberConditionShouldBeValidAnalyzer.cs new file mode 100644 index 0000000000..87dccc1766 --- /dev/null +++ b/src/Analyzers/MSTest.Analyzers/MemberConditionShouldBeValidAnalyzer.cs @@ -0,0 +1,299 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Immutable; + +using Analyzer.Utilities.Extensions; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +using MSTest.Analyzers.Helpers; + +namespace MSTest.Analyzers; + +/// +/// MSTEST0070: . +/// +[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] +public sealed class MemberConditionShouldBeValidAnalyzer : DiagnosticAnalyzer +{ + private static readonly LocalizableResourceString Title = new(nameof(Resources.MemberConditionShouldBeValidTitle), Resources.ResourceManager, typeof(Resources)); + private static readonly LocalizableResourceString Description = new(nameof(Resources.MemberConditionShouldBeValidDescription), Resources.ResourceManager, typeof(Resources)); + + /// + public static readonly DiagnosticDescriptor MemberNotFoundRule = DiagnosticDescriptorHelper.Create( + DiagnosticIds.MemberConditionShouldBeValidRuleId, + Title, + new LocalizableResourceString(nameof(Resources.MemberConditionShouldBeValidMessageFormat_MemberNotFound), Resources.ResourceManager, typeof(Resources)), + Description, + Category.Usage, + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + /// + public static readonly DiagnosticDescriptor MemberNotPublicRule = MemberNotFoundRule + .WithMessage(new(nameof(Resources.MemberConditionShouldBeValidMessageFormat_MemberNotPublic), Resources.ResourceManager, typeof(Resources))); + + /// + public static readonly DiagnosticDescriptor MemberNotStaticRule = MemberNotFoundRule + .WithMessage(new(nameof(Resources.MemberConditionShouldBeValidMessageFormat_MemberNotStatic), Resources.ResourceManager, typeof(Resources))); + + /// + public static readonly DiagnosticDescriptor MemberWrongKindRule = MemberNotFoundRule + .WithMessage(new(nameof(Resources.MemberConditionShouldBeValidMessageFormat_MemberWrongKind), Resources.ResourceManager, typeof(Resources))); + + /// + public static readonly DiagnosticDescriptor MemberWrongReturnTypeRule = MemberNotFoundRule + .WithMessage(new(nameof(Resources.MemberConditionShouldBeValidMessageFormat_MemberWrongReturnType), Resources.ResourceManager, typeof(Resources))); + + /// + public static readonly DiagnosticDescriptor MethodHasParametersRule = MemberNotFoundRule + .WithMessage(new(nameof(Resources.MemberConditionShouldBeValidMessageFormat_MethodHasParameters), Resources.ResourceManager, typeof(Resources))); + + /// + public static readonly DiagnosticDescriptor PropertyNotReadableRule = MemberNotFoundRule + .WithMessage(new(nameof(Resources.MemberConditionShouldBeValidMessageFormat_PropertyNotReadable), Resources.ResourceManager, typeof(Resources))); + + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( + MemberNotFoundRule, + MemberNotPublicRule, + MemberNotStaticRule, + MemberWrongKindRule, + MemberWrongReturnTypeRule, + MethodHasParametersRule, + PropertyNotReadableRule); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(context => + { + if (!context.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingMemberConditionAttribute, out INamedTypeSymbol? conditionAttributeSymbol)) + { + return; + } + + context.RegisterSymbolAction( + ctx => AnalyzeSymbol(ctx, conditionAttributeSymbol), + SymbolKind.Method, + SymbolKind.NamedType); + }); + } + + private static void AnalyzeSymbol(SymbolAnalysisContext context, INamedTypeSymbol conditionAttributeSymbol) + { + foreach (AttributeData attribute in context.Symbol.GetAttributes()) + { + if (!SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, conditionAttributeSymbol)) + { + continue; + } + + AnalyzeAttribute(context, attribute); + } + } + + private static void AnalyzeAttribute(SymbolAnalysisContext context, AttributeData attribute) + { + if (attribute.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken) is not { } attributeSyntax) + { + return; + } + + // Walk the constructor arguments. Across the 4 ctor overloads + // ( (Type, string), (Type, string, params string[]), + // (ConditionMode, Type, string), (ConditionMode, Type, string, params string[]) ) + // we can identify the condition type, the first member name, and the optional params array + // by inspecting argument kinds and types. + INamedTypeSymbol? conditionType = null; + var memberNames = new List(); + foreach (TypedConstant argument in attribute.ConstructorArguments) + { + if (argument.IsNull) + { + continue; + } + + if (argument.Kind == TypedConstantKind.Type && argument.Value is INamedTypeSymbol typeValue) + { + conditionType = typeValue; + } + else if (argument.Kind == TypedConstantKind.Primitive && argument.Value is string singleName) + { + memberNames.Add(singleName); + } + else if (argument.Kind == TypedConstantKind.Array) + { + foreach (TypedConstant element in argument.Values) + { + if (!element.IsNull && element.Value is string s) + { + memberNames.Add(s); + } + } + } + } + + if (conditionType is null || memberNames.Count == 0) + { + return; + } + + string typeName = conditionType.Name; + foreach (string memberName in memberNames) + { + ValidateMember(context, attributeSyntax, conditionType, typeName, memberName); + } + } + + private static void ValidateMember(SymbolAnalysisContext context, SyntaxNode attributeSyntax, INamedTypeSymbol conditionType, string typeName, string memberName) + { + if (string.IsNullOrWhiteSpace(memberName)) + { + // The runtime constructor already throws ArgumentException for null/empty/whitespace + // names. Nothing useful to validate here. + return; + } + + ImmutableArray candidates = LookupMember(conditionType, memberName); + if (candidates.IsEmpty) + { + context.ReportDiagnostic(attributeSyntax.CreateDiagnostic(MemberNotFoundRule, typeName, memberName)); + return; + } + + // Match the runtime resolution preference: property → field → method (first match wins). + ISymbol? selected = + candidates.FirstOrDefault(s => s.Kind == SymbolKind.Property) + ?? candidates.FirstOrDefault(s => s.Kind == SymbolKind.Field) + ?? candidates.FirstOrDefault(s => s.Kind == SymbolKind.Method); + + if (selected is null) + { + // A nested type, event, or other unsupported member kind is shadowing the name. + context.ReportDiagnostic(attributeSyntax.CreateDiagnostic(MemberWrongKindRule, typeName, memberName)); + return; + } + + switch (selected) + { + case IPropertySymbol property: + ValidateProperty(context, attributeSyntax, typeName, memberName, property); + break; + + case IFieldSymbol field: + ValidateField(context, attributeSyntax, typeName, memberName, field); + break; + + case IMethodSymbol method: + ValidateMethod(context, attributeSyntax, typeName, memberName, method); + break; + + default: + context.ReportDiagnostic(attributeSyntax.CreateDiagnostic(MemberWrongKindRule, typeName, memberName)); + break; + } + } + + private static ImmutableArray LookupMember(INamedTypeSymbol type, string memberName) + { + // Walk the type hierarchy so inherited public static members are also recognized, + // matching what reflection with `BindingFlags.Public | Static | FlattenHierarchy` would find. + ImmutableArray.Builder builder = ImmutableArray.CreateBuilder(); + INamedTypeSymbol? current = type; + while (current is not null) + { + foreach (ISymbol member in current.GetMembers(memberName)) + { + builder.Add(member); + } + + current = current.BaseType; + } + + return builder.ToImmutable(); + } + + private static void ValidateProperty(SymbolAnalysisContext context, SyntaxNode attributeSyntax, string typeName, string memberName, IPropertySymbol property) + { + if (property.DeclaredAccessibility != Accessibility.Public) + { + context.ReportDiagnostic(attributeSyntax.CreateDiagnostic(MemberNotPublicRule, typeName, memberName)); + return; + } + + if (!property.IsStatic) + { + context.ReportDiagnostic(attributeSyntax.CreateDiagnostic(MemberNotStaticRule, typeName, memberName)); + return; + } + + if (property.GetMethod is null || property.GetMethod.DeclaredAccessibility != Accessibility.Public) + { + context.ReportDiagnostic(attributeSyntax.CreateDiagnostic(PropertyNotReadableRule, typeName, memberName)); + return; + } + + if (property.Type.SpecialType != SpecialType.System_Boolean) + { + context.ReportDiagnostic(attributeSyntax.CreateDiagnostic(MemberWrongReturnTypeRule, typeName, memberName)); + } + } + + private static void ValidateField(SymbolAnalysisContext context, SyntaxNode attributeSyntax, string typeName, string memberName, IFieldSymbol field) + { + if (field.DeclaredAccessibility != Accessibility.Public) + { + context.ReportDiagnostic(attributeSyntax.CreateDiagnostic(MemberNotPublicRule, typeName, memberName)); + return; + } + + if (!field.IsStatic) + { + context.ReportDiagnostic(attributeSyntax.CreateDiagnostic(MemberNotStaticRule, typeName, memberName)); + return; + } + + if (field.Type.SpecialType != SpecialType.System_Boolean) + { + context.ReportDiagnostic(attributeSyntax.CreateDiagnostic(MemberWrongReturnTypeRule, typeName, memberName)); + } + } + + private static void ValidateMethod(SymbolAnalysisContext context, SyntaxNode attributeSyntax, string typeName, string memberName, IMethodSymbol method) + { + if (method.MethodKind != MethodKind.Ordinary) + { + context.ReportDiagnostic(attributeSyntax.CreateDiagnostic(MemberWrongKindRule, typeName, memberName)); + return; + } + + if (method.DeclaredAccessibility != Accessibility.Public) + { + context.ReportDiagnostic(attributeSyntax.CreateDiagnostic(MemberNotPublicRule, typeName, memberName)); + return; + } + + if (!method.IsStatic) + { + context.ReportDiagnostic(attributeSyntax.CreateDiagnostic(MemberNotStaticRule, typeName, memberName)); + return; + } + + if (method.Parameters.Length > 0) + { + context.ReportDiagnostic(attributeSyntax.CreateDiagnostic(MethodHasParametersRule, typeName, memberName)); + return; + } + + if (method.ReturnType.SpecialType != SpecialType.System_Boolean) + { + context.ReportDiagnostic(attributeSyntax.CreateDiagnostic(MemberWrongReturnTypeRule, typeName, memberName)); + } + } +} diff --git a/src/Analyzers/MSTest.Analyzers/Resources.resx b/src/Analyzers/MSTest.Analyzers/Resources.resx index 0db7f0d359..af9597b960 100644 --- a/src/Analyzers/MSTest.Analyzers/Resources.resx +++ b/src/Analyzers/MSTest.Analyzers/Resources.resx @@ -950,4 +950,40 @@ The type declaring these methods should also respect the following rules: An '[Ignore]' attribute applied to a test method or test class should include a non-empty message explaining why the test or class is ignored. A justification message makes it easier to triage skipped tests, helps reviewers understand the intent, and prevents tests from being silently disabled forever. {Locked="[Ignore]"}{Locked="class"} + + '[MemberCondition]' arguments should be valid + {Locked="[MemberCondition]"} + + + The members referenced by a '[MemberCondition]' attribute must exist on the referenced type, be 'public static', return 'bool', and (for methods) be parameterless. The attribute throws at runtime when these rules are violated; this analyzer surfaces the same problems at build time so typos and refactors do not silently break test gating. + {Locked="[MemberCondition]"}{Locked="public static"}{Locked="bool"} + + + '[MemberCondition]' member '{0}.{1}' cannot be found + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be 'public' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="public"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be 'static' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="static"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be a property, field, or parameterless method + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced member '{0}.{1}' must return 'bool' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="bool"} + + + '[MemberCondition]' referenced method '{0}.{1}' must be parameterless + {0} is the containing type name. {1} is the method name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced property '{0}.{1}' must have a getter + {0} is the containing type name. {1} is the property name. {Locked="[MemberCondition]"} + diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.cs.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.cs.xlf index b2f8fb7724..c3132e9b4b 100644 --- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.cs.xlf +++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.cs.xlf @@ -1,4 +1,4 @@ - + @@ -295,6 +295,51 @@ The type declaring these methods should also respect the following rules: Použijte Assert namísto CollectionAssert {Locked="CollectionAssert"}{Locked="Assert"} + + The members referenced by a '[MemberCondition]' attribute must exist on the referenced type, be 'public static', return 'bool', and (for methods) be parameterless. The attribute throws at runtime when these rules are violated; this analyzer surfaces the same problems at build time so typos and refactors do not silently break test gating. + The members referenced by a '[MemberCondition]' attribute must exist on the referenced type, be 'public static', return 'bool', and (for methods) be parameterless. The attribute throws at runtime when these rules are violated; this analyzer surfaces the same problems at build time so typos and refactors do not silently break test gating. + {Locked="[MemberCondition]"}{Locked="public static"}{Locked="bool"} + + + '[MemberCondition]' member '{0}.{1}' cannot be found + '[MemberCondition]' member '{0}.{1}' cannot be found + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be 'public' + '[MemberCondition]' referenced member '{0}.{1}' must be 'public' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="public"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be 'static' + '[MemberCondition]' referenced member '{0}.{1}' must be 'static' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="static"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be a property, field, or parameterless method + '[MemberCondition]' referenced member '{0}.{1}' must be a property, field, or parameterless method + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced member '{0}.{1}' must return 'bool' + '[MemberCondition]' referenced member '{0}.{1}' must return 'bool' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="bool"} + + + '[MemberCondition]' referenced method '{0}.{1}' must be parameterless + '[MemberCondition]' referenced method '{0}.{1}' must be parameterless + {0} is the containing type name. {1} is the method name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced property '{0}.{1}' must have a getter + '[MemberCondition]' referenced property '{0}.{1}' must have a getter + {0} is the containing type name. {1} is the property name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' arguments should be valid + '[MemberCondition]' arguments should be valid + {Locked="[MemberCondition]"} + DataRow entry should have the following layout to be valid: - should only be set on a test method; diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.de.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.de.xlf index 89a2aa5fe3..87669ddb46 100644 --- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.de.xlf +++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.de.xlf @@ -1,4 +1,4 @@ - + @@ -295,6 +295,51 @@ The type declaring these methods should also respect the following rules: Verwenden Sie „Assert“ anstelle von „CollectionAssert“ {Locked="CollectionAssert"}{Locked="Assert"} + + The members referenced by a '[MemberCondition]' attribute must exist on the referenced type, be 'public static', return 'bool', and (for methods) be parameterless. The attribute throws at runtime when these rules are violated; this analyzer surfaces the same problems at build time so typos and refactors do not silently break test gating. + The members referenced by a '[MemberCondition]' attribute must exist on the referenced type, be 'public static', return 'bool', and (for methods) be parameterless. The attribute throws at runtime when these rules are violated; this analyzer surfaces the same problems at build time so typos and refactors do not silently break test gating. + {Locked="[MemberCondition]"}{Locked="public static"}{Locked="bool"} + + + '[MemberCondition]' member '{0}.{1}' cannot be found + '[MemberCondition]' member '{0}.{1}' cannot be found + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be 'public' + '[MemberCondition]' referenced member '{0}.{1}' must be 'public' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="public"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be 'static' + '[MemberCondition]' referenced member '{0}.{1}' must be 'static' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="static"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be a property, field, or parameterless method + '[MemberCondition]' referenced member '{0}.{1}' must be a property, field, or parameterless method + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced member '{0}.{1}' must return 'bool' + '[MemberCondition]' referenced member '{0}.{1}' must return 'bool' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="bool"} + + + '[MemberCondition]' referenced method '{0}.{1}' must be parameterless + '[MemberCondition]' referenced method '{0}.{1}' must be parameterless + {0} is the containing type name. {1} is the method name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced property '{0}.{1}' must have a getter + '[MemberCondition]' referenced property '{0}.{1}' must have a getter + {0} is the containing type name. {1} is the property name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' arguments should be valid + '[MemberCondition]' arguments should be valid + {Locked="[MemberCondition]"} + DataRow entry should have the following layout to be valid: - should only be set on a test method; diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.es.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.es.xlf index f71bc0367e..624caa85e9 100644 --- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.es.xlf +++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.es.xlf @@ -1,4 +1,4 @@ - + @@ -295,6 +295,51 @@ The type declaring these methods should also respect the following rules: Usar "Assert" en lugar de "CollectionAssert" {Locked="CollectionAssert"}{Locked="Assert"} + + The members referenced by a '[MemberCondition]' attribute must exist on the referenced type, be 'public static', return 'bool', and (for methods) be parameterless. The attribute throws at runtime when these rules are violated; this analyzer surfaces the same problems at build time so typos and refactors do not silently break test gating. + The members referenced by a '[MemberCondition]' attribute must exist on the referenced type, be 'public static', return 'bool', and (for methods) be parameterless. The attribute throws at runtime when these rules are violated; this analyzer surfaces the same problems at build time so typos and refactors do not silently break test gating. + {Locked="[MemberCondition]"}{Locked="public static"}{Locked="bool"} + + + '[MemberCondition]' member '{0}.{1}' cannot be found + '[MemberCondition]' member '{0}.{1}' cannot be found + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be 'public' + '[MemberCondition]' referenced member '{0}.{1}' must be 'public' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="public"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be 'static' + '[MemberCondition]' referenced member '{0}.{1}' must be 'static' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="static"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be a property, field, or parameterless method + '[MemberCondition]' referenced member '{0}.{1}' must be a property, field, or parameterless method + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced member '{0}.{1}' must return 'bool' + '[MemberCondition]' referenced member '{0}.{1}' must return 'bool' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="bool"} + + + '[MemberCondition]' referenced method '{0}.{1}' must be parameterless + '[MemberCondition]' referenced method '{0}.{1}' must be parameterless + {0} is the containing type name. {1} is the method name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced property '{0}.{1}' must have a getter + '[MemberCondition]' referenced property '{0}.{1}' must have a getter + {0} is the containing type name. {1} is the property name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' arguments should be valid + '[MemberCondition]' arguments should be valid + {Locked="[MemberCondition]"} + DataRow entry should have the following layout to be valid: - should only be set on a test method; diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.fr.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.fr.xlf index 236d5160c7..c0cc28c4ac 100644 --- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.fr.xlf +++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.fr.xlf @@ -1,4 +1,4 @@ - + @@ -295,6 +295,51 @@ The type declaring these methods should also respect the following rules: Utilisez « Assert » au lieu de « CollectionAssert » {Locked="CollectionAssert"}{Locked="Assert"} + + The members referenced by a '[MemberCondition]' attribute must exist on the referenced type, be 'public static', return 'bool', and (for methods) be parameterless. The attribute throws at runtime when these rules are violated; this analyzer surfaces the same problems at build time so typos and refactors do not silently break test gating. + The members referenced by a '[MemberCondition]' attribute must exist on the referenced type, be 'public static', return 'bool', and (for methods) be parameterless. The attribute throws at runtime when these rules are violated; this analyzer surfaces the same problems at build time so typos and refactors do not silently break test gating. + {Locked="[MemberCondition]"}{Locked="public static"}{Locked="bool"} + + + '[MemberCondition]' member '{0}.{1}' cannot be found + '[MemberCondition]' member '{0}.{1}' cannot be found + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be 'public' + '[MemberCondition]' referenced member '{0}.{1}' must be 'public' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="public"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be 'static' + '[MemberCondition]' referenced member '{0}.{1}' must be 'static' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="static"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be a property, field, or parameterless method + '[MemberCondition]' referenced member '{0}.{1}' must be a property, field, or parameterless method + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced member '{0}.{1}' must return 'bool' + '[MemberCondition]' referenced member '{0}.{1}' must return 'bool' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="bool"} + + + '[MemberCondition]' referenced method '{0}.{1}' must be parameterless + '[MemberCondition]' referenced method '{0}.{1}' must be parameterless + {0} is the containing type name. {1} is the method name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced property '{0}.{1}' must have a getter + '[MemberCondition]' referenced property '{0}.{1}' must have a getter + {0} is the containing type name. {1} is the property name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' arguments should be valid + '[MemberCondition]' arguments should be valid + {Locked="[MemberCondition]"} + DataRow entry should have the following layout to be valid: - should only be set on a test method; diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.it.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.it.xlf index efcf5c9cbb..edc65b275e 100644 --- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.it.xlf +++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.it.xlf @@ -1,4 +1,4 @@ - + @@ -295,6 +295,51 @@ Anche il tipo che dichiara questi metodi deve rispettare le regole seguenti: Usare "Assert" invece di "CollectionAssert" {Locked="CollectionAssert"}{Locked="Assert"} + + The members referenced by a '[MemberCondition]' attribute must exist on the referenced type, be 'public static', return 'bool', and (for methods) be parameterless. The attribute throws at runtime when these rules are violated; this analyzer surfaces the same problems at build time so typos and refactors do not silently break test gating. + The members referenced by a '[MemberCondition]' attribute must exist on the referenced type, be 'public static', return 'bool', and (for methods) be parameterless. The attribute throws at runtime when these rules are violated; this analyzer surfaces the same problems at build time so typos and refactors do not silently break test gating. + {Locked="[MemberCondition]"}{Locked="public static"}{Locked="bool"} + + + '[MemberCondition]' member '{0}.{1}' cannot be found + '[MemberCondition]' member '{0}.{1}' cannot be found + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be 'public' + '[MemberCondition]' referenced member '{0}.{1}' must be 'public' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="public"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be 'static' + '[MemberCondition]' referenced member '{0}.{1}' must be 'static' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="static"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be a property, field, or parameterless method + '[MemberCondition]' referenced member '{0}.{1}' must be a property, field, or parameterless method + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced member '{0}.{1}' must return 'bool' + '[MemberCondition]' referenced member '{0}.{1}' must return 'bool' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="bool"} + + + '[MemberCondition]' referenced method '{0}.{1}' must be parameterless + '[MemberCondition]' referenced method '{0}.{1}' must be parameterless + {0} is the containing type name. {1} is the method name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced property '{0}.{1}' must have a getter + '[MemberCondition]' referenced property '{0}.{1}' must have a getter + {0} is the containing type name. {1} is the property name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' arguments should be valid + '[MemberCondition]' arguments should be valid + {Locked="[MemberCondition]"} + DataRow entry should have the following layout to be valid: - should only be set on a test method; diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.ja.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.ja.xlf index 9bd0e25a33..294a7b4f3a 100644 --- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.ja.xlf +++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.ja.xlf @@ -1,4 +1,4 @@ - + @@ -295,6 +295,51 @@ The type declaring these methods should also respect the following rules: 'CollectionAssert' の代わりに 'Assert' を使用する {Locked="CollectionAssert"}{Locked="Assert"} + + The members referenced by a '[MemberCondition]' attribute must exist on the referenced type, be 'public static', return 'bool', and (for methods) be parameterless. The attribute throws at runtime when these rules are violated; this analyzer surfaces the same problems at build time so typos and refactors do not silently break test gating. + The members referenced by a '[MemberCondition]' attribute must exist on the referenced type, be 'public static', return 'bool', and (for methods) be parameterless. The attribute throws at runtime when these rules are violated; this analyzer surfaces the same problems at build time so typos and refactors do not silently break test gating. + {Locked="[MemberCondition]"}{Locked="public static"}{Locked="bool"} + + + '[MemberCondition]' member '{0}.{1}' cannot be found + '[MemberCondition]' member '{0}.{1}' cannot be found + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be 'public' + '[MemberCondition]' referenced member '{0}.{1}' must be 'public' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="public"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be 'static' + '[MemberCondition]' referenced member '{0}.{1}' must be 'static' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="static"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be a property, field, or parameterless method + '[MemberCondition]' referenced member '{0}.{1}' must be a property, field, or parameterless method + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced member '{0}.{1}' must return 'bool' + '[MemberCondition]' referenced member '{0}.{1}' must return 'bool' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="bool"} + + + '[MemberCondition]' referenced method '{0}.{1}' must be parameterless + '[MemberCondition]' referenced method '{0}.{1}' must be parameterless + {0} is the containing type name. {1} is the method name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced property '{0}.{1}' must have a getter + '[MemberCondition]' referenced property '{0}.{1}' must have a getter + {0} is the containing type name. {1} is the property name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' arguments should be valid + '[MemberCondition]' arguments should be valid + {Locked="[MemberCondition]"} + DataRow entry should have the following layout to be valid: - should only be set on a test method; diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.ko.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.ko.xlf index 68815c26a1..eecdcdbdb8 100644 --- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.ko.xlf +++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.ko.xlf @@ -1,4 +1,4 @@ - + @@ -295,6 +295,51 @@ The type declaring these methods should also respect the following rules: 'CollectionAssert' 대신 'Assert'를 사용하세요. {Locked="CollectionAssert"}{Locked="Assert"} + + The members referenced by a '[MemberCondition]' attribute must exist on the referenced type, be 'public static', return 'bool', and (for methods) be parameterless. The attribute throws at runtime when these rules are violated; this analyzer surfaces the same problems at build time so typos and refactors do not silently break test gating. + The members referenced by a '[MemberCondition]' attribute must exist on the referenced type, be 'public static', return 'bool', and (for methods) be parameterless. The attribute throws at runtime when these rules are violated; this analyzer surfaces the same problems at build time so typos and refactors do not silently break test gating. + {Locked="[MemberCondition]"}{Locked="public static"}{Locked="bool"} + + + '[MemberCondition]' member '{0}.{1}' cannot be found + '[MemberCondition]' member '{0}.{1}' cannot be found + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be 'public' + '[MemberCondition]' referenced member '{0}.{1}' must be 'public' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="public"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be 'static' + '[MemberCondition]' referenced member '{0}.{1}' must be 'static' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="static"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be a property, field, or parameterless method + '[MemberCondition]' referenced member '{0}.{1}' must be a property, field, or parameterless method + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced member '{0}.{1}' must return 'bool' + '[MemberCondition]' referenced member '{0}.{1}' must return 'bool' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="bool"} + + + '[MemberCondition]' referenced method '{0}.{1}' must be parameterless + '[MemberCondition]' referenced method '{0}.{1}' must be parameterless + {0} is the containing type name. {1} is the method name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced property '{0}.{1}' must have a getter + '[MemberCondition]' referenced property '{0}.{1}' must have a getter + {0} is the containing type name. {1} is the property name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' arguments should be valid + '[MemberCondition]' arguments should be valid + {Locked="[MemberCondition]"} + DataRow entry should have the following layout to be valid: - should only be set on a test method; diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.pl.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.pl.xlf index c98ef31983..5fd7f07552 100644 --- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.pl.xlf +++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.pl.xlf @@ -1,4 +1,4 @@ - + @@ -295,6 +295,51 @@ The type declaring these methods should also respect the following rules: Użyj instrukcji „Assert” zamiast elementu „CollectionAssert” {Locked="CollectionAssert"}{Locked="Assert"} + + The members referenced by a '[MemberCondition]' attribute must exist on the referenced type, be 'public static', return 'bool', and (for methods) be parameterless. The attribute throws at runtime when these rules are violated; this analyzer surfaces the same problems at build time so typos and refactors do not silently break test gating. + The members referenced by a '[MemberCondition]' attribute must exist on the referenced type, be 'public static', return 'bool', and (for methods) be parameterless. The attribute throws at runtime when these rules are violated; this analyzer surfaces the same problems at build time so typos and refactors do not silently break test gating. + {Locked="[MemberCondition]"}{Locked="public static"}{Locked="bool"} + + + '[MemberCondition]' member '{0}.{1}' cannot be found + '[MemberCondition]' member '{0}.{1}' cannot be found + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be 'public' + '[MemberCondition]' referenced member '{0}.{1}' must be 'public' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="public"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be 'static' + '[MemberCondition]' referenced member '{0}.{1}' must be 'static' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="static"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be a property, field, or parameterless method + '[MemberCondition]' referenced member '{0}.{1}' must be a property, field, or parameterless method + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced member '{0}.{1}' must return 'bool' + '[MemberCondition]' referenced member '{0}.{1}' must return 'bool' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="bool"} + + + '[MemberCondition]' referenced method '{0}.{1}' must be parameterless + '[MemberCondition]' referenced method '{0}.{1}' must be parameterless + {0} is the containing type name. {1} is the method name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced property '{0}.{1}' must have a getter + '[MemberCondition]' referenced property '{0}.{1}' must have a getter + {0} is the containing type name. {1} is the property name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' arguments should be valid + '[MemberCondition]' arguments should be valid + {Locked="[MemberCondition]"} + DataRow entry should have the following layout to be valid: - should only be set on a test method; diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.pt-BR.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.pt-BR.xlf index 2ef921e5f3..68515e8476 100644 --- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.pt-BR.xlf +++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.pt-BR.xlf @@ -1,4 +1,4 @@ - + @@ -295,6 +295,51 @@ The type declaring these methods should also respect the following rules: Usar 'Assert' em vez de 'CollectionAssert' {Locked="CollectionAssert"}{Locked="Assert"} + + The members referenced by a '[MemberCondition]' attribute must exist on the referenced type, be 'public static', return 'bool', and (for methods) be parameterless. The attribute throws at runtime when these rules are violated; this analyzer surfaces the same problems at build time so typos and refactors do not silently break test gating. + The members referenced by a '[MemberCondition]' attribute must exist on the referenced type, be 'public static', return 'bool', and (for methods) be parameterless. The attribute throws at runtime when these rules are violated; this analyzer surfaces the same problems at build time so typos and refactors do not silently break test gating. + {Locked="[MemberCondition]"}{Locked="public static"}{Locked="bool"} + + + '[MemberCondition]' member '{0}.{1}' cannot be found + '[MemberCondition]' member '{0}.{1}' cannot be found + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be 'public' + '[MemberCondition]' referenced member '{0}.{1}' must be 'public' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="public"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be 'static' + '[MemberCondition]' referenced member '{0}.{1}' must be 'static' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="static"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be a property, field, or parameterless method + '[MemberCondition]' referenced member '{0}.{1}' must be a property, field, or parameterless method + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced member '{0}.{1}' must return 'bool' + '[MemberCondition]' referenced member '{0}.{1}' must return 'bool' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="bool"} + + + '[MemberCondition]' referenced method '{0}.{1}' must be parameterless + '[MemberCondition]' referenced method '{0}.{1}' must be parameterless + {0} is the containing type name. {1} is the method name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced property '{0}.{1}' must have a getter + '[MemberCondition]' referenced property '{0}.{1}' must have a getter + {0} is the containing type name. {1} is the property name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' arguments should be valid + '[MemberCondition]' arguments should be valid + {Locked="[MemberCondition]"} + DataRow entry should have the following layout to be valid: - should only be set on a test method; diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.ru.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.ru.xlf index 931012b83e..7b46e2628c 100644 --- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.ru.xlf +++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.ru.xlf @@ -1,4 +1,4 @@ - + @@ -295,6 +295,51 @@ The type declaring these methods should also respect the following rules: Использование "Assert" вместо "CollectionAssert" {Locked="CollectionAssert"}{Locked="Assert"} + + The members referenced by a '[MemberCondition]' attribute must exist on the referenced type, be 'public static', return 'bool', and (for methods) be parameterless. The attribute throws at runtime when these rules are violated; this analyzer surfaces the same problems at build time so typos and refactors do not silently break test gating. + The members referenced by a '[MemberCondition]' attribute must exist on the referenced type, be 'public static', return 'bool', and (for methods) be parameterless. The attribute throws at runtime when these rules are violated; this analyzer surfaces the same problems at build time so typos and refactors do not silently break test gating. + {Locked="[MemberCondition]"}{Locked="public static"}{Locked="bool"} + + + '[MemberCondition]' member '{0}.{1}' cannot be found + '[MemberCondition]' member '{0}.{1}' cannot be found + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be 'public' + '[MemberCondition]' referenced member '{0}.{1}' must be 'public' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="public"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be 'static' + '[MemberCondition]' referenced member '{0}.{1}' must be 'static' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="static"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be a property, field, or parameterless method + '[MemberCondition]' referenced member '{0}.{1}' must be a property, field, or parameterless method + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced member '{0}.{1}' must return 'bool' + '[MemberCondition]' referenced member '{0}.{1}' must return 'bool' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="bool"} + + + '[MemberCondition]' referenced method '{0}.{1}' must be parameterless + '[MemberCondition]' referenced method '{0}.{1}' must be parameterless + {0} is the containing type name. {1} is the method name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced property '{0}.{1}' must have a getter + '[MemberCondition]' referenced property '{0}.{1}' must have a getter + {0} is the containing type name. {1} is the property name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' arguments should be valid + '[MemberCondition]' arguments should be valid + {Locked="[MemberCondition]"} + DataRow entry should have the following layout to be valid: - should only be set on a test method; diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.tr.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.tr.xlf index 8714361ba3..a575d33ed4 100644 --- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.tr.xlf +++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.tr.xlf @@ -1,4 +1,4 @@ - + @@ -295,6 +295,51 @@ The type declaring these methods should also respect the following rules: 'CollectionAssert' yerine 'Assert' kullanın {Locked="CollectionAssert"}{Locked="Assert"} + + The members referenced by a '[MemberCondition]' attribute must exist on the referenced type, be 'public static', return 'bool', and (for methods) be parameterless. The attribute throws at runtime when these rules are violated; this analyzer surfaces the same problems at build time so typos and refactors do not silently break test gating. + The members referenced by a '[MemberCondition]' attribute must exist on the referenced type, be 'public static', return 'bool', and (for methods) be parameterless. The attribute throws at runtime when these rules are violated; this analyzer surfaces the same problems at build time so typos and refactors do not silently break test gating. + {Locked="[MemberCondition]"}{Locked="public static"}{Locked="bool"} + + + '[MemberCondition]' member '{0}.{1}' cannot be found + '[MemberCondition]' member '{0}.{1}' cannot be found + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be 'public' + '[MemberCondition]' referenced member '{0}.{1}' must be 'public' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="public"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be 'static' + '[MemberCondition]' referenced member '{0}.{1}' must be 'static' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="static"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be a property, field, or parameterless method + '[MemberCondition]' referenced member '{0}.{1}' must be a property, field, or parameterless method + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced member '{0}.{1}' must return 'bool' + '[MemberCondition]' referenced member '{0}.{1}' must return 'bool' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="bool"} + + + '[MemberCondition]' referenced method '{0}.{1}' must be parameterless + '[MemberCondition]' referenced method '{0}.{1}' must be parameterless + {0} is the containing type name. {1} is the method name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced property '{0}.{1}' must have a getter + '[MemberCondition]' referenced property '{0}.{1}' must have a getter + {0} is the containing type name. {1} is the property name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' arguments should be valid + '[MemberCondition]' arguments should be valid + {Locked="[MemberCondition]"} + DataRow entry should have the following layout to be valid: - should only be set on a test method; diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.zh-Hans.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.zh-Hans.xlf index 5bd0b32459..449f14dff7 100644 --- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.zh-Hans.xlf +++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.zh-Hans.xlf @@ -1,4 +1,4 @@ - + @@ -295,6 +295,51 @@ The type declaring these methods should also respect the following rules: 使用 "Assert" 而非 "CollectionAssert" {Locked="CollectionAssert"}{Locked="Assert"} + + The members referenced by a '[MemberCondition]' attribute must exist on the referenced type, be 'public static', return 'bool', and (for methods) be parameterless. The attribute throws at runtime when these rules are violated; this analyzer surfaces the same problems at build time so typos and refactors do not silently break test gating. + The members referenced by a '[MemberCondition]' attribute must exist on the referenced type, be 'public static', return 'bool', and (for methods) be parameterless. The attribute throws at runtime when these rules are violated; this analyzer surfaces the same problems at build time so typos and refactors do not silently break test gating. + {Locked="[MemberCondition]"}{Locked="public static"}{Locked="bool"} + + + '[MemberCondition]' member '{0}.{1}' cannot be found + '[MemberCondition]' member '{0}.{1}' cannot be found + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be 'public' + '[MemberCondition]' referenced member '{0}.{1}' must be 'public' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="public"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be 'static' + '[MemberCondition]' referenced member '{0}.{1}' must be 'static' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="static"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be a property, field, or parameterless method + '[MemberCondition]' referenced member '{0}.{1}' must be a property, field, or parameterless method + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced member '{0}.{1}' must return 'bool' + '[MemberCondition]' referenced member '{0}.{1}' must return 'bool' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="bool"} + + + '[MemberCondition]' referenced method '{0}.{1}' must be parameterless + '[MemberCondition]' referenced method '{0}.{1}' must be parameterless + {0} is the containing type name. {1} is the method name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced property '{0}.{1}' must have a getter + '[MemberCondition]' referenced property '{0}.{1}' must have a getter + {0} is the containing type name. {1} is the property name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' arguments should be valid + '[MemberCondition]' arguments should be valid + {Locked="[MemberCondition]"} + DataRow entry should have the following layout to be valid: - should only be set on a test method; diff --git a/src/Analyzers/MSTest.Analyzers/xlf/Resources.zh-Hant.xlf b/src/Analyzers/MSTest.Analyzers/xlf/Resources.zh-Hant.xlf index 494ee01b4c..afe1d93a42 100644 --- a/src/Analyzers/MSTest.Analyzers/xlf/Resources.zh-Hant.xlf +++ b/src/Analyzers/MSTest.Analyzers/xlf/Resources.zh-Hant.xlf @@ -1,4 +1,4 @@ - + @@ -295,6 +295,51 @@ The type declaring these methods should also respect the following rules: 使用 'Assert' 而不是 'CollectionAssert' {Locked="CollectionAssert"}{Locked="Assert"} + + The members referenced by a '[MemberCondition]' attribute must exist on the referenced type, be 'public static', return 'bool', and (for methods) be parameterless. The attribute throws at runtime when these rules are violated; this analyzer surfaces the same problems at build time so typos and refactors do not silently break test gating. + The members referenced by a '[MemberCondition]' attribute must exist on the referenced type, be 'public static', return 'bool', and (for methods) be parameterless. The attribute throws at runtime when these rules are violated; this analyzer surfaces the same problems at build time so typos and refactors do not silently break test gating. + {Locked="[MemberCondition]"}{Locked="public static"}{Locked="bool"} + + + '[MemberCondition]' member '{0}.{1}' cannot be found + '[MemberCondition]' member '{0}.{1}' cannot be found + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be 'public' + '[MemberCondition]' referenced member '{0}.{1}' must be 'public' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="public"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be 'static' + '[MemberCondition]' referenced member '{0}.{1}' must be 'static' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="static"} + + + '[MemberCondition]' referenced member '{0}.{1}' must be a property, field, or parameterless method + '[MemberCondition]' referenced member '{0}.{1}' must be a property, field, or parameterless method + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced member '{0}.{1}' must return 'bool' + '[MemberCondition]' referenced member '{0}.{1}' must return 'bool' + {0} is the containing type name. {1} is the member name. {Locked="[MemberCondition]"}{Locked="bool"} + + + '[MemberCondition]' referenced method '{0}.{1}' must be parameterless + '[MemberCondition]' referenced method '{0}.{1}' must be parameterless + {0} is the containing type name. {1} is the method name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' referenced property '{0}.{1}' must have a getter + '[MemberCondition]' referenced property '{0}.{1}' must have a getter + {0} is the containing type name. {1} is the property name. {Locked="[MemberCondition]"} + + + '[MemberCondition]' arguments should be valid + '[MemberCondition]' arguments should be valid + {Locked="[MemberCondition]"} + DataRow entry should have the following layout to be valid: - should only be set on a test method; diff --git a/test/UnitTests/MSTest.Analyzers.UnitTests/MemberConditionShouldBeValidAnalyzerTests.cs b/test/UnitTests/MSTest.Analyzers.UnitTests/MemberConditionShouldBeValidAnalyzerTests.cs new file mode 100644 index 0000000000..7c8acfea80 --- /dev/null +++ b/test/UnitTests/MSTest.Analyzers.UnitTests/MemberConditionShouldBeValidAnalyzerTests.cs @@ -0,0 +1,497 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using VerifyCS = MSTest.Analyzers.Test.CSharpCodeFixVerifier< + MSTest.Analyzers.MemberConditionShouldBeValidAnalyzer, + Microsoft.CodeAnalysis.Testing.EmptyCodeFixProvider>; + +namespace MSTest.Analyzers.Test; + +[TestClass] +public sealed class MemberConditionShouldBeValidAnalyzerTests +{ + [TestMethod] + public async Task WhenMemberIsValidPublicStaticBoolProperty_NoDiagnostic() + { + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + public static class Conditions + { + public static bool IsTrue => true; + } + + [TestClass] + public class MyTestClass + { + [MemberCondition(typeof(Conditions), nameof(Conditions.IsTrue))] + [TestMethod] + public void TestMethod() { } + } + """; + + await VerifyCS.VerifyAnalyzerAsync(code); + } + + [TestMethod] + public async Task WhenMemberIsValidPublicStaticBoolField_NoDiagnostic() + { + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + public static class Conditions + { + public static readonly bool IsTrue = true; + } + + [TestClass] + public class MyTestClass + { + [MemberCondition(typeof(Conditions), nameof(Conditions.IsTrue))] + [TestMethod] + public void TestMethod() { } + } + """; + + await VerifyCS.VerifyAnalyzerAsync(code); + } + + [TestMethod] + public async Task WhenMemberIsValidPublicStaticBoolMethod_NoDiagnostic() + { + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + public static class Conditions + { + public static bool IsTrue() => true; + } + + [TestClass] + public class MyTestClass + { + [MemberCondition(typeof(Conditions), nameof(Conditions.IsTrue))] + [TestMethod] + public void TestMethod() { } + } + """; + + await VerifyCS.VerifyAnalyzerAsync(code); + } + + [TestMethod] + public async Task WhenAttributeIsOnTestClass_StillValidated() + { + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + public static class Conditions + { + public static bool IsTrue => true; + } + + [{|#0:MemberCondition(typeof(Conditions), "DoesNotExist")|}] + [TestClass] + public class MyTestClass + { + [TestMethod] + public void TestMethod() { } + } + """; + + await VerifyCS.VerifyAnalyzerAsync( + code, + VerifyCS.Diagnostic(MemberConditionShouldBeValidAnalyzer.MemberNotFoundRule) + .WithLocation(0) + .WithArguments("Conditions", "DoesNotExist")); + } + + [TestMethod] + public async Task WhenMemberDoesNotExist_MemberNotFound() + { + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + public static class Conditions + { + public static bool IsTrue => true; + } + + [TestClass] + public class MyTestClass + { + [{|#0:MemberCondition(typeof(Conditions), "DoesNotExist")|}] + [TestMethod] + public void TestMethod() { } + } + """; + + await VerifyCS.VerifyAnalyzerAsync( + code, + VerifyCS.Diagnostic(MemberConditionShouldBeValidAnalyzer.MemberNotFoundRule) + .WithLocation(0) + .WithArguments("Conditions", "DoesNotExist")); + } + + [TestMethod] + public async Task WhenMemberIsInternal_MemberNotPublic() + { + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + public static class Conditions + { + internal static bool InternalIsTrue => true; + } + + [TestClass] + public class MyTestClass + { + [{|#0:MemberCondition(typeof(Conditions), nameof(Conditions.InternalIsTrue))|}] + [TestMethod] + public void TestMethod() { } + } + """; + + await VerifyCS.VerifyAnalyzerAsync( + code, + VerifyCS.Diagnostic(MemberConditionShouldBeValidAnalyzer.MemberNotPublicRule) + .WithLocation(0) + .WithArguments("Conditions", "InternalIsTrue")); + } + + [TestMethod] + public async Task WhenPropertyIsInstance_MemberNotStatic() + { + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + public class Conditions + { + public bool InstanceIsTrue => true; + } + + [TestClass] + public class MyTestClass + { + [{|#0:MemberCondition(typeof(Conditions), nameof(Conditions.InstanceIsTrue))|}] + [TestMethod] + public void TestMethod() { } + } + """; + + await VerifyCS.VerifyAnalyzerAsync( + code, + VerifyCS.Diagnostic(MemberConditionShouldBeValidAnalyzer.MemberNotStaticRule) + .WithLocation(0) + .WithArguments("Conditions", "InstanceIsTrue")); + } + + [TestMethod] + public async Task WhenPropertyReturnsNonBool_MemberWrongReturnType() + { + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + public static class Conditions + { + public static int NotBool => 42; + } + + [TestClass] + public class MyTestClass + { + [{|#0:MemberCondition(typeof(Conditions), nameof(Conditions.NotBool))|}] + [TestMethod] + public void TestMethod() { } + } + """; + + await VerifyCS.VerifyAnalyzerAsync( + code, + VerifyCS.Diagnostic(MemberConditionShouldBeValidAnalyzer.MemberWrongReturnTypeRule) + .WithLocation(0) + .WithArguments("Conditions", "NotBool")); + } + + [TestMethod] + public async Task WhenMethodHasParameters_MethodHasParameters() + { + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + public static class Conditions + { + public static bool WithParam(int x) => x > 0; + } + + [TestClass] + public class MyTestClass + { + [{|#0:MemberCondition(typeof(Conditions), nameof(Conditions.WithParam))|}] + [TestMethod] + public void TestMethod() { } + } + """; + + await VerifyCS.VerifyAnalyzerAsync( + code, + VerifyCS.Diagnostic(MemberConditionShouldBeValidAnalyzer.MethodHasParametersRule) + .WithLocation(0) + .WithArguments("Conditions", "WithParam")); + } + + [TestMethod] + public async Task WhenMethodReturnsNonBool_MemberWrongReturnType() + { + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + public static class Conditions + { + public static int NotBoolMethod() => 0; + } + + [TestClass] + public class MyTestClass + { + [{|#0:MemberCondition(typeof(Conditions), nameof(Conditions.NotBoolMethod))|}] + [TestMethod] + public void TestMethod() { } + } + """; + + await VerifyCS.VerifyAnalyzerAsync( + code, + VerifyCS.Diagnostic(MemberConditionShouldBeValidAnalyzer.MemberWrongReturnTypeRule) + .WithLocation(0) + .WithArguments("Conditions", "NotBoolMethod")); + } + + [TestMethod] + public async Task WhenPropertyIsWriteOnly_PropertyNotReadable() + { + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + public static class Conditions + { + public static bool WriteOnly { set { } } + } + + [TestClass] + public class MyTestClass + { + [{|#0:MemberCondition(typeof(Conditions), nameof(Conditions.WriteOnly))|}] + [TestMethod] + public void TestMethod() { } + } + """; + + await VerifyCS.VerifyAnalyzerAsync( + code, + VerifyCS.Diagnostic(MemberConditionShouldBeValidAnalyzer.PropertyNotReadableRule) + .WithLocation(0) + .WithArguments("Conditions", "WriteOnly")); + } + + [TestMethod] + public async Task WhenFieldIsNonBool_MemberWrongReturnType() + { + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + public static class Conditions + { + public static readonly string NotBoolField = "x"; + } + + [TestClass] + public class MyTestClass + { + [{|#0:MemberCondition(typeof(Conditions), nameof(Conditions.NotBoolField))|}] + [TestMethod] + public void TestMethod() { } + } + """; + + await VerifyCS.VerifyAnalyzerAsync( + code, + VerifyCS.Diagnostic(MemberConditionShouldBeValidAnalyzer.MemberWrongReturnTypeRule) + .WithLocation(0) + .WithArguments("Conditions", "NotBoolField")); + } + + [TestMethod] + public async Task WhenAdditionalMembersAreInvalid_AllReported() + { + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + public static class Conditions + { + public static bool IsTrue => true; + public static int NotBool => 0; + } + + [TestClass] + public class MyTestClass + { + [{|#0:MemberCondition(typeof(Conditions), nameof(Conditions.IsTrue), "Missing", nameof(Conditions.NotBool))|}] + [TestMethod] + public void TestMethod() { } + } + """; + + await VerifyCS.VerifyAnalyzerAsync( + code, + VerifyCS.Diagnostic(MemberConditionShouldBeValidAnalyzer.MemberNotFoundRule) + .WithLocation(0) + .WithArguments("Conditions", "Missing"), + VerifyCS.Diagnostic(MemberConditionShouldBeValidAnalyzer.MemberWrongReturnTypeRule) + .WithLocation(0) + .WithArguments("Conditions", "NotBool")); + } + + [TestMethod] + public async Task WhenExplicitConditionMode_StillValidated() + { + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + public static class Conditions + { + public static bool IsTrue => true; + } + + [TestClass] + public class MyTestClass + { + [{|#0:MemberCondition(ConditionMode.Exclude, typeof(Conditions), "DoesNotExist")|}] + [TestMethod] + public void TestMethod() { } + } + """; + + await VerifyCS.VerifyAnalyzerAsync( + code, + VerifyCS.Diagnostic(MemberConditionShouldBeValidAnalyzer.MemberNotFoundRule) + .WithLocation(0) + .WithArguments("Conditions", "DoesNotExist")); + } + + [TestMethod] + public async Task WhenInheritedPublicStaticMember_NoDiagnostic() + { + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + public class BaseConditions + { + public static bool InheritedIsTrue => true; + } + + public class DerivedConditions : BaseConditions + { + } + + [TestClass] + public class MyTestClass + { + [MemberCondition(typeof(DerivedConditions), nameof(DerivedConditions.InheritedIsTrue))] + [TestMethod] + public void TestMethod() { } + } + """; + + await VerifyCS.VerifyAnalyzerAsync(code); + } + + [TestMethod] + public async Task WhenMemberIsEvent_MemberWrongKind() + { + string code = """ + using System; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + public static class Conditions + { + public static event EventHandler SomeEvent; + + public static void Raise() => SomeEvent?.Invoke(null, EventArgs.Empty); + } + + [TestClass] + public class MyTestClass + { + [{|#0:MemberCondition(typeof(Conditions), nameof(Conditions.SomeEvent))|}] + [TestMethod] + public void TestMethod() { } + } + """; + + await VerifyCS.VerifyAnalyzerAsync( + code, + VerifyCS.Diagnostic(MemberConditionShouldBeValidAnalyzer.MemberWrongKindRule) + .WithLocation(0) + .WithArguments("Conditions", "SomeEvent")); + } + + [TestMethod] + public async Task WhenStaticFieldIsInstance_MemberNotStatic() + { + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + public class Conditions + { + public bool InstanceField = true; + } + + [TestClass] + public class MyTestClass + { + [{|#0:MemberCondition(typeof(Conditions), nameof(Conditions.InstanceField))|}] + [TestMethod] + public void TestMethod() { } + } + """; + + await VerifyCS.VerifyAnalyzerAsync( + code, + VerifyCS.Diagnostic(MemberConditionShouldBeValidAnalyzer.MemberNotStaticRule) + .WithLocation(0) + .WithArguments("Conditions", "InstanceField")); + } + + [TestMethod] + public async Task WhenMultipleConditionAttributes_EachValidated() + { + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + public static class Conditions + { + public static bool IsTrue => true; + } + + [TestClass] + public class MyTestClass + { + [{|#0:MemberCondition(typeof(Conditions), "First")|}] + [{|#1:MemberCondition(typeof(Conditions), "Second")|}] + [TestMethod] + public void TestMethod() { } + } + """; + + await VerifyCS.VerifyAnalyzerAsync( + code, + VerifyCS.Diagnostic(MemberConditionShouldBeValidAnalyzer.MemberNotFoundRule) + .WithLocation(0) + .WithArguments("Conditions", "First"), + VerifyCS.Diagnostic(MemberConditionShouldBeValidAnalyzer.MemberNotFoundRule) + .WithLocation(1) + .WithArguments("Conditions", "Second")); + } +} From 1fccdc3bd3e6b0b9ca958c041d76eacb88049db6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 12 Jun 2026 18:37:26 +0200 Subject: [PATCH 8/9] Address review on #9076: mirror runtime member resolution + reject indexers - `MemberConditionShouldBeValidAnalyzer`: candidate selection in `ValidateMember` no longer just picks "first by Kind". It now first tries to find a candidate that the runtime would actually bind to (`GetProperty`/`GetField`/`GetMethod` with `Public | Static | FlattenHierarchy`, methods restricted to `MethodKind.Ordinary` and parameterless), and only falls back to the previous first-by-kind heuristic when no such candidate exists. This fixes: * Overloaded methods where a parameterless overload coexists with parameterized ones (runtime binds to the parameterless one). * An instance member on a derived type shadowing a `public static` base member with the same name (runtime resolves via `FlattenHierarchy` to the base static). Behaviour for "no runtime-valid candidate" cases is preserved -- the fallback still selects a candidate so the more specific diagnostic (not-public/not-static/wrong-return-type) is reported. - `ValidateProperty`: explicitly reject indexer properties (`property.IsIndexer`) with `MemberWrongKindRule`, matching the runtime `InvalidOperationException` for properties with index parameters. (The analyzer was previously silent on this category and would let MSTEST0070 miss cases that would still throw at runtime.) - Apply `Where` filter to the `argument.Values` enumeration to address the github-code-quality LINQ note and avoid the implicit filtering pattern. - Tests: add `WhenMethodHasParameterlessAndParameterizedOverloads_PicksParameterless_NoDiagnostic` and `WhenDerivedHasInstanceMemberShadowingBaseStatic_PicksBaseStatic_NoDiagnostic`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../MemberConditionShouldBeValidAnalyzer.cs | 42 +++++++++++--- ...mberConditionShouldBeValidAnalyzerTests.cs | 57 +++++++++++++++++++ 2 files changed, 90 insertions(+), 9 deletions(-) diff --git a/src/Analyzers/MSTest.Analyzers/MemberConditionShouldBeValidAnalyzer.cs b/src/Analyzers/MSTest.Analyzers/MemberConditionShouldBeValidAnalyzer.cs index 87dccc1766..1275c97b30 100644 --- a/src/Analyzers/MSTest.Analyzers/MemberConditionShouldBeValidAnalyzer.cs +++ b/src/Analyzers/MSTest.Analyzers/MemberConditionShouldBeValidAnalyzer.cs @@ -129,12 +129,9 @@ private static void AnalyzeAttribute(SymbolAnalysisContext context, AttributeDat } else if (argument.Kind == TypedConstantKind.Array) { - foreach (TypedConstant element in argument.Values) + foreach (TypedConstant element in argument.Values.Where(static e => !e.IsNull && e.Value is string)) { - if (!element.IsNull && element.Value is string s) - { - memberNames.Add(s); - } + memberNames.Add((string)element.Value!); } } } @@ -167,11 +164,30 @@ private static void ValidateMember(SymbolAnalysisContext context, SyntaxNode att return; } - // Match the runtime resolution preference: property → field → method (first match wins). + // Match the runtime resolution order: GetProperty / GetField / GetMethod with + // BindingFlags.Public | Static | FlattenHierarchy, where GetMethod looks for the + // *parameterless* overload only. If the runtime would bind to a candidate that + // satisfies those filters, prefer it so we don't report a false positive against an + // instance member or a parameterized overload that shadows the real binding target. ISymbol? selected = - candidates.FirstOrDefault(s => s.Kind == SymbolKind.Property) - ?? candidates.FirstOrDefault(s => s.Kind == SymbolKind.Field) - ?? candidates.FirstOrDefault(s => s.Kind == SymbolKind.Method); + // Property: public + static + non-indexer (runtime rejects indexers). + candidates.OfType() + .FirstOrDefault(static p => p.DeclaredAccessibility == Accessibility.Public && p.IsStatic && !p.IsIndexer) + // Field: public + static. + ?? (ISymbol?)candidates.OfType() + .FirstOrDefault(static f => f.DeclaredAccessibility == Accessibility.Public && f.IsStatic) + // Method: public + static + ordinary + parameterless. + ?? candidates.OfType() + .FirstOrDefault(static m => + m.DeclaredAccessibility == Accessibility.Public + && m.IsStatic + && m.MethodKind == MethodKind.Ordinary + && m.Parameters.Length == 0) + // Fallback when no runtime-binding candidate exists: pick the first member by + // kind so the more specific diagnostic (not-public, not-static, etc.) is reported. + ?? candidates.FirstOrDefault(static s => s.Kind == SymbolKind.Property) + ?? candidates.FirstOrDefault(static s => s.Kind == SymbolKind.Field) + ?? candidates.FirstOrDefault(static s => s.Kind == SymbolKind.Method); if (selected is null) { @@ -221,6 +237,14 @@ private static ImmutableArray LookupMember(INamedTypeSymbol type, strin private static void ValidateProperty(SymbolAnalysisContext context, SyntaxNode attributeSyntax, string typeName, string memberName, IPropertySymbol property) { + if (property.IsIndexer) + { + // Indexer properties (e.g. public static bool this[int i] in C# 13+) are rejected by + // the runtime because the attribute requires a *parameterless* readable property. + context.ReportDiagnostic(attributeSyntax.CreateDiagnostic(MemberWrongKindRule, typeName, memberName)); + return; + } + if (property.DeclaredAccessibility != Accessibility.Public) { context.ReportDiagnostic(attributeSyntax.CreateDiagnostic(MemberNotPublicRule, typeName, memberName)); diff --git a/test/UnitTests/MSTest.Analyzers.UnitTests/MemberConditionShouldBeValidAnalyzerTests.cs b/test/UnitTests/MSTest.Analyzers.UnitTests/MemberConditionShouldBeValidAnalyzerTests.cs index 7c8acfea80..3bc6a666dd 100644 --- a/test/UnitTests/MSTest.Analyzers.UnitTests/MemberConditionShouldBeValidAnalyzerTests.cs +++ b/test/UnitTests/MSTest.Analyzers.UnitTests/MemberConditionShouldBeValidAnalyzerTests.cs @@ -494,4 +494,61 @@ await VerifyCS.VerifyAnalyzerAsync( .WithLocation(1) .WithArguments("Conditions", "Second")); } + + [TestMethod] + public async Task WhenMethodHasParameterlessAndParameterizedOverloads_PicksParameterless_NoDiagnostic() + { + // Runtime binding uses Type.GetMethod(name, ..., types: Type.EmptyTypes), which selects + // the parameterless overload. The analyzer must not falsely flag this just because the + // parameterized overload comes first in declaration order. + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + public static class Conditions + { + public static bool IsTrue(int unused) => true; + public static bool IsTrue() => true; + } + + [TestClass] + public class MyTestClass + { + [MemberCondition(typeof(Conditions), nameof(Conditions.IsTrue))] + [TestMethod] + public void TestMethod() { } + } + """; + + await VerifyCS.VerifyAnalyzerAsync(code); + } + + [TestMethod] + public async Task WhenDerivedHasInstanceMemberShadowingBaseStatic_PicksBaseStatic_NoDiagnostic() + { + // Runtime FlattenHierarchy + Public + Static binds to Base.IsTrue (the static one), + // not Derived.IsTrue (the instance one). The analyzer must mirror that. + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + public class Base + { + public static bool IsTrue => true; + } + + public class Derived : Base + { + public new bool IsTrue => false; // instance, shadows the static base member + } + + [TestClass] + public class MyTestClass + { + [MemberCondition(typeof(Derived), nameof(Derived.IsTrue))] + [TestMethod] + public void TestMethod() { } + } + """; + + await VerifyCS.VerifyAnalyzerAsync(code); + } } From fc21eeed9ba00d83cdaacb94ff74c7205b180277 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 12 Jun 2026 19:05:59 +0200 Subject: [PATCH 9/9] Address review on #9076: sort WellKnownTypeNames, report MSTEST0070 for non-named types, rename test - `WellKnownTypeNames.cs`: move `MicrosoftVisualStudioTestToolsUnitTestingOSConditionAttribute` to its alphabetical position between `Member...` and `Owner...` so the file matches its "Keep sorted alphabetically" header. - `MemberConditionShouldBeValidAnalyzer.AnalyzeAttribute`: accept any `ITypeSymbol` for the condition type rather than only `INamedTypeSymbol`. Non-named types (arrays, pointers, function pointers) can't carry user-declared static bool members and the runtime throws `InvalidOperationException`; the analyzer now surfaces that as MSTEST0070 (`MemberNotFoundRule`) instead of silently skipping the attribute. Uses `ToDisplayString(MinimallyQualifiedFormat)` for the type name when `conditionType.Name` is empty (as it is for `IArrayTypeSymbol`). - Rename `WhenStaticFieldIsInstance_MemberNotStatic` to `WhenFieldIsInstance_MemberNotStatic` -- the field under test is an instance field, not a static one, so the original name was misleading. - Add `WhenConditionTypeIsArrayType_MemberNotFound` test covering `[MemberCondition(typeof(int[]), "AnyName")]`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Helpers/WellKnownTypeNames.cs | 2 +- .../MemberConditionShouldBeValidAnalyzer.cs | 27 ++++++++++++++++--- ...mberConditionShouldBeValidAnalyzerTests.cs | 27 ++++++++++++++++++- 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/Analyzers/MSTest.Analyzers/Helpers/WellKnownTypeNames.cs b/src/Analyzers/MSTest.Analyzers/Helpers/WellKnownTypeNames.cs index 4eca392aa2..1336545d01 100644 --- a/src/Analyzers/MSTest.Analyzers/Helpers/WellKnownTypeNames.cs +++ b/src/Analyzers/MSTest.Analyzers/Helpers/WellKnownTypeNames.cs @@ -30,6 +30,7 @@ internal static class WellKnownTypeNames public const string MicrosoftVisualStudioTestToolsUnitTestingIgnoreAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.IgnoreAttribute"; public const string MicrosoftVisualStudioTestToolsUnitTestingInheritanceBehavior = "Microsoft.VisualStudio.TestTools.UnitTesting.InheritanceBehavior"; public const string MicrosoftVisualStudioTestToolsUnitTestingMemberConditionAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.MemberConditionAttribute"; + public const string MicrosoftVisualStudioTestToolsUnitTestingOSConditionAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.OSConditionAttribute"; public const string MicrosoftVisualStudioTestToolsUnitTestingOwnerAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.OwnerAttribute"; public const string MicrosoftVisualStudioTestToolsUnitTestingParallelizeAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.ParallelizeAttribute"; public const string MicrosoftVisualStudioTestToolsUnitTestingPriorityAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.PriorityAttribute"; @@ -43,7 +44,6 @@ internal static class WellKnownTypeNames public const string MicrosoftVisualStudioTestToolsUnitTestingTestPropertyAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.TestPropertyAttribute"; public const string MicrosoftVisualStudioTestToolsUnitTestingTimeoutAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.TimeoutAttribute"; public const string MicrosoftVisualStudioTestToolsUnitTestingWorkItemAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.WorkItemAttribute"; - public const string MicrosoftVisualStudioTestToolsUnitTestingOSConditionAttribute = "Microsoft.VisualStudio.TestTools.UnitTesting.OSConditionAttribute"; public const string System = "System"; public const string SystemRuntimeInteropServicesRuntimeInformation = "System.Runtime.InteropServices.RuntimeInformation"; diff --git a/src/Analyzers/MSTest.Analyzers/MemberConditionShouldBeValidAnalyzer.cs b/src/Analyzers/MSTest.Analyzers/MemberConditionShouldBeValidAnalyzer.cs index 1275c97b30..297a9ae60f 100644 --- a/src/Analyzers/MSTest.Analyzers/MemberConditionShouldBeValidAnalyzer.cs +++ b/src/Analyzers/MSTest.Analyzers/MemberConditionShouldBeValidAnalyzer.cs @@ -110,7 +110,7 @@ private static void AnalyzeAttribute(SymbolAnalysisContext context, AttributeDat // (ConditionMode, Type, string), (ConditionMode, Type, string, params string[]) ) // we can identify the condition type, the first member name, and the optional params array // by inspecting argument kinds and types. - INamedTypeSymbol? conditionType = null; + ITypeSymbol? conditionType = null; var memberNames = new List(); foreach (TypedConstant argument in attribute.ConstructorArguments) { @@ -119,7 +119,7 @@ private static void AnalyzeAttribute(SymbolAnalysisContext context, AttributeDat continue; } - if (argument.Kind == TypedConstantKind.Type && argument.Value is INamedTypeSymbol typeValue) + if (argument.Kind == TypedConstantKind.Type && argument.Value is ITypeSymbol typeValue) { conditionType = typeValue; } @@ -142,9 +142,30 @@ private static void AnalyzeAttribute(SymbolAnalysisContext context, AttributeDat } string typeName = conditionType.Name; + + // Non-named types (arrays, pointers, function pointers) can't carry user-declared static + // bool members the way [MemberCondition] requires. The runtime will throw + // ``InvalidOperationException`` at first ``IsConditionMet`` access; surface that as + // MSTEST0070 (MemberNotFound) here so the user sees it at edit-time. + if (conditionType is not INamedTypeSymbol namedConditionType) + { + string nonNamedTypeName = string.IsNullOrEmpty(conditionType.Name) + ? conditionType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat) + : conditionType.Name; + foreach (string memberName in memberNames) + { + if (!string.IsNullOrWhiteSpace(memberName)) + { + context.ReportDiagnostic(attributeSyntax.CreateDiagnostic(MemberNotFoundRule, nonNamedTypeName, memberName)); + } + } + + return; + } + foreach (string memberName in memberNames) { - ValidateMember(context, attributeSyntax, conditionType, typeName, memberName); + ValidateMember(context, attributeSyntax, namedConditionType, typeName, memberName); } } diff --git a/test/UnitTests/MSTest.Analyzers.UnitTests/MemberConditionShouldBeValidAnalyzerTests.cs b/test/UnitTests/MSTest.Analyzers.UnitTests/MemberConditionShouldBeValidAnalyzerTests.cs index 3bc6a666dd..1532038c3f 100644 --- a/test/UnitTests/MSTest.Analyzers.UnitTests/MemberConditionShouldBeValidAnalyzerTests.cs +++ b/test/UnitTests/MSTest.Analyzers.UnitTests/MemberConditionShouldBeValidAnalyzerTests.cs @@ -438,7 +438,7 @@ await VerifyCS.VerifyAnalyzerAsync( } [TestMethod] - public async Task WhenStaticFieldIsInstance_MemberNotStatic() + public async Task WhenFieldIsInstance_MemberNotStatic() { string code = """ using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -551,4 +551,29 @@ public void TestMethod() { } await VerifyCS.VerifyAnalyzerAsync(code); } + + [TestMethod] + public async Task WhenConditionTypeIsArrayType_MemberNotFound() + { + // typeof(int[]) is an IArrayTypeSymbol, not INamedTypeSymbol. The runtime would still throw + // InvalidOperationException because int[] has no user-declared static bool members; the + // analyzer must surface MSTEST0070 rather than silently skipping the attribute. + string code = """ + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MyTestClass + { + [{|#0:MemberCondition(typeof(int[]), "AnyName")|}] + [TestMethod] + public void TestMethod() { } + } + """; + + await VerifyCS.VerifyAnalyzerAsync( + code, + VerifyCS.Diagnostic(MemberConditionShouldBeValidAnalyzer.MemberNotFoundRule) + .WithLocation(0) + .WithArguments("int[]", "AnyName")); + } }