Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Relax parameter-argument modifier matching for ref readonly parameters #68224

Merged
Merged
Show file tree
Hide file tree
Changes from 46 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
732ccbb
Relax parameter-argument modifier matching
jjonescz Jun 21, 2023
ba4196c
Remove sequence point verification
jjonescz Jun 23, 2023
95c47c8
Adjust argument emit
jjonescz Jun 23, 2023
6de3783
Test more value kinds and argument modifiers
jjonescz Jun 23, 2023
005decd
Simplify ref kind mismatch logic
jjonescz Jun 23, 2023
023d6a6
Skip arglist arguments explicitly
jjonescz Jun 23, 2023
f14a901
Merge argument checking and coercion
jjonescz Jun 23, 2023
157997f
Check feature availability before emitting mismatch warnings
jjonescz Jun 23, 2023
10f9b05
Verify success interpolated string cases
jjonescz Jun 23, 2023
6214282
Fix wrong error when overload resolution fails
jjonescz Jun 23, 2023
ae921b5
Revert assert in argument ref kind emit
jjonescz Jun 23, 2023
30abc94
Remove a prototype comment about method conversions
jjonescz Jun 23, 2023
3a49016
Use warning level 1
jjonescz Jun 23, 2023
7f9689b
Revert argument expression passing to `GetEffectiveParameterRefKind`
jjonescz Jun 23, 2023
a5af25b
Adjust wording of `WRN_BadArgRef`
jjonescz Jun 23, 2023
d67b7fb
Update patching comments in local rewriter
jjonescz Jun 23, 2023
e6e46e1
Lower `ref readonly` arguments to `in` ref kinds
jjonescz Jun 26, 2023
42300a1
Make a note to improve value kind error wording
jjonescz Jun 27, 2023
6dd4ba6
Reduce ref kind assert before emit
jjonescz Jun 27, 2023
a0b0226
Assert `ref readonly` params have ref kind before emit
jjonescz Jun 27, 2023
389d66e
Assert synthesized argument ref kinds
jjonescz Jun 27, 2023
7bf0117
Fix `ref`/`in` pair error reporting logic
jjonescz Jun 27, 2023
cee5f6f
Store `ref readonly` temps same as `in`
jjonescz Jun 27, 2023
8326054
Disallow `ref readonly` with none/`in` for old LangVersion
jjonescz Jun 27, 2023
e779e56
Fix `ref readonly` parameter value kind
jjonescz Jun 27, 2023
31d5f80
Improve wording for rvalues passed to `ref readonly`
jjonescz Jun 27, 2023
ddae804
Add more value check tests
jjonescz Jun 27, 2023
52b0a96
Improve assert code in synthesized constructor
jjonescz Jun 27, 2023
c05c5ce
Fix ref kinds default or empty assert
jjonescz Jun 27, 2023
81ebe2c
Suggest upgrading to `C#12` for `ref`/`in` error
jjonescz Jun 27, 2023
aed010e
Add separate error for readonly refs
jjonescz Jun 28, 2023
44bcef2
Transform method ref kinds in synthesized calls
jjonescz Jun 28, 2023
1983e18
Skip arglist also in older LangVersion
jjonescz Jun 28, 2023
ed0c02d
Use non-strict ref kinds in synthesized calls
jjonescz Jun 28, 2023
c1c8da7
Ignore missing methods in an assert
jjonescz Jun 28, 2023
5888231
Remove unnecessary `using`
jjonescz Jun 28, 2023
23a3c2a
Suggest upgrading on `ref`/`in` mismatch
jjonescz Jun 28, 2023
e0b8403
Move local `argRefKind` closer to its use-site
jjonescz Jun 28, 2023
a0be827
Pass strict in ref kind to `StoreToTemp`
jjonescz Jun 28, 2023
4e4f716
Improve nested ternary operator readability
jjonescz Jun 28, 2023
3708145
Consider strict `in` during codegen
jjonescz Jun 29, 2023
101906a
Make change consistent with `main`
jjonescz Jun 29, 2023
595b733
Suppress `ref`/`in` warning for interpolated string handlers
jjonescz Jun 30, 2023
50ed0bc
Verify cross assembly scenarios in latest LangVersion
jjonescz Jun 30, 2023
ed895b4
Move and extend interpolated string constructor diagnostic filtering
jjonescz Jun 30, 2023
ff4a182
Handle one more interpolated string diagnostic collection
jjonescz Jun 30, 2023
f9f0f5c
Merge branch 'features/RefReadonly' into RefReadonly-03-OverloadResol…
jjonescz Jul 7, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ public CSharpUpgradeProjectCodeFixProvider()
"CS8885", // warning CS8885: The 'this' object cannot be used before all of its fields have been assigned. Consider updating to language version 'preview' to auto-default the unassigned fields.
"CS8936", // error CS8936: Feature '{0}' is not available in C# 10.0. Please use language version {1} or greater.
"CS9058", // error CS9058: Feature '{0}' is not available in C# 11.0. Please use language version {1} or greater.
// PROTOTYPE: Fixup error numbers.
"CS9505", // error CS9505: Argument {0} may not be passed with the 'ref' keyword in language version {1}. To pass 'ref' arguments to 'in' parameters, upgrade to language version {2} or greater.
});

public override string UpgradeThisProjectResource => CSharpCodeFixesResources.Upgrade_this_project_to_csharp_language_version_0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1267,5 +1267,22 @@ struct Test
expected: LanguageVersion.CSharp11,
new CSharpParseOptions(LanguageVersion.CSharp10));
}

[Fact]
public async Task UpgradeProjectForRefInMismatch()
{
await TestLanguageVersionUpgradedAsync("""
class C
{
void M1(in int x) { }
void M2(ref int y)
{
M1(ref [|y|]);
}
}
""",
expected: LanguageVersion.Preview,
new CSharpParseOptions(LanguageVersion.CSharp11));
}
}
}
4 changes: 2 additions & 2 deletions src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -919,8 +919,8 @@ private bool CheckParameterValueKind(SyntaxNode node, BoundParameter parameter,
ParameterSymbol parameterSymbol = parameter.ParameterSymbol;

// all parameters can be passed by ref/out or assigned to
// except "in" parameters, which are readonly
if (parameterSymbol.RefKind == RefKind.In && RequiresAssignableVariable(valueKind))
// except "in" and "ref readonly" parameters, which are readonly
if (parameterSymbol.RefKind is RefKind.In or RefKind.RefReadOnlyParameter && RequiresAssignableVariable(valueKind))
{
ReportReadOnlyError(parameterSymbol, node, valueKind, checkingReceiver, diagnostics);
return false;
Expand Down
69 changes: 65 additions & 4 deletions src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3213,14 +3213,15 @@ private BoundExpression BindArgumentExpression(BindingDiagnosticBag diagnostics,
}

#nullable enable
private void CoerceArguments<TMember>(
private void CheckAndCoerceArguments<TMember>(
MemberResolutionResult<TMember> methodResult,
ArrayBuilder<BoundExpression> arguments,
AnalyzedArguments analyzedArguments,
BindingDiagnosticBag diagnostics,
BoundExpression? receiver)
where TMember : Symbol
{
var result = methodResult.Result;
var arguments = analyzedArguments.Arguments;

// Parameter types should be taken from the least overridden member:
var parameters = methodResult.LeastOverriddenMember.GetParameters();
Copy link
Contributor

@AlekseyTs AlekseyTs Jun 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

var parameters = methodResult.LeastOverriddenMember.GetParameters();

Should we check IDS_FeatureRefReadonlyParameters availability at the call site for any scenarios? #Closed

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, thanks, I even have a test for it, but didn't notice it had wrong results.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, thanks, I even have a test for it, but didn't notice it had wrong results.

Are you referring to RefReadonlyParameter_CrossAssembly unit-test? I actually thought that we would want to report the fact that the feature is not enabled at least when the corresponding parameter RefKind is ref readonly, the call site modifier is in or omitted, and language version is below 12. Alternatively, we can complain on any attempt to consume ref readonly parameter when language version is below 12.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you referring to RefReadonlyParameter_CrossAssembly unit-test?

Yes. Responded under the comment on that test.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We had an offline discussion about this, and, I think, agreed that a follow-up is necessary.

Expand All @@ -3230,6 +3231,66 @@ private void CoerceArguments<TMember>(
var kind = result.ConversionForArg(arg);
BoundExpression argument = arguments[arg];

if (argument is not BoundArgListOperator && !argument.HasAnyErrors)
{
var argRefKind = analyzedArguments.RefKind(arg);

if (!Compilation.IsFeatureEnabled(MessageID.IDS_FeatureRefReadonlyParameters))
{
// Disallow using `ref readonly` parameters with no or `in` argument modifier,
// same as older versions of the compiler would (since they would see the parameter as `ref`).
if (argRefKind is RefKind.None or RefKind.In &&
GetCorrespondingParameter(ref result, parameters, arg).RefKind == RefKind.RefReadOnlyParameter)
{
var available = CheckFeatureAvailability(argument.Syntax, MessageID.IDS_FeatureRefReadonlyParameters, diagnostics);
Debug.Assert(!available);
}
}
else
{
// Warn for `ref`/`in` or None/`ref readonly` mismatch.
if (argRefKind == RefKind.Ref)
{
if (GetCorrespondingParameter(ref result, parameters, arg).RefKind == RefKind.In)
{
// The 'ref' modifier for argument {0} corresponding to 'in' parameter is equivalent to 'in'. Consider using 'in' instead.
diagnostics.Add(
ErrorCode.WRN_BadArgRef,
argument.Syntax,
arg + 1);
}
}
else if (argRefKind == RefKind.None &&
GetCorrespondingParameter(ref result, parameters, arg).RefKind == RefKind.RefReadOnlyParameter)
{
if (!this.CheckValueKind(argument.Syntax, argument, BindValueKind.RefersToLocation, checkingReceiver: false, BindingDiagnosticBag.Discarded))
{
// Argument {0} should be a variable because it is passed to a 'ref readonly' parameter
diagnostics.Add(
ErrorCode.WRN_RefReadonlyNotVariable,
argument.Syntax,
arg + 1);
}
else if (this.CheckValueKind(argument.Syntax, argument, BindValueKind.Assignable, checkingReceiver: false, BindingDiagnosticBag.Discarded))
{
// Argument {0} should be passed with 'ref' or 'in' keyword
diagnostics.Add(
ErrorCode.WRN_ArgExpectedRefOrIn,
argument.Syntax,
arg + 1);
}
else
{
// Argument {0} should be passed with the 'in' keyword
diagnostics.Add(
ErrorCode.WRN_ArgExpectedIn,
argument.Syntax,
arg + 1);
}
}
}
}

if (kind.IsInterpolatedStringHandler)
{
Debug.Assert(argument is BoundUnconvertedInterpolatedString or BoundBinaryOperator { IsUnconvertedInterpolatedStringAddition: true });
Expand Down Expand Up @@ -6137,7 +6198,7 @@ internal bool TryPerformConstructorOverloadResolution(

if (succeededIgnoringAccessibility)
{
this.CoerceArguments<MethodSymbol>(result.ValidResult, analyzedArguments.Arguments, diagnostics, receiver: null);
this.CheckAndCoerceArguments<MethodSymbol>(result.ValidResult, analyzedArguments, diagnostics, receiver: null);
}

// Fill in the out parameter with the result, if there was one; it might be inaccessible.
Expand Down Expand Up @@ -8557,7 +8618,7 @@ private BoundExpression BindIndexerOrIndexedPropertyAccess(

receiver = ReplaceTypeOrValueReceiver(receiver, property.IsStatic, diagnostics);

this.CoerceArguments<PropertySymbol>(resolutionResult, analyzedArguments.Arguments, diagnostics, receiver);
this.CheckAndCoerceArguments<PropertySymbol>(resolutionResult, analyzedArguments, diagnostics, receiver);

if (!gotError && receiver != null && receiver.Kind == BoundKind.ThisReference && receiver.WasCompilerGenerated)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -601,7 +601,7 @@ private BoundBinaryOperator BindUnconvertedBinaryOperatorToInterpolatedStringHan
{
// We successfully bound the out version, so set all the final data based on that binding
constructorCall = nonOutConstructorCall;
diagnostics.AddRangeAndFree(nonOutConstructorDiagnostics);
addAndFreeConstructorDiagnostics(target: diagnostics, source: nonOutConstructorDiagnostics);
outConstructorDiagnostics.Free();
}
else
Expand All @@ -622,27 +622,27 @@ private BoundBinaryOperator BindUnconvertedBinaryOperatorToInterpolatedStringHan
case (true, false):
constructorCall = outConstructorCall;
additionalConstructorArguments = outConstructorAdditionalArguments;
diagnostics.AddRangeAndFree(outConstructorDiagnostics);
addAndFreeConstructorDiagnostics(target: diagnostics, source: outConstructorDiagnostics);
nonOutConstructorDiagnostics.Free();
break;
case (false, true):
constructorCall = nonOutConstructorCall;
diagnostics.AddRangeAndFree(nonOutConstructorDiagnostics);
addAndFreeConstructorDiagnostics(target: diagnostics, source: nonOutConstructorDiagnostics);
outConstructorDiagnostics.Free();
break;
default:
// For the final output binding info, we'll go with the shorter constructor in the absence of any tiebreaker,
// but we'll report all diagnostics
constructorCall = nonOutConstructorCall;
diagnostics.AddRangeAndFree(nonOutConstructorDiagnostics);
diagnostics.AddRangeAndFree(outConstructorDiagnostics);
addAndFreeConstructorDiagnostics(target: diagnostics, source: nonOutConstructorDiagnostics);
addAndFreeConstructorDiagnostics(target: diagnostics, source: outConstructorDiagnostics);
break;
}
}
}
else
{
diagnostics.AddRangeAndFree(outConstructorDiagnostics);
addAndFreeConstructorDiagnostics(target: diagnostics, source: outConstructorDiagnostics);
constructorCall = outConstructorCall;
additionalConstructorArguments = outConstructorAdditionalArguments;
}
Expand Down Expand Up @@ -677,6 +677,28 @@ static void populateArguments(SyntaxNode syntax, ImmutableArray<BoundInterpolate
// Any other arguments from the call site
argumentsBuilder.AddRange(additionalConstructorArguments);
}

static void addAndFreeConstructorDiagnostics(BindingDiagnosticBag target, BindingDiagnosticBag source)
{
target.AddDependencies(source);

if (source.DiagnosticBag is { IsEmptyWithoutResolution: false } bag)
{
foreach (var diagnostic in bag.AsEnumerableWithoutResolution())
{
// Filter diagnostics that cannot be fixed since they are on the hidden interpolated string constructor.
if (!((ErrorCode)diagnostic.Code is ErrorCode.WRN_BadArgRef
or ErrorCode.WRN_RefReadonlyNotVariable
or ErrorCode.WRN_ArgExpectedRefOrIn
or ErrorCode.WRN_ArgExpectedIn))
{
target.Add(diagnostic);
}
}
}

source.Free();
}
}

private ImmutableArray<BoundExpression> BindInterpolatedStringParts(BoundUnconvertedInterpolatedString unconvertedInterpolatedString, BindingDiagnosticBag diagnostics)
Expand Down
4 changes: 2 additions & 2 deletions src/Compilers/CSharp/Portable/Binder/Binder_Invocation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1072,7 +1072,7 @@ private BoundCall BindInvocationExpressionContinued(

var receiver = ReplaceTypeOrValueReceiver(methodGroup.Receiver, !method.RequiresInstanceReceiver && !invokedAsExtensionMethod, diagnostics);

this.CoerceArguments(methodResult, analyzedArguments.Arguments, diagnostics, receiver);
this.CheckAndCoerceArguments(methodResult, analyzedArguments, diagnostics, receiver);

var expanded = methodResult.Result.Kind == MemberResolutionKind.ApplicableInExpandedForm;
var argsToParams = methodResult.Result.ArgsToParamsOpt;
Expand Down Expand Up @@ -2102,7 +2102,7 @@ private BoundFunctionPointerInvocation BindFunctionPointerInvocation(SyntaxNode
methodsBuilder.Free();

MemberResolutionResult<FunctionPointerMethodSymbol> methodResult = overloadResolutionResult.ValidResult;
CoerceArguments(methodResult, analyzedArguments.Arguments, diagnostics, receiver: null);
CheckAndCoerceArguments(methodResult, analyzedArguments, diagnostics, receiver: null);

var args = analyzedArguments.Arguments.ToImmutable();
var refKinds = analyzedArguments.RefKinds.ToImmutableOrNull();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2192,7 +2192,7 @@ private static BetterResult PreferValOverInOrRefInterpolatedHandlerParameters<TM
static bool isAcceptableRefMismatch(RefKind refKind, bool isInterpolatedStringHandlerConversion)
=> refKind switch
{
RefKind.In => true,
RefKind.In or RefKind.RefReadOnlyParameter => true,
Copy link
Contributor

@AlekseyTs AlekseyTs Jun 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or RefKind.RefReadOnlyParameter

Are we testing effect of this change? #Closed

Copy link
Member Author

@jjonescz jjonescz Jun 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, RefReadonlyParameter_PlainArgument_OverloadResolution fails with "ambiguous call" error without this change. Note that the method doesn't handle only interpolated string handler parameters as its name could suggest; it's called as part of RemoveWorseMembers during overload resolution.

Copy link
Member

@jcouv jcouv Jul 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not related to this PR: I was thrown off by the name of this method (PreferValOverInOrRefInterpolatedHandlerParameters) but as your test demonstrates no interpolation handlers are needed to get here...
Ah, never mind, I was parsing the name wrong. It's "prefer val over ... ref interpolated handler parameter" :-P

Copy link
Member

@jcouv jcouv Jul 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spec: Should the spec mention that we'll have a preference of by-value over ref readonly overload? #Pending

RefKind.Ref when isInterpolatedStringHandlerConversion => true,
_ => false
};
Expand Down Expand Up @@ -3190,9 +3190,27 @@ private static RefKind GetEffectiveParameterRefKind(

// 'None' argument is allowed to match 'In' parameter and should behave like 'None' for the purpose of overload resolution
// unless this is a method group conversion where 'In' must match 'In'
if (!isMethodGroupConversion && argRefKind == RefKind.None && paramRefKind == RefKind.In)
// There are even more relaxations with 'ref readonly' parameters feature:
// - 'ref' argument is allowed to match 'in' parameter,
// - 'ref', 'in', none argument is allowed to match 'ref readonly' parameter.
if (!isMethodGroupConversion)
{
return RefKind.None;
if (paramRefKind == RefKind.In)
{
if (argRefKind == RefKind.None)
{
return RefKind.None;
}

if (argRefKind == RefKind.Ref && binder.Compilation.IsFeatureEnabled(MessageID.IDS_FeatureRefReadonlyParameters))
{
return RefKind.Ref;
}
}
else if (paramRefKind == RefKind.RefReadOnlyParameter && argRefKind is RefKind.None or RefKind.Ref or RefKind.In)
{
return argRefKind;
}
}

// Omit ref feature for COM interop: We can pass arguments by value for ref parameters if we are calling a method/property on an instance of a COM imported type.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1213,11 +1213,25 @@ private static void ReportBadArgumentError(
new FormattedSymbol(UnwrapIfParamsArray(parameter, isLastParameter), SymbolDisplayFormat.CSharpErrorMessageNoParameterNamesFormat));
}
}
else if (refArg != refParameter && !(refArg == RefKind.None && refParameter == RefKind.In))
else if (refArg != refParameter &&
!(refArg == RefKind.None && refParameter == RefKind.In) &&
Copy link
Contributor

@AlekseyTs AlekseyTs Jun 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(refArg == RefKind.None && refParameter == RefKind.In)

It looks like scenario when (refArg == RefKind.Ref && refParameter == RefKind.In) is a valid scenario now, depending on the language version. Are we testing this code path with scenario like that for different versions? #Closed

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Will extend test PassingArgumentsToInParameters_RefKind_None_WrongType and fix the logic.

!(refArg == RefKind.Ref && refParameter == RefKind.In && binder.Compilation.IsFeatureEnabled(MessageID.IDS_FeatureRefReadonlyParameters)) &&
!(refParameter == RefKind.RefReadOnlyParameter && refArg is RefKind.None or RefKind.Ref or RefKind.In))
Copy link
Contributor

@AlekseyTs AlekseyTs Jun 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(refParameter == RefKind.RefReadOnlyParameter && refArg is RefKind.None or RefKind.Ref or RefKind.In)

Do we have tests demonstrating impact of this condition? #Closed

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, RefReadonlyParameter_WrongType - wrong error would be reported without this condition ("wrong ref" instead of "wrong type").

{
if (refParameter == RefKind.None || refParameter == RefKind.In)
if (refArg == RefKind.Ref && refParameter == RefKind.In && !binder.Compilation.IsFeatureEnabled(MessageID.IDS_FeatureRefReadonlyParameters))
{
// Argument {0} should not be passed with the {1} keyword
// Argument {0} may not be passed with the 'ref' keyword in language version {1}. To pass 'ref' arguments to 'in' parameters, upgrade to language version {2} or greater.
diagnostics.Add(
ErrorCode.ERR_BadArgExtraRefLangVersion,
sourceLocation,
symbols,
arg + 1,
binder.Compilation.LanguageVersion.ToDisplayString(),
new CSharpRequiredLanguageVersion(MessageID.IDS_FeatureRefReadonlyParameters.RequiredVersion()));
}
else if (refParameter is RefKind.None or RefKind.In or RefKind.RefReadOnlyParameter)
{
// Argument {0} may not be passed with the '{1}' keyword
diagnostics.Add(
ErrorCode.ERR_BadArgExtraRef,
sourceLocation,
Expand Down
Loading