Skip to content

Conversation

@GerardSmit
Copy link
Contributor

Fixes #92

This PR adds SyncMethods.txt support.

In the project, the following has to be added to the .csproj:

<AdditionalFiles Include="SyncMethods.txt" />

After that, the mappings will be used. The syntax is as following:

OldNamespace.OldClass.OldMethod=NewNamespace.NewClass.NewMethod

Priority

User mappings have always priority. For example, if you do add the following mapping:

System.Threading.Tasks.Task.Delay=Test.CustomThread.Sleep

Normally System.Threading.Thread.Sleep would be used if you do Task.Delay(...), but since there is a user mapping Test.CustomThread.Sleep will be used in the current project.

Checks

The source generator validates the following:

  • Duplicate keys -> ZSMGEN004
  • Both [CreateSyncVersion] and an user mapping on the method -> ZSMGEN005

@@ -0,0 +1,38 @@
namespace Zomp.SyncMethodGenerator;

internal sealed class UserMappings(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe I should rename this class to UserMappingResult or something else.
Plural class names are kinda cursed 🙂

var progress = new global::System.Progress<float>();
WithIProgress(progress);
}
//HintName: Test.Class.CallWithIProgressAsync.g.cs
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah.. I had the issue again with line endings. I've disabled autoclrf in Git and replaced all line endings, but it seems were also wrong.

Should I revert these changes? Or is it OK that they are in this PR?

var result = driver.GetRunResult().Results.Single();
var sourceOutputs =
result.TrackedOutputSteps.SelectMany(outputStep => outputStep.Value).SelectMany(output => output.Outputs);
var (value, reason) = Assert.Single(sourceOutputs);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was failing because there are now 2 context.RegisterSourceOutput's (for the mapping diagnostics and method results). That's why I check for the output length 2 now.

title: "Attribute and user mapping conflict",
messageFormat: "Method '{0}' has both an attribute and a user mapping defined. The user mapping will be used.",
category: Preprocessor,
DiagnosticSeverity.Error,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's possible to have [CreateSyncMethod] and an user mapping on a method:

  • The sync method will still be created. You can directly call it.
  • However, whenever the async-to-sync rewriting happens, the user mapping will be used.

To prevent confusion why the method isn't being called, I've made this an error.

@lsoft
Copy link

lsoft commented Aug 5, 2025

may be it would be benefitical to use custom xml nodes inside Directory.Build.props files instead of brand new SyncMethods.txt.

the benefits:

  1. Natural support for a nested rules (in nested folders). Directory.Build.props can be already exists in a project.
  2. Lesser cognitive burden, because Directory.Build.props is a known concept.

just an idea =)

});
}

private static UserMappings GetUserMappings(ImmutableArray<(string Path, string Content)> array, CancellationToken token)
Copy link

Choose a reason for hiding this comment

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

nit

may be it would be better to move this this inside an UserMappings class and make public and change it name to Create?

@GerardSmit
Copy link
Contributor Author

It's possible to add the following in the Directory.Build.props:

<AdditionalFiles Include="$(MSBuildThisFileDirectory)SyncMethods.txt" />

And then create a SyncMethods.txt in directory next to the Directory.Build.props. All the underlying projects will then use this file.

To add extra methods to a single project, you can create a SyncMethods.txt in the project folder and add the following:

<AdditionalFiles Include="SyncMethods.txt" />

Then those two files will be combined (Directory.Build.props + .csproj).

Custom XML node

If we want to make custom node, we're limited to a single value (or we have to go through the files ourself, but IO access in a source generator is not recommended by Microsoft).

In the .csproj/.props the following have to be added (otherwise we don't have access to this node):

<ItemGroup>
	<CompilerVisibleProperty Include="SyncMethods" />
</ItemGroup>

Then we can read out the SyncMethods in the source generator. For example:

<PropertyGroup>
	<SyncMethods>
		System.Threading.Tasks.Task.Delay=Test.CustomThread.Sleep
	</SyncMethods>
</PropertyGroup>

I'm not sure if we're allowed to have inner XML nodes in the property. I haven't test this. If this is possible, then we can use XML nodes.

Let's say the example above was defined in the .props-file, and we want to extend the methods in the project (.csproj), then you'll have to add $(SyncMethods) to the node, otherwise the whole list will be replaced:

<PropertyGroup>
	<SyncMethods>
		$(SyncMethods)
		System.Threading.Tasks.Task.Delay=Test.CustomThread.Sleep
	</SyncMethods>
</PropertyGroup>

I'm fine with both. I chose the .txt-file because this was discussed in #92 and CsWin32 (a project from Microsoft) uses NativeMethods.txt.

@lsoft
Copy link

lsoft commented Aug 5, 2025

I'm have no strong opinion for this, I just only highlighed idea. I guess you and @virzak should make decision about the implementation. =)

Let me add 3 additional point:

  1. Do we want to support folder level customization inside one project? If so, I guess Directory.Packages.props would be natural choice...
  2. Do we want to differentiate overloads? like MyAsyncMethod(long) -> MySyncMethod(long) but MyAsyncMethod(int) -> MySyncMethod(long)
  3. In any case I pesonally would prefer some structured file like SyncMethods.xml, SyncMethods.json or even Directory.SyncMethods.props.

As I said above, the decision is yours and @virzak. Anyway, this is a cool idea 👍

@GerardSmit
Copy link
Contributor Author

GerardSmit commented Aug 5, 2025

Do we want to differentiate overloads? like MyAsyncMethod(long) -> MySyncMethod(long) but MyAsyncMethod(int) -> MySyncMethod(long)

This is a good point.

Another thing is, for example, EF Core has .FirstOrDefaultAsync(cancellationToken) if you would add a mapping to .FirstOrDefault it will rewrite to .FirstOrDefault(cancellationToken); which is invalid. It should drop the cancellationToken.

I'll add a test case for this.

The decision is yours

Not really, I'm not the project owner on this one; I'm just a contributor 😉

@GerardSmit GerardSmit marked this pull request as draft August 5, 2025 10:53
return obj is not null && (ReferenceEquals(this, obj) || (obj is UserMappings other && Equals(other)));
}

public override int GetHashCode() => HashCode.Combine(Mappings);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Suggested change
public override int GetHashCode() => HashCode.Combine(Mappings);
public override int GetHashCode() => HashCode.Combine(mappings, Diagnostics);

Typo; this is using the dictionary instead of the EquatableArray, and is missing the diagnostics.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: User defined mapping

2 participants