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
Allow parameter type name to be specified for WriteCodeFragment task #6285
Allow parameter type name to be specified for WriteCodeFragment task #6285
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code is very well-written, and you have an excellent description—thank you for that. I had no understanding of the WriteCodeFragment task going in, but I'm reasonably confident that, assuming valid inputs, this doesn't break existing behavior, and I think it adds support for additional types as you describe.
AttributeParameter parameter = parameters[i]; | ||
CodeExpression value; | ||
|
||
switch (parameter.Type.Kind) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
This could be cleaner with the pattern:
CodeExpression value = parameter.Type.Kind switch
{
ParameterTypeKind.Literal => new CodeSnippetExpression(parameter.Value),
ParameterTypeKind.Typed => parameter.Type.TypeName.Equals("System.Type") ?
...
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure this is possible because the case for ParameterTypeKind.Typed
can return false instead of setting value
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the return false case, you had previously called TryConvertParameterValue(parameter.Type.TypeName, parameter.Value, out value)
, which would set value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The default case causes problems here because it delays finding the positional parameters until they are needed, and that can't (as far as I'm aware) be put in a switch expression. This could be worked around using a Lazy<Type[]>
for the positional parameter types, but that would result in an allocation that could be avoided in some situations.
I'm happy to go ahead and make that change, but just thought I would check if it's worth the extra allocation. 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking something like:
int t = 0;
char[] b = null;
int k = t switch
{
2 => 3,
_ => otherFunc((b = b is null ? new char[] { 'A', 'B' } : b)[0])
};
That's a little messy, but I think it would be cleaner. Probably have to write out everything to see for sure, and if you are happy with it this way, I am, too.
… inferring parameter types.
Thank you @reduckted! |
How can I know when this will be available? Ideally I'd subscribe to something somewhere on github to be alerted when I can start using this. |
This should be available in 16.10 preview 3. I will try to remember to ping you in this thread when that's available. That should be roughly a month from now. |
Are you referring to the MSBuild or Visual Studio version? Will it be part of the next .NET 6.0 SDK preview release? |
MSBuild versions and Visual Studio versions line up nicely, so both.
That's a good question. I think it will be in preview 4, but I'm not 100% sure on that. @dsplaisted, do you know? |
@marcpopMSFT for the release alignment question. |
16.10 preview 3 has been released! The .NET 6 preview 4 SDK hasn't been released yet. /cc: @billybraga |
When the .NET 6 preview 4 SDK releases, it should include this fix as we'll be shipping with an MSBuild from the first week of May it looks like. |
Hey @reduckted , thanks for a really nice contribution! Quick question: is this supposed to work for F# projects as well? |
@Crono1981 Good question! I just had a look into it, and no, it doesn't work for F#. It looks like F# has its own |
@reduckted I see. I'll document my plugin accordingly. Thanks for confirming. |
It would be possible to set the `CLSCompliantAttribute` to true in the csproj directly with the .NET 6 SDK and get rid of AssemblyInfo.cs but that's 5 lines of XML, see dotnet/msbuild#6285 So let's keep it simple with a one line assembly attribute instead.
Is this feature documented anywhere at learn.microsoft.com? It was rather non-trivial to find this implementation, but I couldn't find any public docs for it. |
@RussKie good catch: MicrosoftDocs/visualstudio-docs#9379. |
Fixes #2281
Context
This change allows the
WriteCodeFragment
task to define assembly attributes that require parameters that are not of typeSystem.String
. For example,CSLCompliantAttribute
can be generated with a parameter oftrue
instead of"true"
.Changes Made
Additional metadata can be defined on an
AssemblyAttribute
that specifies how to treat the parameters specified in the metadata. There are three different ways that the parameters can be treated.Infer the Type
Without specifying any additional metadata, attributes that are defined in the
mscorlib
assembly (i.e. types that can be loaded viaSystem.Type.GetType(string)
) will have their parameter types inferred by finding the constructor where the parameter count matches the number of parameters specified in the metadata. For example, this:Will produce the code:
For backward-compatibility, if the attribute cannot be found, or no matching constructor is found, the parameter is treated as a string.
Declare the Type
An additional metadata item can be used to specify the full name of the parameter type. To do this, add a metadata item that has the same name as the parameter with "_TypeName" appended to the end. For example, this:
Will produce the code:
This also works with named parameters:
All types that can be used as attribute parameters are supported, except for arrays.
For backward-compatibility, if a metadata item ends with "_TypeName", but there is no metadata item for the parameter with that name, then it will be treated as another named property. For example, this:
Would produce the code:
Specify the Exact Code
For cases where declaring the type is insufficient (such as when the parameter is an array), you can specify the exact that that will be generated for the parameter by adding metadata that has the same name as the parameter with "_IsLiteral" appended to the end. For example, this:
Will produce the code:
The limitation with this is that the code you provide is language-specific. For example, the literal value in the metadata above will only work in C#. If you used that same metadata in a VB.NET project, you would receive a compiler error.
This works with both positional and named parameters. As with the
..._TypeName
metadata, if an..._IsLiteral
metadata does not have a corresponding parameter name, it will be treated as a named parameter for backward-compatibility.Mixed Parameter Behavior
Because the additional metadata only applies to a specific parameter, you can choose to treat different parameters in different ways. For example, you can infer/use the default behavior for one parameter, specify the type for the second parameter and use a literal value for the third. For example:
Testing
I've added tests for inferring the parameter type, declaring the parameter type and using literal values. I've also added tests to confirm backward-compatibility is maintained where it's possible to do so.
The new tests I added all use helper methods to reduce the boilerplate that the existing tests had. The existing tests could also be changed to use these new methods, but I've left them as is to reduce the amount of changes in this PR.
Notes
I'm not sure if "IsLiteral" is the best terminology, but the general consensus in #2281 was that it was a good term to use, so I stuck with it. I'm happy to change it to something else if needed.