Skip to content

Commit

Permalink
Feature: CallerArgumentExpressionAttribute (#51952)
Browse files Browse the repository at this point in the history
* Initial work for caller argument expression

* Add resources for caller argument expression diagnostics

* Account for swapped named arguments

* Add few tests

* Complete implementation in PEParameterSymbol

* Update tests

* Produce warning for invalid parameter name

* Run generate-compiler-code.cmd

* Fix out of range exception and add more tests

* Extract DecodeCallerArgumentExpressionAttribute extension

* Fix freeing twice

* Fix comments

* Simplify PEParameterSymbol

* Fix NullableWarnings test

* Add test for two parameters referring to each other

* Use warning level 1

* Add test for extension methods and remove addressed comments

* Add title resources for new warnings

* Use static lambda

* Add PROTOTYPE comment

* Add trivia test and skip PEVerification

* Address feedback

* Fix resources

* Simplify based on @AlekseyTs feedback

* Pass array builder instead of creating a temporary immutable array

* Check feature availability on language version

* Add "semantic check" comment

* Add missing IDS_Feature resource

* Don't check feature availability on attribute application

* Remove helper that's used only once

* Cleanup

* Don't check generatedDiagnostics

* Use LanguageVersion.Preview in tests

* Cleanup resx

* Use TestOptions.RegularPreview

* ConditionalFact[typeof(CoreClrOnly)]

* Add assertion

* Update tests

* Update test

* Adjust NullableWarnings test

* Address feedback

* Simplify

* Fix formatting

* Address feedback
  • Loading branch information
Youssef1313 authored Apr 10, 2021
1 parent 20777d6 commit 52f6db1
Show file tree
Hide file tree
Showing 33 changed files with 1,513 additions and 20 deletions.
19 changes: 17 additions & 2 deletions src/Compilers/CSharp/Portable/Binder/Binder_Invocation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1308,6 +1308,7 @@ internal void BindDefaultArguments(
// Params array is filled in the local rewriter
var lastIndex = expanded ? ^1 : ^0;

var argumentsCount = argumentsBuilder.Count;
// Go over missing parameters, inserting default values for optional parameters
foreach (var parameter in parameters.AsSpan()[..lastIndex])
{
Expand All @@ -1316,7 +1317,7 @@ internal void BindDefaultArguments(
Debug.Assert(parameter.IsOptional || !assertMissingParametersAreOptional);

defaultArguments[argumentsBuilder.Count] = true;
argumentsBuilder.Add(bindDefaultArgument(node, parameter, containingMember, enableCallerInfo, diagnostics));
argumentsBuilder.Add(bindDefaultArgument(node, parameter, containingMember, enableCallerInfo, diagnostics, argumentsBuilder, argumentsCount, argsToParamsOpt));

if (argumentRefKindsBuilder is { Count: > 0 })
{
Expand All @@ -1335,7 +1336,7 @@ internal void BindDefaultArguments(
argsToParamsBuilder.Free();
}

BoundExpression bindDefaultArgument(SyntaxNode syntax, ParameterSymbol parameter, Symbol containingMember, bool enableCallerInfo, BindingDiagnosticBag diagnostics)
BoundExpression bindDefaultArgument(SyntaxNode syntax, ParameterSymbol parameter, Symbol containingMember, bool enableCallerInfo, BindingDiagnosticBag diagnostics, ArrayBuilder<BoundExpression> argumentsBuilder, int argumentsCount, ImmutableArray<int> argsToParamsOpt)
{
TypeSymbol parameterType = parameter.Type;
if (Flags.Includes(BinderFlags.ParameterDefaultValue))
Expand Down Expand Up @@ -1370,6 +1371,15 @@ BoundExpression bindDefaultArgument(SyntaxNode syntax, ParameterSymbol parameter
var memberName = containingMember.GetMemberCallerName();
defaultValue = new BoundLiteral(syntax, ConstantValue.Create(memberName), Compilation.GetSpecialType(SpecialType.System_String)) { WasCompilerGenerated = true };
}
else if (callerSourceLocation is object && getArgumentIndex(parameter.CallerArgumentExpressionParameterIndex, argsToParamsOpt) is int argumentIndex &&
argumentIndex > -1 && argumentIndex < argumentsCount)
{
CheckFeatureAvailability(syntax, MessageID.IDS_FeatureCallerArgumentExpression, diagnostics);
// PROTOTYPE(caller-expr): Do we need to support VB?

var argument = argumentsBuilder[argumentIndex];
defaultValue = new BoundLiteral(syntax, ConstantValue.Create(argument.Syntax.ToString()), Compilation.GetSpecialType(SpecialType.System_String)) { WasCompilerGenerated = true };
}
else if (defaultConstantValue == ConstantValue.NotAvailable)
{
// There is no constant value given for the parameter in source/metadata.
Expand Down Expand Up @@ -1423,6 +1433,11 @@ BoundExpression bindDefaultArgument(SyntaxNode syntax, ParameterSymbol parameter
}

return defaultValue;

static int getArgumentIndex(int parameterIndex, ImmutableArray<int> argsToParamsOpt)
=> argsToParamsOpt.IsDefault
? parameterIndex
: argsToParamsOpt.IndexOf(parameterIndex);
}

}
Expand Down
41 changes: 40 additions & 1 deletion src/Compilers/CSharp/Portable/CSharpResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -6600,4 +6600,43 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ
<data name="ERR_FunctionPointerTypesInAttributeNotSupported" xml:space="preserve">
<value>Using a function pointer type in a 'typeof' in an attribute is not supported.</value>
</data>
</root>
<data name="ERR_BadCallerArgumentExpressionParamWithoutDefaultValue" xml:space="preserve">
<value>The CallerArgumentExpressionAttribute may only be applied to parameters with default values</value>
</data>
<data name="ERR_NoConversionForCallerArgumentExpressionParam" xml:space="preserve">
<value>CallerArgumentExpressionAttribute cannot be applied because there are no standard conversions from type '{0}' to type '{1}'</value>
</data>
<data name="WRN_CallerArgumentExpressionParamForUnconsumedLocation" xml:space="preserve">
<value>The CallerArgumentExpressionAttribute applied to parameter '{0}' will have no effect because it applies to a member that is used in contexts that do not allow optional arguments</value>
</data>
<data name="WRN_CallerArgumentExpressionParamForUnconsumedLocation_Title" xml:space="preserve">
<value>The CallerArgumentExpressionAttribute will have no effect because it applies to a member that is used in contexts that do not allow optional arguments</value>
</data>
<data name="WRN_CallerFilePathPreferredOverCallerArgumentExpression" xml:space="preserve">
<value>The CallerArgumentExpressionAttribute applied to parameter '{0}' will have no effect. It is overridden by the CallerFilePathAttribute.</value>
</data>
<data name="WRN_CallerFilePathPreferredOverCallerArgumentExpression_Title" xml:space="preserve">
<value>The CallerArgumentExpressionAttribute will have no effect; it is overridden by the CallerFilePathAttribute</value>
</data>
<data name="WRN_CallerLineNumberPreferredOverCallerArgumentExpression" xml:space="preserve">
<value>The CallerArgumentExpressionAttribute applied to parameter '{0}' will have no effect. It is overridden by the CallerLineNumberAttribute.</value>
</data>
<data name="WRN_CallerLineNumberPreferredOverCallerArgumentExpression_Title" xml:space="preserve">
<value>The CallerArgumentExpressionAttribute will have no effect; it is overridden by the CallerLineNumberAttribute</value>
</data>
<data name="WRN_CallerMemberNamePreferredOverCallerArgumentExpression" xml:space="preserve">
<value>The CallerArgumentExpressionAttribute applied to parameter '{0}' will have no effect. It is overriden by the CallerMemberNameAttribute.</value>
</data>
<data name="WRN_CallerMemberNamePreferredOverCallerArgumentExpression_Title" xml:space="preserve">
<value>The CallerArgumentExpressionAttribute will have no effect; it is overridden by the CallerMemberNameAttribute</value>
</data>
<data name="WRN_CallerArgumentExpressionAttributeHasInvalidParameterName" xml:space="preserve">
<value>The CallerArgumentExpressionAttribute applied to parameter '{0}' will have no effect. It is applied with an invalid parameter name.</value>
</data>
<data name="WRN_CallerArgumentExpressionAttributeHasInvalidParameterName_Title" xml:space="preserve">
<value>The CallerArgumentExpressionAttribute is applied with an invalid parameter name.</value>
</data>
<data name="IDS_FeatureCallerArgumentExpression" xml:space="preserve">
<value>caller argument expression</value>
</data>
</root>
11 changes: 11 additions & 0 deletions src/Compilers/CSharp/Portable/Errors/ErrorCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1932,6 +1932,17 @@ internal enum ErrorCode

#endregion diagnostics introduced for C# 9.0

#region diagnostics introduced in C# 10.0
WRN_CallerArgumentExpressionParamForUnconsumedLocation = 8912,
ERR_NoConversionForCallerArgumentExpressionParam = 8913,
ERR_BadCallerArgumentExpressionParamWithoutDefaultValue = 8914,
WRN_CallerLineNumberPreferredOverCallerArgumentExpression = 8915,
WRN_CallerFilePathPreferredOverCallerArgumentExpression = 8916,
WRN_CallerMemberNamePreferredOverCallerArgumentExpression = 8917,
WRN_CallerArgumentExpressionAttributeHasInvalidParameterName = 8918,

#endregion

// Note: you will need to re-generate compiler code after adding warnings (eng\generate-compiler-code.cmd)
}
}
5 changes: 5 additions & 0 deletions src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,11 @@ internal static int GetWarningLevel(ErrorCode code)
case ErrorCode.WRN_AnalyzerReferencesFramework:
case ErrorCode.WRN_UnreadRecordParameter:
case ErrorCode.WRN_DoNotCompareFunctionPointers:
case ErrorCode.WRN_CallerArgumentExpressionParamForUnconsumedLocation:
case ErrorCode.WRN_CallerLineNumberPreferredOverCallerArgumentExpression:
case ErrorCode.WRN_CallerFilePathPreferredOverCallerArgumentExpression:
case ErrorCode.WRN_CallerMemberNamePreferredOverCallerArgumentExpression:
case ErrorCode.WRN_CallerArgumentExpressionAttributeHasInvalidParameterName:
return 1;
default:
return 0;
Expand Down
2 changes: 2 additions & 0 deletions src/Compilers/CSharp/Portable/Errors/MessageID.cs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ internal enum MessageID
IDS_FeatureVarianceSafetyForStaticInterfaceMembers = MessageBase + 12791,
IDS_FeatureConstantInterpolatedStrings = MessageBase + 12792,
IDS_FeatureMixedDeclarationsAndExpressionsInDeconstruction = MessageBase + 12793,
IDS_FeatureCallerArgumentExpression = MessageBase + 12794,
}

// Message IDs may refer to strings that need to be localized.
Expand Down Expand Up @@ -324,6 +325,7 @@ internal static LanguageVersion RequiredVersion(this MessageID feature)
{
// C# preview features.
case MessageID.IDS_FeatureMixedDeclarationsAndExpressionsInDeconstruction:
case MessageID.IDS_FeatureCallerArgumentExpression: // semantic check
return LanguageVersion.Preview;
// C# 9.0 features.
case MessageID.IDS_FeatureLambdaDiscardParameters: // semantic check
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ internal int MethodHashCode()
internal override bool IsCallerFilePath => false;
internal override bool IsCallerLineNumber => false;
internal override bool IsCallerMemberName => false;
internal override int CallerArgumentExpressionParameterIndex => -1;
internal override FlowAnalysisAnnotations FlowAnalysisAnnotations => FlowAnalysisAnnotations.None;
internal override ImmutableHashSet<string> NotNullIfParameterNotNull => ImmutableHashSet<string>.Empty;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ private struct PackedFlags
// r = RefKind. 2 bits.
// n = hasNameInMetadata. 1 bit.
// f = FlowAnalysisAnnotations. 9 bits (8 value bits + 1 completion bit).
// Current total = 28 bits.

private const int WellKnownAttributeDataOffset = 0;
private const int WellKnownAttributeCompletionFlagOffset = 8;
Expand Down Expand Up @@ -141,6 +142,12 @@ public bool TryGetFlowAnalysisAnnotations(out FlowAnalysisAnnotations value)
private ConstantValue _lazyDefaultValue = ConstantValue.Unset;
private ThreeState _lazyIsParams;

/// <summary>
/// The index of a CallerArgumentExpression. The value -2 means uninitialized, -1 means
/// not found. Otherwise, the index of the CallerArgumentExpression.
/// </summary>
private int _lazyCallerArgumentExpressionParameterIndex = -2;

/// <summary>
/// Attributes filtered out from m_lazyCustomAttributes, ParamArray, etc.
/// </summary>
Expand Down Expand Up @@ -656,6 +663,42 @@ internal override bool IsCallerMemberName
}
}

internal override int CallerArgumentExpressionParameterIndex
{
get
{
if (_lazyCallerArgumentExpressionParameterIndex != -2)
{
return _lazyCallerArgumentExpressionParameterIndex;
}

var info = _moduleSymbol.Module.FindTargetAttribute(_handle, AttributeDescription.CallerArgumentExpressionAttribute);
var discardedUseSiteInfo = CompoundUseSiteInfo<AssemblySymbol>.Discarded;
bool isCallerArgumentExpression = info.HasValue
&& !HasCallerLineNumberAttribute
&& !HasCallerFilePathAttribute
&& !HasCallerMemberNameAttribute
&& new TypeConversions(ContainingAssembly).HasCallerInfoStringConversion(this.Type, ref discardedUseSiteInfo);

if (isCallerArgumentExpression)
{
_moduleSymbol.Module.TryExtractStringValueFromAttribute(info.Handle, out var parameterName);
var parameters = ContainingSymbol.GetParameters();
for (int i = 0; i < parameters.Length; i++)
{
if (parameters[i].Name == parameterName)
{
_lazyCallerArgumentExpressionParameterIndex = i;
return i;
}
}
}

_lazyCallerArgumentExpressionParameterIndex = -1;
return -1;
}
}

internal override FlowAnalysisAnnotations FlowAnalysisAnnotations
{
get
Expand Down
2 changes: 2 additions & 0 deletions src/Compilers/CSharp/Portable/Symbols/ParameterSymbol.cs
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,8 @@ internal sealed override ObsoleteAttributeData ObsoleteAttributeData

internal abstract bool IsCallerMemberName { get; }

internal abstract int CallerArgumentExpressionParameterIndex { get; }

internal abstract FlowAnalysisAnnotations FlowAnalysisAnnotations { get; }

internal abstract ImmutableHashSet<string> NotNullIfParameterNotNull { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ public SignatureOnlyParameterSymbol(

internal override bool IsCallerMemberName { get { throw ExceptionUtilities.Unreachable; } }

internal override int CallerArgumentExpressionParameterIndex { get { throw ExceptionUtilities.Unreachable; } }

internal override FlowAnalysisAnnotations FlowAnalysisAnnotations { get { throw ExceptionUtilities.Unreachable; } }

internal override ImmutableHashSet<string> NotNullIfParameterNotNull { get { throw ExceptionUtilities.Unreachable; } }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,11 @@ internal override bool IsCallerMemberName
get { return _originalParam.IsCallerMemberName; }
}

internal override int CallerArgumentExpressionParameterIndex
{
get { return _originalParam.CallerArgumentExpressionParameterIndex; }
}

internal override FlowAnalysisAnnotations FlowAnalysisAnnotations
{
get { return FlowAnalysisAnnotations.None; }
Expand Down
Loading

0 comments on commit 52f6db1

Please sign in to comment.