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

[API Proposal]: Creating new TypeName without reparsing #102263

Open
adamsitnik opened this issue May 15, 2024 · 19 comments
Open

[API Proposal]: Creating new TypeName without reparsing #102263

adamsitnik opened this issue May 15, 2024 · 19 comments
Assignees
Labels
api-approved API was approved in API review, it can be implemented area-System.Reflection.Metadata
Milestone

Comments

@adamsitnik
Copy link
Member

adamsitnik commented May 15, 2024

Background and motivation

As we have started using the new TypeName APIs all over the place, I've realized that it would be very nice to have the ability to create a new TypeName instance with given AssemblyNameInfo without the need of re-parsing the concatenated input again.

For example, in BinaryFormatter the type and library names are provided separately (#102014 (comment)), I would like to join them without re-parsing the whole thing again.

API Proposal

namespace System.Reflection.Metadata;

public sealed class TypeName
{
    /// <summary>
    /// Returns a <see cref="TypeName" /> object representing a one-dimensional array
    /// of the current type, with a lower bound of zero.
    /// </summary>
    /// <returns>
    public TypeName MakeArrayTypeName();
    
    /// <summary>
    /// Returns a <see cref="TypeName" /> object representing an array of the current type,
    /// with the specified number of dimensions.
    /// </summary>
    /// <param name="rank">The number of dimensions for the array. This number must be more than zero and less than or equal to 32.</param>
    /// <exception cref="ArgumentOutOfRangeException">rank is invalid. For example, 0 or negative.</exception>
    public TypeName MakeArrayTypeName(int rank);
    
    /// <summary>
    /// Returns a <see cref="TypeName" /> object that represents a pointer to the current type.
    /// </summary>
    public TypeName MakePointerTypeName();
    
    /// <summary>
    /// Returns a <see cref="TypeName" /> object that represents a managed reference to the current type.
    /// </summary>
    public TypeName MakeByRefTypeName();
    
    /// <summary>
    /// Creates a new constructed generic type name.
    /// </summary>
    /// <param name="typeArguments">An array of type names to be used as generic arguments of the current simple type name.</param>
    /// <exception cref="InvalidOperationException">The current type name is not simple.</exception>
    public TypeName MakeGenericTypeName(ImmutableArray<TypeName> typeArguments)

    /// <summary>
    /// Returns a <see cref="TypeName" /> object that represents the current type name with provided assembly name.
    /// </summary>
    public TypeName WithAssemblyName(AssemblyNameInfo? assemblyName);

    public static TypeName CreateSimpleTypeName(string fullName, TypeName? declaringType = null, AssemblyNameInfo? assemblyInfo = null);
}

API Usage

TypeName WithoutAssemblyVersion(TypeName typeName) 
    => typeName.WithAssemblyName(
        new AssemblyNameInfo(
            typeName.AssemblyName.Name,
            version: null,
            typeName.AssemblyName.CultureName,
            typeName.AssemblyName.Flags,
            typeName.AssemblyName.PublicKeyOrToken));

Risks

Some users may miss the fact that the API creates a new instance of TypeName rather than mutating the current instance.

Sample implementation with tests and usages in dotnet/runtime available at #103713

@adamsitnik adamsitnik added area-System.Reflection.Metadata api-ready-for-review API is ready for review, it is NOT ready for implementation labels May 15, 2024
@adamsitnik adamsitnik added this to the 9.0.0 milestone May 15, 2024
@adamsitnik adamsitnik self-assigned this May 15, 2024
Copy link
Contributor

Tagging subscribers to this area: @dotnet/area-system-reflection-metadata
See info in area-owners.md if you want to be subscribed.

@jkotas
Copy link
Member

jkotas commented May 15, 2024

Should we rather address the more general problem of creating TypeName of any shape through constructors or factory methods?

It happens that the AssemblyName case showed up with the binary formatter. I think it is a tell-take of a general need for creating arbitrary TypeNames.

@adamsitnik
Copy link
Member Author

Should we rather address the more general problem of creating TypeName of any shape through constructors or factory methods?

All TypeName ctors that are going to accept strings of any kind are going to need to be fully validated, which is almost like reparsing them. And this is something that I would like to avoid with the provided API.

I am not saying no, I just don't have a clear vision of how these APIs would look like.

@jkotas
Copy link
Member

jkotas commented May 16, 2024

All TypeName ctors that are going to accept strings of any kind are going to need to be fully validate

Why do the strings need to be validated? Type names can be anything.

@jkotas
Copy link
Member

jkotas commented May 16, 2024

Ah, it is because the APIs returned escaped names. The escaping validation should be very cheap, much cheaper than parsing.

The more I see the issues with the name escaping, the more I think that it was a bad design decision to make the Name and FullName properties to match reflection behavior. Should we revisit that?

The APIs can look like this:

public static TypeName MakeSimpleTypeName(string fullName, TypeName? declaringType = null, AssemblyNameInfo? assemblyInfo = null);
public static TypeName MakeArrayTypeName(TypeName elementType);
public static TypeName MakeArrayTypeName(TypeName elementType, int rank);
public static TypeName MakePointerTypeName(TypeName elementType);
public static TypeName MakeByRefTypeName(TypeName elementType);
public static TypeName MakeGenericTypeName(TypeName genericTypeDefinition, ImmutableArray<TypeName> typeArguments);

// Matches Unscape method proposed in #101774
public static string Escape(string name);

@adamsitnik
Copy link
Member Author

the more I think that it was a bad design decision to make the Name and FullName properties to match reflection behavior

But it allows for things like if (typeName.FullName == typeof(T).FullName) which already proved very useful in payload reader.

I know it may sound silly but IMO we would not have that problem if we would simply forbid using certain characters without the ability to escape them in the first place.

@jkotas
Copy link
Member

jkotas commented May 16, 2024

if (typeName.FullName == typeof(T).FullName)

Yeah, that's the conundrum when you want to use this together with reflection. It is hard to tell how common this use case is going to be.

Anyway, I edited my suggestion at #102263 (comment) to include Escape method, so that there is a reasonable path when you have the raw name.

we would not have that problem if we would simply forbid using certain characters without the ability to escape them in the first place.

It would produce non-compliant type name parser. It may be ok for BF-specific parser. I do not think it is ok for S.R.Metadata or the runtime that need compliant and backward compatible type name parsers. It would be a needless breaking change to drop support for escaped type names.

@adamsitnik adamsitnik changed the title [API Proposal]: Creating new TypeName with given AssemblyNameInfo [API Proposal]: Creating new TypeName without reparsing Jun 28, 2024
@steveharter
Copy link
Member

Moving to v10 since we have reached the "feature complete" milestone; still in API review backlog.

@steveharter steveharter modified the milestones: 9.0.0, 10.0.0 Jul 24, 2024
@bartonjs
Copy link
Member

bartonjs commented Aug 1, 2024

Video

  • WithAssemblyName doesn't seem to have a solid scenario (outside of one case), so it's being withheld until it has a more clear use pattern with regard to composed types (e.g. generics).
  • It was asked if the instance methods should be prefixed with "Make" or "Create", and it seems that "Make" matches reflection, and the static one "Create" is a patterned prefix.
  • We changed CreateSimpleTypeName's fullName parameter to metadataName to avoid confusion between the unescaped value as a parameter and the escaped value as the property with the same name.
  • The parameterless MakeArrayTypeName is renamed to MakeSZArrayTypeName because MakeArrayTypeName(1) and MakeArrayTypeName() are not the same.
namespace System.Reflection.Metadata;

public partial class TypeName
{      
    public TypeName MakeSZArrayTypeName();
    public TypeName MakeArrayTypeName(int rank);
    public TypeName MakePointerTypeName();
    public TypeName MakeByRefTypeName();
    public TypeName MakeGenericTypeName(ImmutableArray<TypeName> typeArguments);

    public static TypeName CreateSimpleTypeName(
        string metadataName,
        TypeName? declaringType = null,
        AssemblyNameInfo? assemblyInfo = null);
}

@bartonjs bartonjs added api-approved API was approved in API review, it can be implemented and removed api-ready-for-review API is ready for review, it is NOT ready for implementation labels Aug 1, 2024
@dotnet-policy-service dotnet-policy-service bot added the in-pr There is an active PR which will close this issue when it is merged label Aug 2, 2024
@adamsitnik
Copy link
Member Author

I've adopted my initial implementation (#103713) to the approved API in 9a0cd58.

So far, all TypeName instances that any library or app could somehow get were valid and escaped type names. Users could simply rely on that and the information the API provides no matter where it comes from.

The introduction of CreateSimpleTypeName that accepts unescaped full names introduces a possibility to create invalid TypeName instances.

Example: TypeName.Parse("TypeName, MyLib") returns a TypeName where FullName=TypeName and AssemblyName=MyLib.
When we pass the same input to CreateSimpleTypeName we don't know whether , was previously escaped (it's not a begining of assembly name) or not (it's a begining of an assembly name).

Same goes for escaping. If we have a name that consists of escaped escape character followed by an escaped special character like * and unescape it, the escape character is followed by a special character. It seems to still be escaped, but it's not.

Example: \\\* -> Unescape -> \* -> Unescape -> *.

In my opinion, since static string Unescape(string name) got approved #101774 (comment), we don't need CreateSimpleTypeName.

But we still need the ability to replace AssemblyName for simple types. That is why I propose following change:

public class TypeName
{

-   public static TypeName CreateSimpleTypeName(string metadataName, TypeName? declaringType = null, AssemblyNameInfo? assemblyInfo = null);
+   public TypeName MakeSimpleTypeName(TypeName? declaringType = null, AssemblyNameInfo? assemblyInfo = null);
}

That method would throw for non-simple names and prevent the users from creating invalid instances. I've implemented it in aabe51b

cc @jkotas @GrabYourPitchforks

@jkotas
Copy link
Member

jkotas commented Aug 2, 2024

Example: TypeName.Parse("TypeName, MyLib") returns a TypeName where FullName=TypeName and AssemblyName=MyLib.
When we pass the same input to CreateSimpleTypeName we don't know whether , was previously escaped (it's not a begining of assembly name) or not (it's a begining of an assembly name).

CreateSimpleTypeName was meant to take unescaped name. It is why we have opted to name the argument metadataName to make it clear that it is not the regular (escaped) name.

If you pass TypeName, MyLib to CreateSimpleTypeName, it should create a TypeName that returns TypeName\, MyLib from FullName property. It is a valid typename. There is no ambiguity.

In my opinion, since static string Unescape(string name) got approved #101774 (comment), we don't need CreateSimpleTypeName.

CreateSimpleTypeName was equivalent of Escape. Unescape is not a replacement.

@adamsitnik
Copy link
Member Author

@jkotas big thanks for the clarification, now it all makes sense.

Since CreateSimpleTypeName needs to parse the name to unescape it, I can not use it (my requirement is to create a new type name with given assembly name without reparsing).

I've finished implementing #103713 and I need just one more method:

namespace System.Reflection.Metadata;

public partial class TypeName
{      
    public TypeName MakeSZArrayTypeName();
    public TypeName MakeArrayTypeName(int rank);
    public TypeName MakePointerTypeName();
    public TypeName MakeByRefTypeName();
    public TypeName MakeGenericTypeName(ImmutableArray<TypeName> typeArguments);
+   public TypeName MakeSimpleTypeName(AssemblyNameInfo? assemblyInfo);

    public static TypeName CreateSimpleTypeName(
        string metadataName,
        TypeName? declaringType = null,
        AssemblyNameInfo? assemblyInfo = null);
}

I am going to create a new issue with the proposal for it.

@jkotas
Copy link
Member

jkotas commented Aug 6, 2024

Since CreateSimpleTypeName needs to parse the name to unescape it, I can not use it (my requirement is to create a new type name with given assembly name without reparsing).

I do not see why escaping the type name passed into CreateSimpleTypeName is a problem for your BF use case. It does not create non-linear amount of work.

@adamsitnik
Copy link
Member Author

I do not see why escaping the type name passed into CreateSimpleTypeName is a problem for your BF use case.
It does not create non-linear amount of work.

I think it depends on how you treat the comparison of current char vs chars that needs to be escaped. For me, assuming that we have n needles (characters that need escaping) and i characters in the input name, it's O(n * i).

@jkotas
Copy link
Member

jkotas commented Aug 6, 2024

This is not a general-purpose escaping with arbitrary escape characters. The number of characters that need escaping is very small (7 to be exact) and set in stone. So it is O(7 * i).

@teo-tsirpanis teo-tsirpanis removed the in-pr There is an active PR which will close this issue when it is merged label Jan 21, 2025
@teo-tsirpanis
Copy link
Contributor

@adamsitnik is there anything left to do here? Can we close this issue?

@adamsitnik
Copy link
Member Author

@adamsitnik is there anything left to do here? Can we close this issue?

The CreateSimpleTypeName method has not been implemented yet.

@jkotas
Copy link
Member

jkotas commented Feb 9, 2025

The CreateSimpleTypeName method has not been implemented yet.

That's correct. On the other hand, I am not aware of any place that can use the API of this shape currently, so we may want to let this issue linger until this need materializes.

There are a few places that do raw type name escaping (e.g.

public static string EscapeTypeNameIdentifier(this string identifier)
), but this API is not of the right shape to replace them. A static method that does raw escaping may be more useful.

@jkotas jkotas modified the milestones: 10.0.0, Future Feb 9, 2025
@adamsitnik
Copy link
Member Author

That's correct. On the other hand, I am not aware of any place that can use the API of this shape currently, so we may want to let this issue linger until this need materializes.

I agree, let's wait 👍 .

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-approved API was approved in API review, it can be implemented area-System.Reflection.Metadata
Projects
None yet
Development

No branches or pull requests

5 participants