diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 5b57d90..3522752 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -21,7 +21,10 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/dotVariant.Generator.Function.Test/DiagnosedResultTest.cs b/src/dotVariant.Generator.Function.Test/DiagnosedResultTest.cs new file mode 100644 index 0000000..084697b --- /dev/null +++ b/src/dotVariant.Generator.Function.Test/DiagnosedResultTest.cs @@ -0,0 +1,59 @@ +// +// Copyright Miro Knejp 2021. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE.txt or copy at https://www.boost.org/LICENSE_1_0.txt) +// + +using dotVariant.Generator; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace dotVariant.Generator.Function.Test; + +public sealed class DiagnosedResultTest +{ + [Test] + public void DefaultHasError() + { + DiagnosedResult res = default; + Assert.That(res.HasErrors); + } + + public static IEnumerable ValueEmptyDiagnosticsNotHasErrorsSource => new object[] { 23, }; + + [Test, TestCaseSource(nameof(ValueEmptyDiagnosticsNotHasErrorsSource))] + public void ValueEmptyDiagnosticsNotHasErrors(object expectedValue) + { + DiagnosedResult res = new(expectedValue); + Assert.That(!res.HasErrors); + Assert.That(res.TryGetValue(out var value)); + Assert.That(value, Is.EqualTo(expectedValue)); + } + + + private static Diagnostic CreateDummyDiagnostic(DiagnosticSeverity severity) + { + var severityName = severity.ToString(); + return Diagnostic.Create( + new DiagnosticDescriptor("0", severityName, severityName, severityName, severity, true), + Location.Create("Test", new TextSpan(0, 1), + new LinePositionSpan(new LinePosition(0, 0), new LinePosition(0, 1)))); + } + + public static IEnumerable HasErrorDiagnosticsHasErrorsSource => + Enum.GetValues().Select(CreateDummyDiagnostic).ToArray(); + + [Test, TestCaseSource(nameof(HasErrorDiagnosticsHasErrorsSource))] + public void ValueHasErrorDiagnosticsHasErrors(Diagnostic diagnostic) + { + var res = default(DiagnosedResult).WithDiagnostics(ImmutableArray.Create(diagnostic)); + var isSeverityError = diagnostic.Severity >= DiagnosticSeverity.Error; + Assert.That(res.HasErrors, Is.EqualTo(isSeverityError)); + Assert.That(res.TryGetValue(out _), Is.EqualTo(!isSeverityError)); + } +} diff --git a/src/dotVariant.Generator.Function.Test/dotVariant.Generator.Function.Test.csproj b/src/dotVariant.Generator.Function.Test/dotVariant.Generator.Function.Test.csproj new file mode 100644 index 0000000..b690da5 --- /dev/null +++ b/src/dotVariant.Generator.Function.Test/dotVariant.Generator.Function.Test.csproj @@ -0,0 +1,25 @@ + + + + net6.0 + enable + 11 + + + + + + + + + + + + + + + + + + + diff --git a/src/dotVariant.Generator.Test/GeneratorTools.cs b/src/dotVariant.Generator.Test/GeneratorTools.cs index cb81d5c..a24ebf1 100644 --- a/src/dotVariant.Generator.Test/GeneratorTools.cs +++ b/src/dotVariant.Generator.Test/GeneratorTools.cs @@ -19,7 +19,7 @@ namespace dotVariant.Generator.Test { internal static class GeneratorTools { - public static Dictionary GetGeneratedOutput(IDictionary sources, Func makeGenerator, bool failOnInvalidSource = false) + public static Dictionary GetGeneratedOutput(IDictionary sources, Func makeGenerator, bool failOnInvalidSource = false) { var compilation = Compile(sources); @@ -39,10 +39,10 @@ public static Dictionary GetGeneratedOutput(IDictionary GetGeneratedOutput(IDictionary sources, bool failOnInvalidSource = false) - where TGenerator : ISourceGenerator, new() + where TGenerator : IIncrementalGenerator, new() => GetGeneratedOutput(sources, () => new TGenerator(), failOnInvalidSource); - public static ImmutableArray GetGeneratorDiagnostics(IDictionary sources, Func makeGenerator) + public static ImmutableArray GetGeneratorDiagnostics(IDictionary sources, Func makeGenerator) { var compilation = Compile(sources); @@ -55,7 +55,7 @@ public static ImmutableArray GetGeneratorDiagnostics(IDictionary GetGeneratorDiagnostics(IDictionary sources) - where TGenerator : ISourceGenerator, new() + where TGenerator : IIncrementalGenerator, new() => GetGeneratorDiagnostics(sources, () => new TGenerator()); public static CSharpCompilation Compile( diff --git a/src/dotVariant.Generator.Test/RenderInfo.cs b/src/dotVariant.Generator.Test/RenderInfo.cs index 553d640..29f66c3 100644 --- a/src/dotVariant.Generator.Test/RenderInfo.cs +++ b/src/dotVariant.Generator.Test/RenderInfo.cs @@ -20,12 +20,11 @@ private static ImmutableArray GetRenderInfoFromCompilation( { var compilation = Compile(SupportSources.Add("input", source), version); var generator = new SourceGenerator(); - var driver = CSharpGeneratorDriver.Create( - new[] { generator }, - optionsProvider: new AnalyzerConfigOptionsProvider(msBuildProperties), - parseOptions: new CSharpParseOptions(version)); + var driver = CSharpGeneratorDriver.Create(generator) + .WithUpdatedAnalyzerConfigOptions(new AnalyzerConfigOptionsProvider(msBuildProperties)) + .WithUpdatedParseOptions(new CSharpParseOptions(version)); _ = driver.RunGeneratorsAndUpdateCompilation(compilation, out var _, out var _); - return generator.RenderInfos; + return DebugInfoCollector.TakeRenderInfoList().ToImmutableArray(); } } } diff --git a/src/dotVariant.Generator/CompilationInfo.cs b/src/dotVariant.Generator/CompilationInfo.cs new file mode 100644 index 0000000..983c474 --- /dev/null +++ b/src/dotVariant.Generator/CompilationInfo.cs @@ -0,0 +1,23 @@ +// +// Copyright Miro Knejp 2021. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE.txt or copy at https://www.boost.org/LICENSE_1_0.txt) +// + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using System; + +namespace dotVariant.Generator; + +public readonly record struct CompilationInfo(LanguageVersion LanguageVersion, bool HasReactive, + INamedTypeSymbol DisposableInterface, bool HasHashCode) +{ + public static CompilationInfo FromCompilation(CSharpCompilation compilation) + { + var hasReactive = compilation.GetTypeByMetadataName("System.Reactive.Linq.Observable") is not null; + var disposableInterface = compilation.GetTypeByMetadataName(typeof(IDisposable).FullName)!; + var hasHashCode = compilation.GetTypeByMetadataName("System.HashCode") is not null; + return new(compilation.LanguageVersion, hasReactive, disposableInterface, hasHashCode); + } +} diff --git a/src/dotVariant.Generator/DebugInfoCollector.cs b/src/dotVariant.Generator/DebugInfoCollector.cs new file mode 100644 index 0000000..53a3be3 --- /dev/null +++ b/src/dotVariant.Generator/DebugInfoCollector.cs @@ -0,0 +1,36 @@ +// +// Copyright Miro Knejp 2021. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE.txt or copy at https://www.boost.org/LICENSE_1_0.txt) +// + +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; + +namespace dotVariant.Generator; + +public static class DebugInfoCollector +{ + private static readonly object _lock = new(); + private static List? _renderInfos; + + [Conditional("DEBUG")] + public static void AddRenderInfo(RenderInfo info) + { + lock (_lock) + { + var renderInfos = _renderInfos ??= new List(); + renderInfos.Add(info); + } + } + + public static List TakeRenderInfoList() + { + lock (_lock) + { + var list = Interlocked.Exchange(ref _renderInfos, null); + return list ?? new List(); + } + } +} diff --git a/src/dotVariant.Generator/Descriptor.cs b/src/dotVariant.Generator/Descriptor.cs index d548a61..37de40c 100644 --- a/src/dotVariant.Generator/Descriptor.cs +++ b/src/dotVariant.Generator/Descriptor.cs @@ -8,19 +8,25 @@ namespace dotVariant.Generator { - public sealed record Descriptor( + public readonly record struct Descriptor( INamedTypeSymbol Type, TypeDeclarationSyntax Syntax, ImmutableArray Options, NullableContext NullableContext) { public static Descriptor FromDeclaration( - INamedTypeSymbol type, - TypeDeclarationSyntax syntax, + SemanticType type, NullableContext nullability) { - var options = Inspect.GetOptions(type); - return new(type, syntax, options, nullability); + var options = Inspect.GetOptions(type.Symbol); + return new(type.Symbol, type.Syntax, options, nullability); } + + public string HintName => $"{Type.ToString() + // If the type contains type parameters replace angle brackets as those are not allowed in AddSource() + .Replace('<', '{') + .Replace('>', '}') + // Escaped names like @class or @event aren't supported either + .Replace('@', '.')}.cs"; } } diff --git a/src/dotVariant.Generator/Diagnose.cs b/src/dotVariant.Generator/Diagnose.cs index 2601bdc..ede5e8e 100644 --- a/src/dotVariant.Generator/Diagnose.cs +++ b/src/dotVariant.Generator/Diagnose.cs @@ -15,11 +15,11 @@ namespace dotVariant.Generator { internal static class Diagnose { - public static IEnumerable Variant(ITypeSymbol type, TypeDeclarationSyntax syntax, CancellationToken token) + public static IEnumerable Variant(SemanticType type, CancellationToken token) => new[] { - CheckType(type, syntax, token), - CheckOptions(type, token), + CheckType(type.Symbol, type.Syntax, token), + CheckOptions(type.Symbol, token), } .Concat(); @@ -155,14 +155,14 @@ public static IEnumerable NotTooManyOptions( ImmutableArray options, CancellationToken token) { - const int max = byte.MaxValue; + const int Max = byte.MaxValue; - if (options.Count() > max) + if (options.Count() > Max) { yield return MakeError( nameof(NotTooManyOptions), - $"'VariantOf()' must not have more than {max} parameters.", - $"Variant types must not have more than {max} parameters in their 'VariantOf()' method.", + $"'VariantOf()' must not have more than {Max} parameters.", + $"Variant types must not have more than {Max} parameters in their 'VariantOf()' method.", LocationOfFirstToken(syntax, SyntaxKind.IdentifierToken)); } } diff --git a/src/dotVariant.Generator/DiagnosedResult.cs b/src/dotVariant.Generator/DiagnosedResult.cs new file mode 100644 index 0000000..218e837 --- /dev/null +++ b/src/dotVariant.Generator/DiagnosedResult.cs @@ -0,0 +1,114 @@ +// +// Copyright Miro Knejp 2021. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE.txt or copy at https://www.boost.org/LICENSE_1_0.txt) +// + +using Microsoft.CodeAnalysis; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace dotVariant.Generator; + +/// A result type carrying values and a if and only if no errors are diagnosed. +/// The type of the value +/// Diagnosed result ignores diagnostics during equality comparison to improve caching consistency. +public readonly struct DiagnosedResult +{ + private readonly bool _noErrors; + + public DiagnosedResult(ImmutableArray diagnostics, TValue value) + { + _noErrors = !diagnostics.HasErrors(); + Diagnostics = diagnostics; + ValueOrDefault = HasErrors ? default : value; + } + + public DiagnosedResult(TValue value) + { + _noErrors = true; + Diagnostics = ImmutableArray.Empty; + ValueOrDefault = value; + } + + private DiagnosedResult(ImmutableArray diagnostics, bool hasErrors, TValue? valueOrDefault) + { + _noErrors = !hasErrors; + Diagnostics = diagnostics; + ValueOrDefault = valueOrDefault; + } + + public readonly ImmutableArray Diagnostics; + public readonly TValue? ValueOrDefault; + + public bool HasErrors => !_noErrors; + + public bool TryGetValue(out TValue value) + { + value = ValueOrDefault!; + return _noErrors; + } + + public DiagnosedResult Select(Func selector) + { + var result = HasErrors ? default : selector(ValueOrDefault!); + return new(Diagnostics, HasErrors, result); + } + + public ImmutableArray> SelectMany(Func> selector) + { + var diagnostics = Diagnostics; + if (HasErrors) + { + // preserve diagnostics + return ImmutableArray.Create(new DiagnosedResult(diagnostics, true, default!)); + } + + var results = selector(ValueOrDefault!); + return results.Select(r => new DiagnosedResult(diagnostics, false, r)).ToImmutableArray(); + } + + public DiagnosedResult WithDiagnostics(ImmutableArray diagnostics) + { + if (Diagnostics.IsDefaultOrEmpty && diagnostics.IsDefaultOrEmpty) + { + return new(ImmutableArray.Empty, HasErrors, ValueOrDefault); + } + + diagnostics = Diagnostics.IsDefaultOrEmpty ? diagnostics : Diagnostics.AddRange(diagnostics); + return new(diagnostics, ValueOrDefault!); + } + + public bool Equals(DiagnosedResult other) + { + return HasErrors == other.HasErrors && EqualityComparer.Default.Equals(ValueOrDefault, other.ValueOrDefault); + } + + public override bool Equals(object? obj) + { + return obj is DiagnosedResult other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + return HasErrors ? 1337 : EqualityComparer.Default.GetHashCode(ValueOrDefault); + } + } +} + +public static class DiagnosedResultExtensions +{ + public static DiagnosedResult AsDiagnosedResult(this TValue value) + { + return new(value); + } + + public static bool HasErrors(this ImmutableArray diagnostics) + { + return diagnostics.Any(static d => d.Severity >= DiagnosticSeverity.Error); + } +} diff --git a/src/dotVariant.Generator/Inspect.cs b/src/dotVariant.Generator/Inspect.cs index 03783ff..b6c426c 100644 --- a/src/dotVariant.Generator/Inspect.cs +++ b/src/dotVariant.Generator/Inspect.cs @@ -62,11 +62,11 @@ public static bool IsAncestorOf(ITypeSymbol ancestor, ITypeSymbol type) public static Accessibility EffectiveAccessibility(ITypeSymbol type) => type.DeclaredAccessibility; - public static bool ImplementsDispose(ITypeSymbol type, CSharpCompilation compilation) + public static bool ImplementsDispose(ITypeSymbol type, INamedTypeSymbol disposableInterface) { var dispose = FindMethod( - compilation.GetTypeByMetadataName($"{nameof(System)}.{nameof(IDisposable)}")!, + disposableInterface, m => m.Name == nameof(IDisposable.Dispose)); return type.FindImplementationForInterfaceMember(dispose!) is not null; } @@ -97,8 +97,8 @@ public static int NumReferenceFields(IParameterSymbol param) public static bool HasImplicitConversionDisabled(IParameterSymbol param) { - const string attributeName = nameof(dotVariant) + "." + nameof(NoImplicitConversionAttribute); - return param.GetAttributes().Any(a => a.AttributeClass?.ToDisplayString() == attributeName); + const string AttributeName = nameof(dotVariant) + "." + nameof(NoImplicitConversionAttribute); + return param.GetAttributes().Any(a => a.AttributeClass?.ToDisplayString() == AttributeName); } public static int NumReferenceFields(ITypeSymbol type) diff --git a/src/dotVariant.Generator/RenderInfo.cs b/src/dotVariant.Generator/RenderInfo.cs index fb2aaf9..f4ad2e8 100644 --- a/src/dotVariant.Generator/RenderInfo.cs +++ b/src/dotVariant.Generator/RenderInfo.cs @@ -22,7 +22,7 @@ namespace dotVariant.Generator /// Properties of the parameters provided to VariantOf. /// Information about the .NET runtime we are generating code for. /// Properties of the variant class. - public sealed record RenderInfo( + public readonly record struct RenderInfo( RenderInfo.LanguageInfo Language, RenderInfo.OptionsInfo Options, ImmutableArray Params, @@ -35,7 +35,7 @@ public sealed record RenderInfo( /// /// Integer of the form ABB where A=major and BB=minor version of the language (i.e. 703 -> 7.3) /// - public sealed record LanguageInfo( + public readonly record struct LanguageInfo( string Nullable, int Version); @@ -43,7 +43,7 @@ public sealed record LanguageInfo( /// The namespace in which to generate extension method implementations. /// If use the global namespace. /// - public sealed record OptionsInfo( + public readonly record struct OptionsInfo( string? ExtensionClassNamespace); /// @@ -52,7 +52,7 @@ public sealed record OptionsInfo( /// /// if namespace is found. /// - public sealed record RuntimeInfo( + public readonly record struct RuntimeInfo( bool HasHashCode, bool HasSystemReactiveLinq); @@ -100,7 +100,7 @@ public sealed record RuntimeInfo( /// /// Contains info about relevant members the user has defined. /// - public sealed record VariantInfo( + public readonly record struct VariantInfo( string? Accessibility, bool CanBeNull, string DiagType, @@ -119,7 +119,7 @@ public sealed record VariantInfo( /// /// if a user-defined exists. /// - public sealed record UserDefinitions( + public readonly record struct UserDefinitions( bool Dispose); /// @@ -128,7 +128,7 @@ public sealed record UserDefinitions( /// /// Identifier of the generic parameter. /// - public sealed record GenericInfo( + public readonly record struct GenericInfo( ImmutableArray Constraints, string Identifier); } @@ -173,7 +173,7 @@ public sealed record GenericInfo( /// /// The fully qualified name of the type including type parameter list, without nullability annotation. /// - public sealed record ParamInfo( + public readonly record struct ParamInfo( bool CanBeNull, string DiagType, bool EmitImplicitCast, @@ -190,14 +190,14 @@ public sealed record ParamInfo( public static RenderInfo FromDescriptor( Descriptor desc, - CSharpCompilation compilation, + CompilationInfo compilation, AnalyzerConfigOptionsProvider options, CancellationToken token) { var maxObjects = desc.Options.Max(NumReferenceFields); var type = desc.Type; var emitNullable = desc.NullableContext.HasFlag(NullableContext.AnnotationsEnabled); - var disposable = compilation.GetTypeByMetadataName(typeof(IDisposable).FullName)!; + var disposable = compilation.DisposableInterface; var paramDescriptors = desc @@ -227,8 +227,8 @@ public static RenderInfo FromDescriptor( ExtensionClassNamespace: ExtensionsNamespace(options, typeNamespace)), Params: paramDescriptors.ToImmutableArray(), Runtime: new( - HasHashCode: compilation.GetTypeByMetadataName("System.HashCode") is not null, - HasSystemReactiveLinq: HasReactive(compilation)), + HasHashCode: compilation.HasHashCode, + HasSystemReactiveLinq: compilation.HasReactive), Variant: new( Accessibility: VariantAccessibility(type), CanBeNull: type.IsReferenceType, @@ -245,7 +245,7 @@ public static RenderInfo FromDescriptor( Type: type.ToDisplayString(TopLevelTypeFormat), UserDefined: new( // If the user defined any method named Dispose() bail out. Too risky! - Dispose: ImplementsDispose(type, compilation) || HasAnyDisposeMethod(type)))); + Dispose: ImplementsDispose(type, compilation.DisposableInterface) || HasAnyDisposeMethod(type)))); } private static string DetermineOutType(IParameterSymbol p, bool emitNullable, LanguageVersion version) @@ -349,9 +349,6 @@ private static int ConvertLanguageVersion(LanguageVersion v) return string.IsNullOrWhiteSpace(value) ? typeNamespace : value; } - private static bool HasReactive(CSharpCompilation compilation) - => compilation.GetTypeByMetadataName("System.Reactive.Linq.Observable") is not null; - public static readonly SymbolDisplayFormat TopLevelTypeFormat = new( globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted, typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameOnly, diff --git a/src/dotVariant.Generator/SourceGenerator.cs b/src/dotVariant.Generator/SourceGenerator.cs index 21eedc6..67b0c61 100644 --- a/src/dotVariant.Generator/SourceGenerator.cs +++ b/src/dotVariant.Generator/SourceGenerator.cs @@ -5,85 +5,96 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Threading; namespace dotVariant.Generator { [Generator] - public sealed class SourceGenerator : ISourceGenerator + public sealed class SourceGenerator : IIncrementalGenerator { - public void Execute(GeneratorExecutionContext context) + public void Initialize(IncrementalGeneratorInitializationContext generatorContext) { - var receiver = (SyntaxReceiver)context.SyntaxContextReceiver!; + var variantDecls = generatorContext.SyntaxProvider.CreateSyntaxProvider((node, ct) => NodeIsTypeDeclaration(node), TryCreateCompilationInfo) + .SelectNotNull() + .Diagnose((tuple, ct) => Diagnose.Variant(tuple.decl.Type, ct).ToImmutableArray()); - var decls = - receiver - .VariantDecls - .Select(decl => (decl.Symbol, decl.Syntax, decl.Nullable, Diags: Diagnose.Variant(decl.Symbol, decl.Syntax, context.CancellationToken))) - .Memoize(); + var descriptors = variantDecls.Select((tuple, _) => + { + var (decl, compInfo) = tuple; + return (desc: Descriptor.FromDeclaration(decl.Type, decl.Nullable), nested: decl.NestingTrace.Select((type, _) => Descriptor.FromDeclaration(type, decl.Nullable)).ToImmutableArray(), compInfo); + }); - decls - .SelectMany(desc => desc.Diags) - .ForEach(context.ReportDiagnostic); + var renderInfos = descriptors.Combine(generatorContext.AnalyzerConfigOptionsProvider).Select( + (tuple, ct) => + { + var (source, analyzerOptionProvider) = tuple; + var (desc, nested, compInfo) = source; + return (desc.HintName, RenderInfo.FromDescriptor(desc, compInfo, analyzerOptionProvider, ct)); + }); - Descriptors = - decls - .Where(decl => !decl.Diags.Any(d => d.Severity == DiagnosticSeverity.Error)) - .Select(decl => Descriptor.FromDeclaration(decl.Symbol, decl.Syntax, decl.Nullable)) - .ToImmutableArray(); + generatorContext.RegisterSourceOutput(renderInfos, (context, source) => + { + source.Diagnostics.ForEach(context.ReportDiagnostic); + if (!source.TryGetValue(out var tuple)) + { + return; + } + var (name, info) = tuple; + DebugInfoCollector.AddRenderInfo(info); + context.AddSource(name, Renderer.Render(info)); + }); + } - RenderInfos = - Descriptors - .Select(desc => RenderInfo.FromDescriptor(desc, (CSharpCompilation)context.Compilation, context.AnalyzerConfigOptions, context.CancellationToken)) - .ToImmutableArray(); + private static (VariantDecl decl, CompilationInfo compInfo)? TryCreateCompilationInfo(GeneratorSyntaxContext context, CancellationToken ct) + { + var sema = context.SemanticModel; + if (context.Node is not TypeDeclarationSyntax syntax) + { + return default; + } - Enumerable - .Zip(Descriptors, RenderInfos, (desc, ri) => (desc, ri)) - .ForEach(v => context.AddSource(SanitizeName(v.desc.Type.ToDisplayString()), Renderer.Render(v.ri))); + if (sema.GetDeclaredSymbol(syntax, ct) is not { } symbol) + { + return default; + } - static string SanitizeName(string name) - => name - // If the contains type parameters replace angle brackets as those are not allowed in AddSource() - .Replace('<', '{') - .Replace('>', '}') - // Escaped names like @class or @event aren't supported either - .Replace('@', '.'); - } + if (sema.Compilation is not CSharpCompilation comp) + { + return default; + } - public ImmutableArray Descriptors { get; private set; } - public ImmutableArray RenderInfos { get; private set; } + if (!symbol.GetAttributes().Any(a => a.AttributeClass?.ToDisplayString() == VariantDecl.AttributeName)) + { + return default; + } - public void Initialize(GeneratorInitializationContext context) - { - context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); + var type = new SemanticType(symbol, syntax); + var decl = new VariantDecl(type, CreateNestingTrace(type, sema), + sema.GetNullableContext(syntax.GetLocation().SourceSpan.Start)); + var compInfo = CompilationInfo.FromCompilation(comp); + return (decl, compInfo).AsNullable(); } - private sealed class SyntaxReceiver : ISyntaxContextReceiver + private static bool NodeIsTypeDeclaration(SyntaxNode node) { - public List<(INamedTypeSymbol Symbol, TypeDeclarationSyntax Syntax, NullableContext Nullable)> VariantDecls { get; } = new(); + return node.Kind() is SyntaxKind.ClassDeclaration + or SyntaxKind.StructDeclaration + or SyntaxKind.RecordDeclaration or SyntaxKind.RecordStructDeclaration; + } - public void OnVisitSyntaxNode(GeneratorSyntaxContext context) + private static ImmutableArray CreateNestingTrace(SemanticType semanticType, SemanticModel semanticModel) + { + var trace = ImmutableArray.CreateBuilder(); + var parent = semanticType.Syntax.Parent; + while (parent is TypeDeclarationSyntax current) { - if (context.Node is not TypeDeclarationSyntax tds || tds.AttributeLists.IsEmpty()) - { - return; - } - - var sema = context.SemanticModel; - - if (sema.GetDeclaredSymbol(tds) is not INamedTypeSymbol symbol) - { - return; - } - - const string attributeName = nameof(dotVariant) + "." + nameof(VariantAttribute); - if (symbol.GetAttributes().Any(a => a.AttributeClass?.ToDisplayString() == attributeName)) - { - VariantDecls.Add((symbol, tds, sema.GetNullableContext(context.Node.GetLocation().SourceSpan.Start))); - } + trace.Add(new(semanticModel.GetDeclaredSymbol(current)!, current)); + parent = current.Parent; } + trace.Reverse(); + return trace.ToImmutable(); } } } diff --git a/src/dotVariant.Generator/ValueProviderExtensions.cs b/src/dotVariant.Generator/ValueProviderExtensions.cs new file mode 100644 index 0000000..e7b7a11 --- /dev/null +++ b/src/dotVariant.Generator/ValueProviderExtensions.cs @@ -0,0 +1,108 @@ +// +// Copyright Miro Knejp 2021. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE.txt or copy at https://www.boost.org/LICENSE_1_0.txt) +// + +using Microsoft.CodeAnalysis; +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace dotVariant.Generator +{ + public static class ValueProviderExtensions + { + public static IncrementalValuesProvider SelectNotNull( + this IncrementalValuesProvider source, Func selector) + where TResult : struct + { + return source.SelectMany((source, ct) => + selector(source, ct) is { } result ? ImmutableArray.Create(result) : ImmutableArray.Empty); + } + + public static IncrementalValuesProvider SelectNotNull(this IncrementalValuesProvider source) + where TSource : struct + { + return source.SelectMany(static (source, _) => + source is { } result ? ImmutableArray.Create(result) : ImmutableArray.Empty); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T? AsNullable(this T value) where T : struct => value; + + public static IncrementalValuesProvider> Select( + this IncrementalValuesProvider> source, + Func selector) + { + return source.Select((result, ct) => + { + return result.Select(s => selector(s, ct)); + }); + } + + public static IncrementalValuesProvider> SelectMany( + this IncrementalValuesProvider> source, + Func> selector) + { + return source.SelectMany((result, ct) => + { + return result.SelectMany(s => selector(s, ct)); + }); + } + + public static IncrementalValuesProvider> Combine( + this IncrementalValuesProvider> left, + IncrementalValueProvider right) + { + return IncrementalValueProviderExtensions.Combine(left, right).Select((tuple, ct) => + { + var (lhs, rhs) = tuple; + return lhs.Select(v => (v, rhs)); + }); + } + + public static IncrementalValuesProvider> Diagnose( + this IncrementalValuesProvider> source, + Func> diagnose) + { + return IncrementalValueProviderExtensions.Select(source, (result, ct) => + { + if (result.TryGetValue(out var value)) + { + var diagnostics = diagnose(value, ct); + return result.WithDiagnostics(diagnostics); + } + + return result; + }); + } + + public static IncrementalValuesProvider> Diagnose( + this IncrementalValuesProvider source, + Func> diagnose) + { + return IncrementalValueProviderExtensions.Select(source, (value, ct) => + { + var diagnostics = diagnose(value, ct); + return new DiagnosedResult(diagnostics, value); + }); + } + + public static void RegisterSourceOutput(this IncrementalGeneratorInitializationContext context, + IncrementalValuesProvider> source, + Action action) + { + context.RegisterSourceOutput(source, (context, value) => + { + value.Diagnostics.ForEach(context.ReportDiagnostic); + if (value.TryGetValue(out var v)) + { + action(context, v); + } + }); + } + } +} diff --git a/src/dotVariant.Generator/VariantDecl.cs b/src/dotVariant.Generator/VariantDecl.cs new file mode 100644 index 0000000..f3eb665 --- /dev/null +++ b/src/dotVariant.Generator/VariantDecl.cs @@ -0,0 +1,19 @@ +// +// Copyright Miro Knejp 2021. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE.txt or copy at https://www.boost.org/LICENSE_1_0.txt) +// + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Collections.Immutable; + +namespace dotVariant.Generator; + +public readonly record struct SemanticType(INamedTypeSymbol Symbol, TypeDeclarationSyntax Syntax); + +public readonly record struct VariantDecl(SemanticType Type, ImmutableArray NestingTrace, + NullableContext Nullable) +{ + public const string AttributeName = nameof(dotVariant) + "." + nameof(VariantAttribute); +} diff --git a/src/dotVariant.Generator/dotVariant.Generator.csproj b/src/dotVariant.Generator/dotVariant.Generator.csproj index 3c42003..1bc3e05 100644 --- a/src/dotVariant.Generator/dotVariant.Generator.csproj +++ b/src/dotVariant.Generator/dotVariant.Generator.csproj @@ -3,8 +3,9 @@ netstandard2.0 enable - 9.0 + 11.0 true + true diff --git a/src/dotVariant.sln b/src/dotVariant.sln index 87af4e9..5010894 100644 --- a/src/dotVariant.sln +++ b/src/dotVariant.sln @@ -28,12 +28,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ ..\.github\workflows\test.yaml = ..\.github\workflows\test.yaml EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotVariant.Generator.Function.Test", "dotVariant.Generator.Function.Test\dotVariant.Generator.Function.Test.csproj", "{42329E06-F134-4254-8089-FC4F121BEF82}" +EndProject Global - GlobalSection(SharedMSBuildProjectFiles) = preSolution - dotVariant.Test\dotVariant.Test.projitems*{1305501c-65ab-4338-848f-3d2b919b7255}*SharedItemsImports = 5 - dotVariant.Test\dotVariant.Test.projitems*{193d7a79-92bf-46be-bb02-60360da0883d}*SharedItemsImports = 13 - dotVariant.Test\dotVariant.Test.projitems*{5fa8381b-0576-498a-9968-006a38839d3c}*SharedItemsImports = 5 - EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU @@ -63,6 +60,10 @@ Global {1305501C-65AB-4338-848F-3D2B919B7255}.Debug|Any CPU.Build.0 = Debug|Any CPU {1305501C-65AB-4338-848F-3D2B919B7255}.Release|Any CPU.ActiveCfg = Release|Any CPU {1305501C-65AB-4338-848F-3D2B919B7255}.Release|Any CPU.Build.0 = Release|Any CPU + {42329E06-F134-4254-8089-FC4F121BEF82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {42329E06-F134-4254-8089-FC4F121BEF82}.Debug|Any CPU.Build.0 = Debug|Any CPU + {42329E06-F134-4254-8089-FC4F121BEF82}.Release|Any CPU.ActiveCfg = Release|Any CPU + {42329E06-F134-4254-8089-FC4F121BEF82}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -70,4 +71,9 @@ Global GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5601C60A-02E6-42A8-B802-79FCCC6CD6C3} EndGlobalSection + GlobalSection(SharedMSBuildProjectFiles) = preSolution + dotVariant.Test\dotVariant.Test.projitems*{1305501c-65ab-4338-848f-3d2b919b7255}*SharedItemsImports = 5 + dotVariant.Test\dotVariant.Test.projitems*{193d7a79-92bf-46be-bb02-60360da0883d}*SharedItemsImports = 13 + dotVariant.Test\dotVariant.Test.projitems*{5fa8381b-0576-498a-9968-006a38839d3c}*SharedItemsImports = 5 + EndGlobalSection EndGlobal diff --git a/src/dotVariant/dotVariant.csproj b/src/dotVariant/dotVariant.csproj index c2e1011..30a2240 100644 --- a/src/dotVariant/dotVariant.csproj +++ b/src/dotVariant/dotVariant.csproj @@ -30,7 +30,7 @@ - +