Skip to content

Commit 6539101

Browse files
committed
analyzer & co. must use the custom SwitchMapStateParameterName if configured
1 parent 21fcc3a commit 6539101

File tree

7 files changed

+791
-15
lines changed

7 files changed

+791
-15
lines changed

.serena/project.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,7 @@ symbol_info_budget:
125125
# Note: the backend is fixed at startup. If a project with a different backend
126126
# is activated post-init, an error will be returned.
127127
language_backend:
128+
129+
# list of regex patterns which, when matched, mark a memory entry as read‑only.
130+
# Extends the list from the global configuration, merging the two lists.
131+
read_only_memory_patterns: []

src/Thinktecture.Runtime.Extensions.Analyzers/CodeAnalysis/CodeFixes/ThinktectureRuntimeExtensionsCodeFixProvider.cs

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -827,6 +827,26 @@ private static async Task<Document> UseStateOverloadAsync(
827827
if (semanticModel is null)
828828
return document;
829829

830+
// Resolve the actual state parameter name from the state overload sibling method
831+
var stateParameterName = Constants.Parameters.STATE;
832+
833+
if (semanticModel.GetSymbolInfo(invocation, cancellationToken).Symbol is IMethodSymbol currentMethod)
834+
{
835+
var containingType = currentMethod.ContainingType;
836+
var methodName = currentMethod.Name;
837+
838+
foreach (var member in containingType.GetMembers(methodName))
839+
{
840+
if (member is IMethodSymbol sibling
841+
&& sibling.OriginalDefinition.Parameters.Length > 0
842+
&& sibling.OriginalDefinition.Parameters[0].Type.TypeKind == TypeKind.TypeParameter)
843+
{
844+
stateParameterName = sibling.Parameters[0].Name;
845+
break;
846+
}
847+
}
848+
}
849+
830850
// Build state expression
831851
ExpressionSyntax stateExpression;
832852

@@ -848,7 +868,7 @@ private static async Task<Document> UseStateOverloadAsync(
848868

849869
// State argument (first)
850870
var stateArg = SyntaxFactory.Argument(
851-
SyntaxFactory.NameColon(Constants.Parameters.STATE),
871+
SyntaxFactory.NameColon(stateParameterName),
852872
default,
853873
stateExpression);
854874
newArguments.Add(stateArg);
@@ -861,7 +881,7 @@ private static async Task<Document> UseStateOverloadAsync(
861881
if (argument.Expression is LambdaExpressionSyntax lambda
862882
&& !lambda.Modifiers.Any(SyntaxKind.StaticKeyword))
863883
{
864-
var newLambda = TransformLambdaForStateOverload(semanticModel, lambda, captures, captureSet);
884+
var newLambda = TransformLambdaForStateOverload(semanticModel, lambda, captures, captureSet, stateParameterName);
865885
newArguments.Add(argument.WithExpression(newLambda));
866886
}
867887
else
@@ -883,7 +903,8 @@ private static LambdaExpressionSyntax TransformLambdaForStateOverload(
883903
SemanticModel semanticModel,
884904
LambdaExpressionSyntax lambda,
885905
List<ISymbol> captures,
886-
HashSet<ISymbol> captureSet)
906+
HashSet<ISymbol> captureSet,
907+
string stateParameterName)
887908
{
888909
// Step 1: Replace capture references in the body
889910
var body = (SyntaxNode?)lambda.ExpressionBody ?? lambda.Block;
@@ -904,13 +925,13 @@ private static LambdaExpressionSyntax TransformLambdaForStateOverload(
904925

905926
if (captures.Count == 1)
906927
{
907-
replacement = SyntaxFactory.IdentifierName(Constants.Parameters.STATE).WithTriviaFrom(identifier);
928+
replacement = SyntaxFactory.IdentifierName(stateParameterName).WithTriviaFrom(identifier);
908929
}
909930
else
910931
{
911932
replacement = SyntaxFactory.MemberAccessExpression(
912933
SyntaxKind.SimpleMemberAccessExpression,
913-
SyntaxFactory.IdentifierName(Constants.Parameters.STATE),
934+
SyntaxFactory.IdentifierName(stateParameterName),
914935
SyntaxFactory.IdentifierName(identifier.Identifier.Text)).WithTriviaFrom(identifier);
915936
}
916937

@@ -932,7 +953,7 @@ private static LambdaExpressionSyntax TransformLambdaForStateOverload(
932953
}
933954

934955
// Step 3: Add state parameter
935-
var stateParam = SyntaxFactory.Parameter(SyntaxFactory.Identifier(Constants.Parameters.STATE));
956+
var stateParam = SyntaxFactory.Parameter(SyntaxFactory.Identifier(stateParameterName));
936957

937958
if (transformedLambda is SimpleLambdaExpressionSyntax simple)
938959
{

src/Thinktecture.Runtime.Extensions.Analyzers/CodeAnalysis/Diagnostics/ThinktectureRuntimeExtensionsAnalyzer.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -466,8 +466,11 @@ private static void AnalyzeAnyUnionSwitchMap(
466466
{
467467
var numberOfCallbacks = operation.TargetMethod.Parameters.IsDefaultOrEmpty ? 0 : operation.TargetMethod.Parameters.Length;
468468

469+
var originalMethod = operation.TargetMethod.OriginalDefinition;
470+
469471
if (numberOfCallbacks > 0
470-
&& operation.TargetMethod.Parameters[0].Name == Constants.Parameters.STATE)
472+
&& originalMethod.Parameters[0].Type is { TypeKind: TypeKind.TypeParameter } firstParamType
473+
&& !SymbolEqualityComparer.Default.Equals(firstParamType, originalMethod.ReturnType))
471474
{
472475
numberOfCallbacks--;
473476
}

src/Thinktecture.Runtime.Extensions.Refactorings/CodeAnalysis/Refactorings/SwitchMapCompletionRefactoringProvider.cs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -298,15 +298,17 @@ private static ExpressionSyntax BuildArgumentExpression(IParameterSymbol paramet
298298
return SyntaxFactory.IdentifierName(CreateIdentifier(parameter.Name));
299299
}
300300

301+
var stateParameterName = IsStateOverload(method) ? method.Parameters[0].Name : Constants.Parameters.STATE;
302+
301303
if (paramType is INamedTypeSymbol namedType)
302304
{
303305
// Check for System.Action
304306
if (namedType.IsSystemAction())
305-
return BuildActionLambda(namedType);
307+
return BuildActionLambda(namedType, stateParameterName);
306308

307309
// Check for System.Func
308310
if (namedType.IsSystemFunc())
309-
return BuildFuncLambda(namedType);
311+
return BuildFuncLambda(namedType, stateParameterName);
310312

311313
// Check for Thinktecture.Argument<T>
312314
if (namedType.IsThinktectureArgument())
@@ -319,7 +321,7 @@ private static ExpressionSyntax BuildArgumentExpression(IParameterSymbol paramet
319321
return SyntaxFactory.LiteralExpression(SyntaxKind.DefaultLiteralExpression);
320322
}
321323

322-
private static ExpressionSyntax BuildActionLambda(INamedTypeSymbol actionType)
324+
private static ExpressionSyntax BuildActionLambda(INamedTypeSymbol actionType, string stateParameterName)
323325
{
324326
var staticModifier = SyntaxFactory.TokenList(
325327
SyntaxFactory.Token(SyntaxKind.StaticKeyword).WithTrailingTrivia(SyntaxFactory.Space));
@@ -343,12 +345,12 @@ private static ExpressionSyntax BuildActionLambda(INamedTypeSymbol actionType)
343345

344346
// Action<T1, T2, ...> → static (x1, x2, ...) => { }
345347
return SyntaxFactory.ParenthesizedLambdaExpression(
346-
SyntaxFactory.ParameterList(BuildMultipleParameters(actionType.TypeArguments.Length)),
348+
SyntaxFactory.ParameterList(BuildMultipleParameters(actionType.TypeArguments.Length, stateParameterName)),
347349
SyntaxFactory.Block())
348350
.WithModifiers(staticModifier);
349351
}
350352

351-
private static ExpressionSyntax BuildFuncLambda(INamedTypeSymbol funcType)
353+
private static ExpressionSyntax BuildFuncLambda(INamedTypeSymbol funcType, string stateParameterName)
352354
{
353355
var staticModifier = SyntaxFactory.TokenList(
354356
SyntaxFactory.Token(SyntaxKind.StaticKeyword).WithTrailingTrivia(SyntaxFactory.Space));
@@ -380,12 +382,12 @@ private static ExpressionSyntax BuildFuncLambda(INamedTypeSymbol funcType)
380382
// Func<T1, T2, ..., TResult> → static (x1, x2, ...) => throw new System.NotImplementedException()
381383
// TypeArguments.Length - 1 because the last type argument is TResult
382384
return SyntaxFactory.ParenthesizedLambdaExpression(
383-
SyntaxFactory.ParameterList(BuildMultipleParameters(funcType.TypeArguments.Length - 1)),
385+
SyntaxFactory.ParameterList(BuildMultipleParameters(funcType.TypeArguments.Length - 1, stateParameterName)),
384386
throwExpression)
385387
.WithModifiers(staticModifier);
386388
}
387389

388-
private static SeparatedSyntaxList<ParameterSyntax> BuildMultipleParameters(int count)
390+
private static SeparatedSyntaxList<ParameterSyntax> BuildMultipleParameters(int count, string stateParameterName)
389391
{
390392
var nodesAndTokens = new SyntaxNodeOrToken[count * 2 - 1];
391393

@@ -397,7 +399,7 @@ private static SeparatedSyntaxList<ParameterSyntax> BuildMultipleParameters(int
397399
.WithTrailingTrivia(SyntaxFactory.Space);
398400
}
399401

400-
var name = i == 0 ? Constants.Parameters.STATE : "x";
402+
var name = i == 0 ? stateParameterName : "x";
401403
nodesAndTokens[i * 2] = SyntaxFactory.Parameter(SyntaxFactory.Identifier(name));
402404
}
403405

test/Thinktecture.Runtime.Extensions.Analyzers.Tests/AnalyzerAndCodeFixTests/TTRESG046_IndexBasedSwitchAndMapMustUseNamedParameters.cs

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2840,4 +2840,221 @@ public void Do()
28402840
await Verifier.VerifyAnalyzerAsync(code, [typeof(ComplexValueObjectAttribute).Assembly]);
28412841
}
28422842
}
2843+
2844+
public class CustomSwitchMapStateParameterName
2845+
{
2846+
public class AdHocUnionSwitchWithActionAndState
2847+
{
2848+
[Fact]
2849+
public async Task Should_trigger_without_named_arg()
2850+
{
2851+
var code = """
2852+
2853+
using System;
2854+
using Thinktecture;
2855+
using Thinktecture.Runtime.Tests.TestAdHocUnions;
2856+
2857+
namespace TestNamespace
2858+
{
2859+
public class Test
2860+
{
2861+
public void Do()
2862+
{
2863+
var testUnion = new TestUnionWithCustomSwitchMapStateParameterName("text");
2864+
2865+
{|#0:testUnion.Switch(42,
2866+
@string: static (ctx, s) => {},
2867+
static (ctx, i) => {})|};
2868+
}
2869+
}
2870+
}
2871+
""";
2872+
2873+
var expected = Verifier.Diagnostic(_DIAGNOSTIC_ID).WithLocation(0).WithArguments(nameof(TestAdHocUnions.TestUnionWithCustomSwitchMapStateParameterName));
2874+
await Verifier.VerifyAnalyzerAsync(code, [typeof(UnionAttribute<,>).Assembly, typeof(TestAdHocUnions.TestUnionWithCustomSwitchMapStateParameterName).Assembly], expected);
2875+
}
2876+
2877+
[Fact]
2878+
public async Task Should_not_trigger_when_all_args_are_named()
2879+
{
2880+
var code = """
2881+
2882+
using System;
2883+
using Thinktecture;
2884+
using Thinktecture.Runtime.Tests.TestAdHocUnions;
2885+
2886+
namespace TestNamespace
2887+
{
2888+
public class Test
2889+
{
2890+
public void Do()
2891+
{
2892+
var testUnion = new TestUnionWithCustomSwitchMapStateParameterName("text");
2893+
2894+
{|#0:testUnion.Switch(42,
2895+
@string: static (ctx, s) => {},
2896+
int32: static (ctx, i) => {})|};
2897+
}
2898+
}
2899+
}
2900+
""";
2901+
2902+
await Verifier.VerifyAnalyzerAsync(code, [typeof(UnionAttribute<,>).Assembly, typeof(TestAdHocUnions.TestUnionWithCustomSwitchMapStateParameterName).Assembly]);
2903+
}
2904+
}
2905+
2906+
public class RegularUnionSwitchWithActionAndState
2907+
{
2908+
[Fact]
2909+
public async Task Should_trigger_without_named_arg()
2910+
{
2911+
var code = """
2912+
2913+
using System;
2914+
using Thinktecture;
2915+
using Thinktecture.Runtime.Tests.TestRegularUnions;
2916+
2917+
namespace TestNamespace
2918+
{
2919+
public class Test
2920+
{
2921+
public void Do()
2922+
{
2923+
var testUnion = new TestUnionWithCustomSwitchMapStateParameterName.Child1("text");
2924+
2925+
{|#0:testUnion.Switch(42,
2926+
child1: static (ctx, c) => {},
2927+
static (ctx, c) => {})|};
2928+
}
2929+
}
2930+
}
2931+
""";
2932+
2933+
var expected = Verifier.Diagnostic(_DIAGNOSTIC_ID).WithLocation(0).WithArguments(nameof(TestRegularUnions.TestUnionWithCustomSwitchMapStateParameterName));
2934+
await Verifier.VerifyAnalyzerAsync(code, [typeof(UnionAttribute).Assembly, typeof(TestRegularUnions.TestUnionWithCustomSwitchMapStateParameterName).Assembly], expected);
2935+
}
2936+
2937+
[Fact]
2938+
public async Task Should_not_trigger_when_all_args_are_named()
2939+
{
2940+
var code = """
2941+
2942+
using System;
2943+
using Thinktecture;
2944+
using Thinktecture.Runtime.Tests.TestRegularUnions;
2945+
2946+
namespace TestNamespace
2947+
{
2948+
public class Test
2949+
{
2950+
public void Do()
2951+
{
2952+
var testUnion = new TestUnionWithCustomSwitchMapStateParameterName.Child1("text");
2953+
2954+
{|#0:testUnion.Switch(42,
2955+
child1: static (ctx, c) => {},
2956+
child2: static (ctx, c) => {})|};
2957+
}
2958+
}
2959+
}
2960+
""";
2961+
2962+
await Verifier.VerifyAnalyzerAsync(code, [typeof(UnionAttribute).Assembly, typeof(TestRegularUnions.TestUnionWithCustomSwitchMapStateParameterName).Assembly]);
2963+
}
2964+
}
2965+
2966+
public class AdHocUnionSwitchPartiallyWithActionAndState
2967+
{
2968+
[Fact]
2969+
public async Task Should_trigger_without_named_arg()
2970+
{
2971+
var code = """
2972+
2973+
using System;
2974+
using Thinktecture;
2975+
using Thinktecture.Runtime.Tests.TestAdHocUnions;
2976+
2977+
namespace TestNamespace
2978+
{
2979+
public class Test
2980+
{
2981+
public void Do()
2982+
{
2983+
var testUnion = new TestUnionWithCustomSwitchMapStateParameterName("text");
2984+
2985+
{|#0:testUnion.SwitchPartially(42,
2986+
@default: static (ctx, value) => {},
2987+
@string: static (ctx, s) => {},
2988+
static (ctx, i) => {})|};
2989+
}
2990+
}
2991+
}
2992+
""";
2993+
2994+
var expected = Verifier.Diagnostic(_DIAGNOSTIC_ID).WithLocation(0).WithArguments(nameof(TestAdHocUnions.TestUnionWithCustomSwitchMapStateParameterName));
2995+
await Verifier.VerifyAnalyzerAsync(code, [typeof(UnionAttribute<,>).Assembly, typeof(TestAdHocUnions.TestUnionWithCustomSwitchMapStateParameterName).Assembly], expected);
2996+
}
2997+
2998+
[Fact]
2999+
public async Task Should_not_trigger_when_all_args_are_named()
3000+
{
3001+
var code = """
3002+
3003+
using System;
3004+
using Thinktecture;
3005+
using Thinktecture.Runtime.Tests.TestAdHocUnions;
3006+
3007+
namespace TestNamespace
3008+
{
3009+
public class Test
3010+
{
3011+
public void Do()
3012+
{
3013+
var testUnion = new TestUnionWithCustomSwitchMapStateParameterName("text");
3014+
3015+
{|#0:testUnion.SwitchPartially(42,
3016+
@default: static (ctx, value) => {},
3017+
@string: static (ctx, s) => {},
3018+
int32: static (ctx, i) => {})|};
3019+
}
3020+
}
3021+
}
3022+
""";
3023+
3024+
await Verifier.VerifyAnalyzerAsync(code, [typeof(UnionAttribute<,>).Assembly, typeof(TestAdHocUnions.TestUnionWithCustomSwitchMapStateParameterName).Assembly]);
3025+
}
3026+
}
3027+
3028+
public class AdHocUnionMapPartiallyWithFunc
3029+
{
3030+
[Fact]
3031+
public async Task Should_not_trigger_when_all_args_are_named()
3032+
{
3033+
var code = """
3034+
3035+
using System;
3036+
using Thinktecture;
3037+
using Thinktecture.Runtime.Tests.TestAdHocUnions;
3038+
3039+
namespace TestNamespace
3040+
{
3041+
public class Test
3042+
{
3043+
public void Do()
3044+
{
3045+
var testUnion = new TestUnionWithCustomSwitchMapStateParameterName("text");
3046+
3047+
var returnValue = {|#0:testUnion.MapPartially(
3048+
@default: 0,
3049+
@string: 1,
3050+
int32: 2)|};
3051+
}
3052+
}
3053+
}
3054+
""";
3055+
3056+
await Verifier.VerifyAnalyzerAsync(code, [typeof(UnionAttribute<,>).Assembly, typeof(TestAdHocUnions.TestUnionWithCustomSwitchMapStateParameterName).Assembly]);
3057+
}
3058+
}
3059+
}
28433060
}

0 commit comments

Comments
 (0)