C
C#3mo ago
Okno

Source generator unable to resolve external type

I am writing a source generator to learn csharp and I stubbled into something strange. Here is the IncrementalValueProvider in my source generator:
var mapToPipeline = context.SyntaxProvider.ForAttributeWithMetadataName(
fullyQualifiedMetadataName: "CartographeAutomatique.MapToAttribute",
predicate: static (syntaxNode, _) => syntaxNode is TypeDeclarationSyntax,
transform: static (context, _) => PopulateTypeMapping(context, MappingKind.MapTo)
);
var mapToPipeline = context.SyntaxProvider.ForAttributeWithMetadataName(
fullyQualifiedMetadataName: "CartographeAutomatique.MapToAttribute",
predicate: static (syntaxNode, _) => syntaxNode is TypeDeclarationSyntax,
transform: static (context, _) => PopulateTypeMapping(context, MappingKind.MapTo)
);
It generate mapping code for a class A to a class B and is used like so:
[MapTo(typeof(Point3))]
public class Vector3
{
public float X { get; set; }
public float Y { get; set; }
public float Z { get; set; }
}
[MapTo(typeof(Point3))]
public class Vector3
{
public float X { get; set; }
public float Y { get; set; }
public float Z { get; set; }
}
Everything work as expected as long as the attribute parameter (here typeof(Point3))) is accessible in the current package. If the typeof attribute parameter lives in an external package the PopulateTyypeMapping method is never called by the IncrementalValueProvider`. I think I understand that the compiler is unable to resolve the class so it skip the attribute, but it's very puzzling that there is no error message or warning emitted at compile time. Am I missing something or is there a way to make this work without changing the attribute parameter to something else ?
18 Replies
Okno
OknoOP3mo ago
GitHub
CartographeAutomatique/CartographeAutomatique/CartographeGenerator....
For now just a C# learning project. Contribute to oknozor/CartographeAutomatique development by creating an account on GitHub.
333fred
333fred3mo ago
Where the typeof parameter comes from shouldn't matter, it just needs to be accessible. Side note: all the data you're using from syntax is available in the context parameter, it gives you the attribute info. Please make sure you've read $ig
MODiX
MODiX3mo ago
If you want to make an incremental source generator, please make sure to read through both the design document and the cookbook before starting.
333fred
333fred3mo ago
GitHub
CartographeAutomatique/CartographeAutomatique/CartographeGenerator....
For now just a C# learning project. Contribute to oknozor/CartographeAutomatique development by creating an account on GitHub.
Okno
OknoOP3mo ago
Accessible to what ?
333fred
333fred3mo ago
The location it's used
Okno
OknoOP3mo ago
well it is, as I said the source generator works as long typeof(TargetClass) lives in the same project. If TargetClass comes from an external dependency the source generator silently fail.
333fred
333fred3mo ago
Have you written unit tests? Because more than likely, it's some assumption you've made
Okno
OknoOP3mo ago
I'd not some assumption I have made
var mapToPipeline = context.SyntaxProvider.ForAttributeWithMetadataName(
fullyQualifiedMetadataName: "CartographeAutomatique.MapToAttribute",
predicate: static (syntaxNode, _) => syntaxNode is TypeDeclarationSyntax,
transform: static (context, _) => PopulateTypeMapping(context, MappingKind.MapTo)
);
var mapToPipeline = context.SyntaxProvider.ForAttributeWithMetadataName(
fullyQualifiedMetadataName: "CartographeAutomatique.MapToAttribute",
predicate: static (syntaxNode, _) => syntaxNode is TypeDeclarationSyntax,
transform: static (context, _) => PopulateTypeMapping(context, MappingKind.MapTo)
);
` this is just not seeing type imported from external dependencies. Yes but I really don't see how I could emulate a type being in an external project there https://github.com/oknozor/CartographeAutomatique/blob/main/CartographeAutomatique.Tests/Fixtures/ClassToClass.cs there is also a project containing samples using the source generator, I can reproduce my issue here, but I am not able to debug there : https://github.com/oknozor/CartographeAutomatique/blob/main/CartographeAutomatique.Samples/SimpleMapping.cs
333fred
333fred3mo ago
I mean, it would be as simple as using typeof(object) For reference, I was able to modify your test project a bit to add a test that demonstrates how to do a metadata reference. First, don't use typeof(object) to populate metadata references like you are. Use $bar. Second, you'd do something like this:
private static void CodeGenerationAssertion(SourceGenerationAssertion assertion)
{
var generator = new CartographeGenerator();
var driver = CSharpGeneratorDriver.Create(generator);

var referenceComp = CSharpCompilation.Create("test", references: Basic.Reference.Assemblies.Net80.References.All, syntaxTrees: [CSharpSyntaxTree.ParseText("""
public partial class Point
{
public float X { get; set; }
public float Y { get; set; }
public float Z { get; set; }
};
""")]);

var compilation = CSharpCompilation.Create(
nameof(CartographeGeneratorTests),
[CSharpSyntaxTree.ParseText(assertion.InputSource)],
ImmutableArray<MetadataReference>.CastUp(Basic.Reference.Assemblies.Net80.References.All).Add(referenceComp.ToMetadataReference())
);

var runResult = driver.RunGenerators(compilation).GetRunResult();

foreach (var sourceGenerationOutput in assertion.Outputs)
{
var actual = runResult
.GeneratedTrees.Single(t =>
t.FilePath.EndsWith($"{sourceGenerationOutput.GeneratedFileName}.g.cs")
)
.GetText()
.ToString();

Assert.Equal(
sourceGenerationOutput.ExpectedSource,
actual,
ignoreLineEndingDifferences: true
);
}
}
private static void CodeGenerationAssertion(SourceGenerationAssertion assertion)
{
var generator = new CartographeGenerator();
var driver = CSharpGeneratorDriver.Create(generator);

var referenceComp = CSharpCompilation.Create("test", references: Basic.Reference.Assemblies.Net80.References.All, syntaxTrees: [CSharpSyntaxTree.ParseText("""
public partial class Point
{
public float X { get; set; }
public float Y { get; set; }
public float Z { get; set; }
};
""")]);

var compilation = CSharpCompilation.Create(
nameof(CartographeGeneratorTests),
[CSharpSyntaxTree.ParseText(assertion.InputSource)],
ImmutableArray<MetadataReference>.CastUp(Basic.Reference.Assemblies.Net80.References.All).Add(referenceComp.ToMetadataReference())
);

var runResult = driver.RunGenerators(compilation).GetRunResult();

foreach (var sourceGenerationOutput in assertion.Outputs)
{
var actual = runResult
.GeneratedTrees.Single(t =>
t.FilePath.EndsWith($"{sourceGenerationOutput.GeneratedFileName}.g.cs")
)
.GetText()
.ToString();

Assert.Equal(
sourceGenerationOutput.ExpectedSource,
actual,
ignoreLineEndingDifferences: true
);
}
}
MODiX
MODiX3mo ago
To create in-memory Roslyn compilations, use https://github.com/jaredpar/basic-reference-assemblies to set up your framework references. You'll get it wrong if you try to do it manually!
333fred
333fred3mo ago
Third, I'm nearly certain that https://github.com/oknozor/CartographeAutomatique/blob/d8011cb9fa745a1f733dce81c6e4f70c7f3be075/CartographeAutomatique/CartographeGenerator.cs#L84-L94 is your issue. If the target type isn't from the current compilation, you'll generate nothing. Also, as I mentioned previously, you should avoid all of this parsing of syntax to get the types involved and just use the attribute data that is on GeneratorAttributeSyntaxContext for you
Okno
OknoOP3mo ago
Thanks for the example, Not sure to understand what you mean by use "$bar instead of typeof(object)" ?
MODiX
MODiX3mo ago
To create in-memory Roslyn compilations, use https://github.com/jaredpar/basic-reference-assemblies to set up your framework references. You'll get it wrong if you try to do it manually!
333fred
333fred3mo ago
Read the link, then look at the example I showed and replace how you're setting up your references
Okno
OknoOP3mo ago
Hey @333fred I removed all the custom parsing logic, using only the provided context and now it seems to be working as expected. Also I need to setup testing with basic reference assemblies. Thank you so much for the help 🙂
333fred
333fred3mo ago
Glad to hear it. Don't forget to mark the thread with $close if there's nothing else
MODiX
MODiX3mo ago
If you have no further questions, please use /close to mark the forum thread as answered

Did you find this page helpful?