Article series
- Code sharing of the future
- Better experience through Roslyn Analyzers and Code Fixes
- Testing Source Generators, Roslyn Analyzers and Code Fixes
- Increasing Performance through Harnessing of the Memoization
- Adapt Code Generation Based on Project Dependencies
- Using 3rd-Party Libraries
- Using Additional Files
- High-Level API – ForAttributeWithMetadataName
- Reduction of resource consumption in IDE ⬅
- Configuration
- Logging
More information about the Smart Enums and the source code can be found on GitHub:
- Smart Enums
- Source Code (see commits starting with message “Part 9”)
After a detour to the new high-level in previos article (TODO: link), we will see how to reduce the resource consumption of a Source Generator when running inside an IDE by redirecting the code generation to RegisterImplementationSourceOutput
.
Roslyn API: RegisterImplementationSourceOutput
With newer compiler versions, Roslyn team introduced the method RegisterImplementationSourceOutput
to give developers a means to split the code generation in 2 categories: the code that can be used during development directly and the code which is important during runtime of the application only. If the code must be able to be referenced by the developers, like property Items
generated in the first part of this series, then continue using RegisterSourceOutput
, otherwise use RegisterImplementationSourceOutput
. An IDE has a choice whether to run the pipeline registered with RegisterImplementationSourceOutput
during development in the background or to skip the code generation until build (of the assembly) as a performance optimization.
At the time of writing, as I can see, the IDEs doesn’t treat RegisterImplementationSourceOutput
any different than RegisterSourceOutput
. But this should change in the future to be able to handle growing number of Roslyn Source Generators.
Pipeline for Generation of Translations
The TranslationAttribute
, generated in the part 7, is nothing that can be used by a developer, so, it is a perfect candidate for RegisterImplementationSourceOutput
.
Everything that is related to translations is moved to a new generation pipeline in InitializeTranslationsGenerator
. The pipeline became more fine grained to leverage the efficiency of memoization described in part 4.
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var enumTypes = context.SyntaxProvider
.ForAttributeWithMetadataName("DemoLibrary.EnumGenerationAttribute",
CouldBeEnumerationAsync,
GetEnumInfoOrNull)
.Collect()
.SelectMany((enumInfos, _) => enumInfos.Distinct());
...
InitializeTranslationsGenerator(context, enumTypes);
}
private static void InitializeTranslationsGenerator(
IncrementalGeneratorInitializationContext context,
IncrementalValuesProvider enumTypes)
{
// fetch namespace and name only, so the generation is not triggered anew,
// if something else (unimportant) changes.
var enumNames = enumTypes.Select((t, _) => (t.Namespace, t.Name));
var mergedTranslations = context.AdditionalTextsProvider
.Where(text => text.Path.EndsWith("translations.json",
StringComparison.OrdinalIgnoreCase))
.Select((text, token) => text.GetText(token)?.ToString())
// Parse JSON before generation
.Select((json, _) => ParseTranslations(json))
.Where(translations => !translations.IsEmpty)
.Collect()
// Merge translations from different JSON files
.Select(MergeTranslations);
var translationInfos = enumNames.Combine(mergedTranslations)
.SelectMany((tuple, _) =>
{
var (ns, name) = tuple.Left;
var translationsByClassName = tuple.Right;
// search for the translations of current smart enum,
// it there are none, then don't generate anything
if (!translationsByClassName.TryGetValue(name, out var translations))
return ImmutableArray.Empty;
var translationInfo = new EnumTranslationInfo(ns, name, translations);
return ImmutableArray.Create(translationInfo);
});
context.RegisterImplementationSourceOutput(translationInfos, GenerateCode);
}
EnumTranslationInfo
implements Equals
and GetHashCode
for proper caching.
public readonly struct EnumTranslationInfo : IEquatable
{
public string? Namespace { get; }
public string Name { get; }
public ImmutableDictionary Translations { get; }
public EnumTranslationInfo(
string? ns,
string name,
ImmutableDictionary translations)
{
Namespace = ns;
Name = name;
Translations = translations;
}
// Equals and GetHashCode
}
ImmutableDictionary
to be able to return empty dictionaries without creation of new instances.
private static ImmutableDictionary> ParseTranslations(string? json)
{
if (String.IsNullOrWhiteSpace(json))
return ImmutableDictionary>.Empty;
try
{
return JsonConvert.DeserializeObject>>(json!)
?? ImmutableDictionary>.Empty;
}
catch (Exception)
{
return ImmutableDictionary>.Empty;
}
}
The source generator must merge the translations if there are multiple JSON files with translations.
private static ImmutableDictionary> MergeTranslations(
ImmutableArray>>
collectedTranslations,
CancellationToken cancellationToken)
{
if (collectedTranslations.IsDefaultOrEmpty)
return ImmutableDictionary>.Empty;
if (collectedTranslations.Length == 1)
return collectedTranslations[0];
var mergedTranslations = ImmutableDictionary>.Empty;
foreach (var translationsByClassName in collectedTranslations)
{
cancellationToken.ThrowIfCancellationRequested();
foreach (var kvp in translationsByClassName)
{
try
{
var className = kvp.Key;
var translationByLanguage = kvp.Value;
if (mergedTranslations.TryGetValue(className, out var otherTranslations))
translationByLanguage = translationByLanguage.AddRange(otherTranslations);
mergedTranslations = mergedTranslations.SetItem(className, translationByLanguage);
}
catch (Exception)
{
// Report the error
}
}
}
return mergedTranslations;
}
DemoCodeGenerator
to EnumTranslationsGenerator
.
private static void GenerateCode(
SourceProductionContext context,
EnumTranslationInfo translationInfo)
{
var ns = translationInfo.Namespace is null ? null : $"{translationInfo.Namespace}.";
var code = EnumTranslationsGenerator.Instance.Generate(translationInfo);
if (!String.IsNullOrWhiteSpace(code))
context.AddSource($"{ns}{translationInfo.Name}.Translations.g.cs", code);
}
// -------------------------------
public class EnumTranslationsGenerator
{
public static readonly EnumTranslationsGenerator Instance = new();
public string Generate(EnumTranslationInfo translationsInfo)
{
var ns = translationsInfo.Namespace;
var name = translationsInfo.Name;
var sb = new StringBuilder(@$"//
#nullable enable
{(ns is null ? null : $@"namespace {ns};
")}");
GenerateTranslationAttributes(sb, translationsInfo.Translations);
sb.Append(@$"
partial class {name}
{{
}}
");
return sb.ToString();
}
private static void GenerateTranslationAttributes(
StringBuilder sb,
IReadOnlyDictionary translations)
{
foreach (var kvp in translations.OrderBy(kvp => kvp.Key))
{
sb.Append(@"
[global::DemoLibrary.TranslationAttribute(""").Append(kvp.Key).Append("\", \"").Append(kvp.Value).Append("\")]");
}
}
}
Summary
RegisterImplementationSourceOutput
is treated properly.